├── .gitignore ├── composer.json ├── generate.php ├── LICENSE ├── src ├── Totp.php ├── Base32.php └── Hotp.php ├── tests └── totp-test.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lfkeitel/phptotp", 3 | "description": "TOTP/HOTP library for PHP", 4 | "keywords": [ 5 | "totp", 6 | "hotp", 7 | "two-factor", 8 | "authentication" 9 | ], 10 | "license": "MIT", 11 | "require": { 12 | "php": "^5.6 || ^7.0 || ^8.0" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "lfkeitel\\phptotp\\": "src/" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /generate.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | GenerateToken($key) . "\n"; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Lee Keitel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/Totp.php: -------------------------------------------------------------------------------- 1 | startTime = $start; 14 | $this->timeInterval = $ti; 15 | } 16 | 17 | public function GenerateToken($key, $time = null, $length = 6) 18 | { 19 | // Pad the key if necessary 20 | if ($this->algo === 'sha256') { 21 | $key = $key . substr($key, 0, 12); 22 | } elseif ($this->algo === 'sha512') { 23 | $key = $key . $key . $key . substr($key, 0, 4); 24 | } 25 | 26 | // Get the current unix timestamp if one isn't given 27 | if (is_null($time)) { 28 | $time = (new \DateTime())->getTimestamp(); 29 | } 30 | 31 | // Calculate the count 32 | $now = $time - $this->startTime; 33 | $count = floor($now / $this->timeInterval); 34 | 35 | // Generate a normal HOTP token 36 | return parent::GenerateToken($key, $count, $length); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Base32.php: -------------------------------------------------------------------------------- 1 | 4) { 23 | $remainderSize -= 5; 24 | $c = $remainder & (self::BITS_5_RIGHT << $remainderSize); 25 | $c >>= $remainderSize; 26 | $res .= self::$CHARS[$c]; 27 | } 28 | } 29 | if ($remainderSize > 0) { 30 | // remainderSize < 5: 31 | $remainder <<= (5 - $remainderSize); 32 | $c = $remainder & self::BITS_5_RIGHT; 33 | $res .= self::$CHARS[$c]; 34 | } 35 | 36 | return $res; 37 | } 38 | 39 | public static function decode($data) 40 | { 41 | $data = strtolower($data); 42 | $dataSize = strlen($data); 43 | $buf = 0; 44 | $bufSize = 0; 45 | $res = ''; 46 | 47 | for ($i = 0; $i < $dataSize; $i++) { 48 | $c = $data[$i]; 49 | $b = strpos(self::$CHARS, $c); 50 | if ($b === false) { 51 | throw new \Exception('Encoded string is invalid, it contains unknown char #'.ord($c)); 52 | } 53 | $buf = ($buf << 5) | $b; 54 | $bufSize += 5; 55 | if ($bufSize > 7) { 56 | $bufSize -= 8; 57 | $b = ($buf & (0xff << $bufSize)) >> $bufSize; 58 | $res .= chr($b); 59 | } 60 | } 61 | 62 | return $res; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/totp-test.php: -------------------------------------------------------------------------------- 1 | $token) { 28 | $t = $hotp->GenerateToken($key, $count); 29 | if ($t != $token) { 30 | echo "Count $count: Expected $token, got $t\n"; 31 | $hotp_failed = true; 32 | } 33 | } 34 | 35 | if (!$hotp_failed) { 36 | echo "HOTP tests passed\n"; 37 | } 38 | 39 | // Test data from RFC 6238 40 | // time, sha1, sha256, sha512 41 | $totp_tests = [ 42 | [59, '94287082', '46119246', '90693936'], 43 | [1111111109, '07081804', '68084774', '25091201'], 44 | [1111111111, '14050471', '67062674', '99943326'], 45 | [1234567890, '89005924', '91819424', '93441116'], 46 | [2000000000, '69279037', '90698825', '38618901'], 47 | [20000000000, '65353130', '77737706', '47863826'], 48 | ]; 49 | 50 | $totp_length = 8; 51 | 52 | $totp1 = new Totp(); 53 | $totp256 = new Totp('sha256'); 54 | $totp512 = new Totp('sha512'); 55 | 56 | $totp_failed = false; 57 | foreach ($totp_tests as $test) { 58 | $sha1 = $totp1->GenerateToken($key, $test[0], $totp_length); 59 | if ($sha1 != $test[1]) { 60 | $totp_failed = true; 61 | echo "SHA1: Time {$test[0]}. Expected {$test[1]}, got $sha1\n"; 62 | } 63 | 64 | $sha256 = $totp256->GenerateToken($key, $test[0], $totp_length); 65 | if ($sha256 != $test[2]) { 66 | $totp_failed = true; 67 | echo "SHA256: Time {$test[0]}. Expected {$test[2]}, got $sha256\n"; 68 | } 69 | 70 | $sha512 = $totp512->GenerateToken($key, $test[0], $totp_length); 71 | if ($sha512 != $test[3]) { 72 | $totp_failed = true; 73 | echo "SHA512: Time {$test[0]}. Expected {$test[3]}, got $sha512\n"; 74 | } 75 | } 76 | 77 | if (!$totp_failed) { 78 | echo "TOTP tests passed\n"; 79 | } 80 | 81 | if ($hotp_failed || $totp_failed) { 82 | exit(1); 83 | } 84 | -------------------------------------------------------------------------------- /src/Hotp.php: -------------------------------------------------------------------------------- 1 | algo = $algo; 12 | } 13 | 14 | public function GenerateToken($key, $count = 0, $length = 6) 15 | { 16 | $count = $this->packCounter($count); 17 | $hash = hash_hmac($this->algo, $count, $key); 18 | $code = $this->genHTOPValue($hash, $length); 19 | 20 | $code = str_pad($code, $length, "0", STR_PAD_LEFT); 21 | $code = substr($code, (-1 * $length)); 22 | 23 | return $code; 24 | } 25 | 26 | private function packCounter($counter) 27 | { 28 | // the counter value can be more than one byte long, 29 | // so we need to pack it down properly. 30 | $cur_counter = array(0, 0, 0, 0, 0, 0, 0, 0); 31 | for ($i = 7; $i >= 0; $i--) { 32 | $cur_counter[$i] = pack('C*', $counter); 33 | $counter = $counter >> 8; 34 | } 35 | 36 | $bin_counter = implode($cur_counter); 37 | 38 | // Pad to 8 chars 39 | if (strlen($bin_counter) < 8) { 40 | $bin_counter = str_repeat(chr(0), 8 - strlen($bin_counter)) . $bin_counter; 41 | } 42 | 43 | return $bin_counter; 44 | } 45 | 46 | private function genHTOPValue($hash, $length) 47 | { 48 | // store calculate decimal 49 | $hmac_result = []; 50 | 51 | // Convert to decimal 52 | foreach (str_split($hash, 2) as $hex) { 53 | $hmac_result[] = hexdec($hex); 54 | } 55 | 56 | $offset = (int)$hmac_result[count($hmac_result)-1] & 0xf; 57 | 58 | $code = (int)($hmac_result[$offset] & 0x7f) << 24 59 | | ($hmac_result[$offset+1] & 0xff) << 16 60 | | ($hmac_result[$offset+2] & 0xff) << 8 61 | | ($hmac_result[$offset+3] & 0xff); 62 | 63 | return $code % pow(10, $length); 64 | } 65 | 66 | public static function GenerateSecret($length = 16) 67 | { 68 | if ($length % 8 != 0) { 69 | throw new \Exception("Length must be a multiple of 8"); 70 | } 71 | 72 | $secret = openssl_random_pseudo_bytes($length, $strong); 73 | if (!$strong) { 74 | throw new \Exception("Random string generation was not strong"); 75 | } 76 | 77 | return $secret; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HOTP/TOTP Token Generator 2 | 3 | This is a simple PHP library and script that will generate HOTP and TOTP tokens. The library fully conforms to RFCs 4226 and 6238. All hashing algorithms are supported as well as the length of a token and the start time for TOTP. 4 | 5 | ## Installation 6 | 7 | Add the following to your composer.json: 8 | 9 | ``` 10 | { 11 | "require": { 12 | "lfkeitel/phptotp": "^1.0" 13 | } 14 | } 15 | ``` 16 | 17 | And run `composer install`. 18 | 19 | ## Usage 20 | 21 | ```php 22 | GenerateToken($secret); 36 | 37 | # Check if user submitted correct key 38 | if ($user_submitted_key !== $key) { 39 | exit(); 40 | } 41 | ``` 42 | 43 | ## Documentation 44 | 45 | ### lfkeitel\phptotp\Totp extends Hotp 46 | 47 | - `__construct($algo = 'sha1', $start = 0, $ti = 30): Totp` 48 | - `$algo`: Algorithm to use when generating token 49 | - `$start`: The beginning of time 50 | - `$ti`: Time interval between tokens 51 | - `GenerateToken($key, $time = null, $length = 6): string` 52 | - `$key`: Secret key as bytes, base32 decoded 53 | - `$time`: Time to use for the token, defaults to now 54 | - `$length`: Length of token 55 | 56 | ### lfkeitel\phptotp\Hotp 57 | 58 | - `__construct($algo = 'sha1'): Hotp` 59 | - `$algo`: Algorithm to use when generating token 60 | - `GenerateToken($key, $count = 0, $length = 6): string` 61 | - `$key`: Secret key as bytes, base32 decoded 62 | - `$count`: HOTP counter 63 | - `$length`: Length of token 64 | - `GenerateSecret($length = 16): string` 65 | - `$length`: Length of string in bytes 66 | - `Return`: This method returns a string of random bytes, use Base32::encode when displaying to the user. 67 | 68 | ### lfkeitel\phptotp\Base32 69 | 70 | - `static encode($data): string` 71 | - `$data`: Data to base32 encode 72 | - `static decode($data): string` 73 | - `$data`: Data to base32 decode 74 | 75 | ## generate.php 76 | 77 | generate.php is a script that acts exactly like Google Authenticator. It takes a secret key, either as an argument or can be entered when prompted on standard input, and generates a token assuming SHA1, Unix timestamp for start, and 30 second time intervals. The secret key should be base32 encoded. 78 | 79 | ```php 80 | $ ./generate.php 81 | Enter secret key: turtles 82 | Token: 338914 83 | $ 84 | ``` 85 | 86 | ## License 87 | 88 | This software is released under the MIT license which can be found in LICENSE. 89 | --------------------------------------------------------------------------------