From 17e65d640991092c42badd47c22b36504f485d95 Mon Sep 17 00:00:00 2001 From: Mustafa Talaeezadeh Khouzani Date: Sun, 4 Jan 2015 19:26:12 +0330 Subject: [PATCH] Initial commit --- .gitignore | 4 + .travis.yml | 13 + LICENSE | 22 ++ README.md | 17 + composer.json | 25 ++ phpunit.xml | 18 + public/.gitkeep | 0 src/MrAudioGuy/Oath/Base32.php | 322 ++++++++++++++++++ .../Oath/BaseConverterInterface.php | 104 ++++++ src/MrAudioGuy/Oath/Facades/Base32.php | 14 + src/MrAudioGuy/Oath/Facades/Oath.php | 14 + src/MrAudioGuy/Oath/Oath.php | 222 ++++++++++++ src/MrAudioGuy/Oath/OathServiceProvider.php | 58 ++++ src/config/.gitkeep | 0 src/controllers/.gitkeep | 0 src/lang/.gitkeep | 0 src/migrations/.gitkeep | 0 src/views/.gitkeep | 0 tests/.gitkeep | 0 19 files changed, 833 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 public/.gitkeep create mode 100644 src/MrAudioGuy/Oath/Base32.php create mode 100644 src/MrAudioGuy/Oath/BaseConverterInterface.php create mode 100644 src/MrAudioGuy/Oath/Facades/Base32.php create mode 100644 src/MrAudioGuy/Oath/Facades/Oath.php create mode 100644 src/MrAudioGuy/Oath/Oath.php create mode 100644 src/MrAudioGuy/Oath/OathServiceProvider.php create mode 100644 src/config/.gitkeep create mode 100644 src/controllers/.gitkeep create mode 100644 src/lang/.gitkeep create mode 100644 src/migrations/.gitkeep create mode 100644 src/views/.gitkeep create mode 100644 tests/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5826402 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.phar +composer.lock +.DS_Store diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f60bbe0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - hhvm + +before_script: + - travis_retry composer self-update + - travis_retry composer install --prefer-source --no-interaction --dev + +script: phpunit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1bacf7b --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Mustafa Talaeezadeh Khouzani + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..55f7b46 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +Oath +==== + +Oath is a One Time Password library mostly known as authenticators in Google's Two-Step Verification and similar products. It covers bose HOTP and TOTP based on their RFC descriptions. + + +Description of Oath +=================== + +It implements the Two Step Authentication specified in RFC6238 @ http://tools.ietf.org/html/rfc6238 using OATH and compatible with Google Authenticator App for android. It uses a 3rd party class called Base32 for RFC3548 base 32 encode/decode. Feel free to use better adjusted implementation. + +Special Thanks goes to +====================== + +phil@idontplaydarts.com for this article https://www.idontplaydarts.com/2011/07/google-totp-two-factor-authentication-for-php/ +Wikipedia.org for this article http://en.wikipedia.org/wiki/Google_Authenticator +devicenull@github.com for this class https://github.com/devicenull/PHP-Google-Authenticator/blob/master/base32.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3a62003 --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "mr-audio-guy/oath", + "description": "The TOTP based on [RFC6238](https://tools.ietf.org/html/rfc6238)", + "license": "MIT", + "keywords": ["oath", "totp", "authenticator", "google authenticator"], + "authors": [ + { + "name": "Mustafa Talaeezadeh Khouzani", + "email": "mustafa.t@780.ir" + } + ], + "require": { + "php": ">=5.5.0", + "illuminate/support": "4.2.*" + }, + "autoload": { + "classmap": [ + "src/migrations" + ], + "psr-0": { + "MrAudioGuy\\Oath\\": "src/" + } + }, + "minimum-stability": "stable" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..3347b75 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/MrAudioGuy/Oath/Base32.php b/src/MrAudioGuy/Oath/Base32.php new file mode 100644 index 0000000..25fc7f0 --- /dev/null +++ b/src/MrAudioGuy/Oath/Base32.php @@ -0,0 +1,322 @@ + 0) + { + throw new Exception('Length must be divisible by 8'); + } + if (!preg_match('/^[01]+$/', $str)) + { + throw new Exception('Only 0\'s and 1\'s are permitted'); + } + + preg_match_all('/.{8}/', $str, $chrs); + $chrs = array_map('bindec', $chrs[0]); + // I'm just being slack here + array_unshift($chrs, 'C*'); + + return call_user_func_array('pack', $chrs); + } + + /** + * fromBin + * + * Converts a correct binary string to base32 + * + * @param string $str The string of 0's and 1's you want to convert + * + * @return string String encoded as base32 + * @throws exception + */ + public static function fromBin ($str) + { + if (strlen($str) % 8 > 0) + { + throw new Exception('Length must be divisible by 8'); + } + if (!preg_match('/^[01]+$/', $str)) + { + throw new Exception('Only 0\'s and 1\'s are permitted'); + } + + // Base32 works on the first 5 bits of a byte, so we insert blanks to pad it out + $str = preg_replace('/(.{5})/', '000$1', $str); + + // We need a string divisible by 5 + $length = strlen($str); + $rbits = $length & 7; + + if ($rbits > 0) + { + // Excessive bits need to be padded + $ebits = substr($str, $length - $rbits); + $str = substr($str, 0, $length - $rbits); + $str .= "000$ebits" . str_repeat('0', 5 - strlen($ebits)); + } + + preg_match_all('/.{8}/', $str, $chrs); + $chrs = array_map('static::_mapcharset', $chrs[0]); + + return join('', $chrs); + } + + /** + * toBin + * + * Accepts a base32 string and returns an ascii binary string + * + * @throws \Exception Must mach character set + * + * @param string $str The base32 string to convert + * + * @return string Ascii binary string + */ + public static function toBin ($str) + { + if (!preg_match('/^[' . static::$_charset . ']+$/', $str)) + { + throw new \Exception('Must match character set'); + } + + // Convert the base32 string back to a binary string + $str = join('', array_map('static::_mapbin', str_split($str))); + // Remove the extra 0's we added + $str = preg_replace('/000(.{5})/', '$1', $str); + // Unpad if nessicary + $length = strlen($str); + $rbits = $length & 7; + if ($rbits > 0) + { + $str = substr($str, 0, $length - $rbits); + } + + return $str; + } + + /** + * fromString + * + * Convert any string to a base32 string + * This should be binary safe... + * + * @param string $str The string to convert + * + * @return string The converted base32 string + */ + public static function fromString ($str) + { + return static::fromBin(static::str2bin($str)); + } + + /** + * toString + * + * Convert any base32 string to a normal sctring + * This should be binary safe... + * + * @param string $str The base32 string to convert + * + * @return string The normal string + */ + public static function toString ($str) + { + $str = strtoupper($str); + + // csSave actually has to be able to consider extra characters + if (static::$_charset == self::csSafe) + { + $str = str_replace('O', '0', $str); + $str = str_replace(['I', 'L'], '1', $str); + } + + return static::bin2str(static::tobin($str)); + } + + /** + * _mapcharset + * + * Used with array_map to map the bits from a binary string + * directly into a base32 character set + * + * @access private + * + * @param string $str The string of 0's and 1's you want to convert + * + * @return char Resulting base32 character + */ + private static function _mapcharset ($str) + { + return static::$_charset[bindec($str)]; + } + + /** + * _mapbin + * + * Used with array_map to map the characters from a base32 + * character set directly into a binary string + * + * @access private + * + * @param char $chr The caracter to map + * + * @return str String of 0's and 1's + */ + private static function _mapbin ($chr) + { + return sprintf('%08b', strpos(static::$_charset, $chr)); + } + + /** + * setCharset + * + * Used to set the internal _charset variable + * I've left it so that people can arbirtrarily set their + * own charset + * + * Can be called with: + * * Base32::csRFC3548 + * * Base32::csSafe + * * Base32::cs09AV + * + * @param string $charset The character set you want to use + * + * @throws Exception + */ + public static function setCharset ($charset = self::csRFC3548) + { + if (strlen($charset) == 32) + { + static::$_charset = strtoupper($charset); + } + else + { + throw new Exception('Length must be exactly 32'); + } + } + } \ No newline at end of file diff --git a/src/MrAudioGuy/Oath/BaseConverterInterface.php b/src/MrAudioGuy/Oath/BaseConverterInterface.php new file mode 100644 index 0000000..8ff8611 --- /dev/null +++ b/src/MrAudioGuy/Oath/BaseConverterInterface.php @@ -0,0 +1,104 @@ + + */ + + namespace MrAudioGuy\Oath; + + defined("OATH_TOTP") ?: define("OATH_TOTP", 'totp', true); + defined("OATH_HOTP") ?: define("OATH_HOTP", 'hotp', true); + + class Oath + { + /** + * @const string TOTP type descriptor + */ + const TOTP = 'totp'; + + /** + * @const string HOTP type descriptor + */ + const HOTP = 'hotp'; + + /** + * + * @var BaseConverterInterface Converter class for RFC3548 base 32 conversion + */ + protected static $converter; + + /** + * + * @var String type of one time password. 'totp' is default + * totp: time-based one time password + * hotp: counter-based one time password + */ + protected static $type; + + /** + * + * @var string Shared secret for HMAC + */ + public static $secret; + + /** + * + * @var string The issuer of oath QR code generator + */ + public static $issuer; + + /** + * + * @var string Account name for distinction. recommended to used as account@domain combination. + */ + public static $account; + + /** + * + * @var string Domain name for distinction. recommended to used as account@domain combination. + */ + public static $domain; + + /** + * + * @var string A url linking to the oath QR code provider. It is concatenated with oath combination compatible + * with Google Authenticator to generate live QR codes. + */ + public static $qrURL; + + /** + * Generates a new secret + * + * @param string $message Used as secret for a hash to generate a shared secret. + * @param int $length The length of the shared key (minus salt). Default is 50 (resulting secret of + * size 80). + * @param int $iterations Iterations of the hash algorithm. Default is 10. + * @param string $algorithm The hash algorithm. + * + * @return string Shared secret key + */ + public static function Secret ($message = null, $length = 50, $iterations = 10, $algorithm = "sha512") + { + if (empty($message)) + { + mt_srand(microtime(true)); + $message = mt_rand(); + } + if (empty($length)) + { + $length = 50; + } + if (empty($iterations)) + { + $iterations = 10; + } + if (empty($algorithm)) + { + $algorithm = "sha512"; + } + $message = hash_pbkdf2($algorithm, $message, $message, $iterations, $length, true); + + // Base32 conversion, Use the appropriate base32 converter method here to transform secret TO base32 + return static::$converter->fromString($message); + } + + /** + * Returns a live QR code. + * + * @param string $secret Shared Secret Key + * @param string $account + * @param string $domain + * @param string $issuer + * @param string $type + * + * @return string URL to the live QR code generator + */ + public static function getQrUrl ($secret = null, $account = null, $domain = null, $issuer = null, $type = null) + { + if (empty($type)) + { + $type = self::$type; + } + if (empty($issuer)) + { + $issuer = self::$issuer; + } + if (empty($account)) + { + $account = self::$account; + } + if (empty($domain)) + { + $domain = self::$domain; + } + if (empty($secret)) + { + $secret = ""; + } + + return static::$qrURL . "otpauth://$type/$issuer%3A$account@$domain?secret=$secret&issuer=$issuer"; + } + + /** + * Generates a 6 digit code for authentication. + * + * @param string $secret Shared Secret Key + * @param int $interval The code generation interval in seconds. Default is 30. + * @param string $algorithm The hmac algorithm. Default is sha1. + * + * @return int 6 digit authentication code. + */ + public static function Generate ($secret, $interval = 30, $algorithm = 'sha1') + { + $key = static::$converter->toString($secret); + $message = floor(microtime(true) / $interval); + $message = pack('N*', 0) . pack('N*', $message); + $hash = hash_hmac($algorithm, $message, $key, true); + $offset = ord($hash[19]) & 0xf; + + $otp = ( + ((ord($hash[$offset + 0]) & 0x7f) << 24) | + ((ord($hash[$offset + 1]) & 0xff) << 16) | + ((ord($hash[$offset + 2]) & 0xff) << 8) | + (ord($hash[$offset + 3]) & 0xff) + ) % pow(10, 6); + + return $otp; + } + + /** + * Checks if the code is valid + * + * @param string $secret Shared Secret Key + * @param int $code 6 digit authentication code. + * + * @return bool True if succeeds, false if otherwise. + */ + public static function check ($secret, $code) + { + if (static::Generate($secret) === $code) + { + return true; + } + + return false; + } + + /** + * Default constructor + * + * @param BaseConverterInterface $baseConverter Converter + * @param string $type OTP type + * @param string $issuer Issuer + * @param string $account Account + * @param string $domain Domain + * @param string $qrURL Base url for qr-code generator + */ + public static function __construct (BaseConverterInterface $baseConverter, $type = OATH_TOTP, $issuer = '', + $account = '', $domain = '', + $qrURL = 'https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=') + { + static::$converter = $baseConverter; + static::$type = $type; + static::$issuer = $issuer; + static::$account = $account; + static::$domain = $domain; + static::$qrURL = $qrURL; + //static::$secret = static::$Secret(); + } + } \ No newline at end of file diff --git a/src/MrAudioGuy/Oath/OathServiceProvider.php b/src/MrAudioGuy/Oath/OathServiceProvider.php new file mode 100644 index 0000000..f18b6ff --- /dev/null +++ b/src/MrAudioGuy/Oath/OathServiceProvider.php @@ -0,0 +1,58 @@ +package('mr-audio-guy/oath'); + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + // + $this->app->booting(function() + { + $loader = \Illuminate\Foundation\AliasLoader::getInstance(); + $loader->alias('Oath', 'MrAudioGuy\Commons\Facades\Oath'); + $loader->alias('Base32', 'MrAudioGuy\Commons\Facades\Base32'); + }); + $this->app['baseConverter'] = $this->app->share(function($app) + { + return new Base32(); + }); + $this->app['oath'] = $this->app->share(function($app) + { + return new Oath($app['baseConverter']); + }); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return array('baseConverter','oath'); + } + +} diff --git a/src/config/.gitkeep b/src/config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/.gitkeep b/src/controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/lang/.gitkeep b/src/lang/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/migrations/.gitkeep b/src/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/views/.gitkeep b/src/views/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29