diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/Providers/Qr/QRException.php | 10 | ||||
-rw-r--r-- | lib/Providers/Rng/RNGException.php | 10 | ||||
-rw-r--r-- | lib/TwoFactorAuth.php | 159 | ||||
-rw-r--r-- | lib/TwoFactorAuthException.php | 10 |
4 files changed, 91 insertions, 98 deletions
diff --git a/lib/Providers/Qr/QRException.php b/lib/Providers/Qr/QRException.php index 1eba26d..c28e829 100644 --- a/lib/Providers/Qr/QRException.php +++ b/lib/Providers/Qr/QRException.php @@ -1,11 +1,5 @@ <?php -namespace RobThree\Auth\Providers\Qr; +use RobThree\Auth\TwoFactorAuthException; -class QRException extends \Exception -{ - function __construct($message = "", $code = 0, $exception = null) - { - parent::__construct($message, $code, $exception); - } -}
\ No newline at end of file +class QRException extends TwoFactorAuthException {}
\ No newline at end of file diff --git a/lib/Providers/Rng/RNGException.php b/lib/Providers/Rng/RNGException.php index 48ecdd8..eb5e913 100644 --- a/lib/Providers/Rng/RNGException.php +++ b/lib/Providers/Rng/RNGException.php @@ -1,11 +1,5 @@ <?php -namespace RobThree\Auth\Providers\Rng; +use RobThree\Auth\TwoFactorAuthException; -class RNGException extends \Exception -{ - function __construct($message = "", $code = 0, $exception = null) - { - parent::__construct($message, $code, $exception); - } -}
\ No newline at end of file +class RNGException extends TwoFactorAuthException {}
\ No newline at end of file diff --git a/lib/TwoFactorAuth.php b/lib/TwoFactorAuth.php index e30b711..410e61c 100644 --- a/lib/TwoFactorAuth.php +++ b/lib/TwoFactorAuth.php @@ -1,103 +1,78 @@ <?php - namespace RobThree\Auth; +use RobThree\Auth\Providers\Qr\IQRCodeProvider; +use RobThree\Auth\Providers\Rng\IRNGProvider; + // Based on / inspired by: https://github.com/PHPGangsta/GoogleAuthenticator // Algorithms, digits, period etc. explained: https://github.com/google/google-authenticator/wiki/Key-Uri-Format -class TwoFactorAuth +class TwoFactorAuth { private $algorithm; private $period; private $digits; private $issuer; - private $qrcodeprovider; - private $rngprovider; + private $qrcodeprovider = null; + private $rngprovider = null; private static $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567='; private static $_base32; private static $_base32lookup = array(); private static $_supportedalgos = array('sha1', 'sha256', 'sha512', 'md5'); - - function __construct($issuer = null, $digits = 6, $period = 30, $algorithm = 'sha1', $qrcodeprovider = null, $rngprovider = null) + + function __construct($issuer = null, $digits = 6, $period = 30, $algorithm = 'sha1', IQRCodeProvider $qrcodeprovider = null, IRNGProvider $rngprovider = null) { $this->issuer = $issuer; - if (!is_int($digits) || $digits <= 0) throw new TwoFactorAuthException('Digits must be int > 0'); $this->digits = $digits; - + if (!is_int($period) || $period <= 0) throw new TwoFactorAuthException('Period must be int > 0'); $this->period = $period; - + $algorithm = strtolower(trim($algorithm)); if (!in_array($algorithm, self::$_supportedalgos)) throw new TwoFactorAuthException('Unsupported algorithm: ' . $algorithm); $this->algorithm = $algorithm; - - // Set default QR Code provider if none was specified - if ($qrcodeprovider==null) - $qrcodeprovider = new Providers\Qr\GoogleQRCodeProvider(); - - if (!($qrcodeprovider instanceof Providers\Qr\IQRCodeProvider)) - throw new TwoFactorAuthException('QRCodeProvider must implement IQRCodeProvider'); - $this->qrcodeprovider = $qrcodeprovider; - - // Try to find best available RNG provider if none was specified - if ($rngprovider==null) { - if (function_exists('random_bytes')) { - $rngprovider = new Providers\Rng\CSRNGProvider(); - } elseif (function_exists('mcrypt_create_iv')) { - $rngprovider = new Providers\Rng\MCryptRNGProvider(); - } elseif (function_exists('openssl_random_pseudo_bytes')) { - $rngprovider = new Providers\Rng\OpenSSLRNGProvider(); - } elseif (function_exists('hash')) { - $rngprovider = new Providers\Rng\HashRNGProvider(); - } else { - throw new TwoFactorAuthException('Unable to find a suited RNGProvider'); - } - } - - if (!($rngprovider instanceof Providers\Rng\IRNGProvider)) - throw new TwoFactorAuthException('RNGProvider must implement IRNGProvider'); - $this->rngprovider = $rngprovider; - + self::$_base32 = str_split(self::$_base32dict); self::$_base32lookup = array_flip(self::$_base32); } - + /** * Create a new secret */ - public function createSecret($bits = 80, $requirecryptosecure = true) + public function createSecret($bits = 80, $requirecryptosecure = true) { $secret = ''; $bytes = ceil($bits / 5); //We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32) - if ($requirecryptosecure && !$this->rngprovider->isCryptographicallySecure()) + $rngprovider = $this->getRngprovider(); + if ($requirecryptosecure && !$rngprovider->isCryptographicallySecure()) throw new TwoFactorAuthException('RNG provider is not cryptographically secure'); - $rnd = $this->rngprovider->getRandomBytes($bytes); + $rnd = $rngprovider->getRandomBytes($bytes); for ($i = 0; $i < $bytes; $i++) $secret .= self::$_base32[ord($rnd[$i]) & 31]; //Mask out left 3 bits for 0-31 values return $secret; } - + /** * Calculate the code with given secret and point in time */ public function getCode($secret, $time = null) { $secretkey = $this->base32Decode($secret); - + $timestamp = "\0\0\0\0" . pack('N*', $this->getTimeSlice($this->getTime($time))); // Pack time into binary string $hashhmac = hash_hmac($this->algorithm, $timestamp, $secretkey, true); // Hash it with users secret key $hashpart = substr($hashhmac, ord(substr($hashhmac, -1)) & 0x0F, 4); // Use last nibble of result as index/offset and grab 4 bytes of the result $value = unpack('N', $hashpart); // Unpack binary value $value = $value[1] & 0x7FFFFFFF; // Drop MSB, keep only 31 bits - + return str_pad($value % pow(10, $this->digits), $this->digits, '0', STR_PAD_LEFT); } - + /** * Check if the code is correct. This will accept codes starting from ($discrepancy * $period) sec ago to ($discrepancy * period) sec from now */ @@ -105,57 +80,57 @@ class TwoFactorAuth { $result = false; $timetamp = $this->getTime($time); - + // To keep safe from timing-attachs we iterate *all* possible codes even though we already may have verified a code is correct - for ($i = -$discrepancy; $i <= $discrepancy; $i++) + for ($i = -$discrepancy; $i <= $discrepancy; $i++) $result |= $this->codeEquals($this->getCode($secret, $timetamp + ($i * $this->period)), $code); - + return (bool)$result; } - + /** * Timing-attack safe comparison of 2 codes (see http://blog.ircmaxell.com/2014/11/its-all-about-time.html) */ private function codeEquals($safe, $user) { if (function_exists('hash_equals')) { return hash_equals($safe, $user); - } else { - // In general, it's not possible to prevent length leaks. So it's OK to leak the length. The important part is that - // we don't leak information about the difference of the two strings. - if (strlen($safe)===strlen($user)) { - $result = 0; - for ($i = 0; $i < strlen($safe); $i++) - $result |= (ord($safe[$i]) ^ ord($user[$i])); - return $result === 0; - } + } + // In general, it's not possible to prevent length leaks. So it's OK to leak the length. The important part is that + // we don't leak information about the difference of the two strings. + if (strlen($safe)===strlen($user)) { + $result = 0; + for ($i = 0; $i < strlen($safe); $i++) + $result |= (ord($safe[$i]) ^ ord($user[$i])); + return $result === 0; } return false; } - + /** * Get data-uri of QRCode */ - public function getQRCodeImageAsDataUri($label, $secret, $size = 200) + public function getQRCodeImageAsDataUri($label, $secret, $size = 200) { if (!is_int($size) || $size <= 0) throw new TwoFactorAuthException('Size must be int > 0'); - + + $qrcodeprovider = $this->getQrCodeProvider(); return 'data:' - . $this->qrcodeprovider->getMimeType() + . $qrcodeprovider->getMimeType() . ';base64,' - . base64_encode($this->qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size)); + . base64_encode($qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size)); } - - private function getTime($time) + + private function getTime($time) { return ($time === null) ? time() : $time; } - - private function getTimeSlice($time = null, $offset = 0) + + private function getTimeSlice($time = null, $offset = 0) { return (int)floor($time / $this->period) + ($offset * $this->period); } - + /** * Builds a string to be encoded in a QR code */ @@ -168,27 +143,61 @@ class TwoFactorAuth . '&algorithm=' . rawurlencode(strtoupper($this->algorithm)) . '&digits=' . intval($this->digits); } - + private function base32Decode($value) { if (strlen($value)==0) return ''; - + if (preg_match('/[^'.preg_quote(self::$_base32dict).']/', $value) !== 0) throw new TwoFactorAuthException('Invalid base32 string'); - + $buffer = ''; - foreach (str_split($value) as $char) + foreach (str_split($value) as $char) { if ($char !== '=') $buffer .= str_pad(decbin(self::$_base32lookup[$char]), 5, 0, STR_PAD_LEFT); } $length = strlen($buffer); $blocks = trim(chunk_split(substr($buffer, 0, $length - ($length % 8)), 8, ' ')); - + $output = ''; foreach (explode(' ', $blocks) as $block) $output .= chr(bindec(str_pad($block, 8, 0, STR_PAD_RIGHT))); - return $output; } -} + /** + * @return IQRCodeProvider + * @throws TwoFactorAuthException + */ + public function getQrCodeProvider() + { + // Set default QR Code provider if none was specified + if (null === $this->qrcodeprovider) { + return $this->qrcodeprovider = new Providers\Qr\GoogleQRCodeProvider(); + } + return $this->qrcodeprovider; + } + /** + * @return IRNGProvider + * @throws TwoFactorAuthException + */ + public function getRngprovider() + { + if (null !== $this->rngprovider) { + return $this->rngprovider; + } + if (function_exists('random_bytes')) { + return $this->rngprovider = new Providers\Rng\CSRNGProvider(); + } + if (function_exists('mcrypt_create_iv')) { + return $this->rngprovider = new Providers\Rng\MCryptRNGProvider(); + } + if (function_exists('openssl_random_pseudo_bytes')) { + return $this->rngprovider = new Providers\Rng\OpenSSLRNGProvider(); + } + if (function_exists('hash')) { + return $this->rngprovider = new Providers\Rng\HashRNGProvider(); + } + throw new TwoFactorAuthException('Unable to find a suited RNGProvider'); + } +}
\ No newline at end of file diff --git a/lib/TwoFactorAuthException.php b/lib/TwoFactorAuthException.php index 2d46d4e..af51b74 100644 --- a/lib/TwoFactorAuthException.php +++ b/lib/TwoFactorAuthException.php @@ -2,10 +2,6 @@ namespace RobThree\Auth; -class TwoFactorAuthException extends \Exception -{ - function __construct($message = "", $code = 0, $exception = null) - { - parent::__construct($message, $code, $exception); - } -}
\ No newline at end of file +use Exception; + +class TwoFactorAuthException extends \Exception {}
\ No newline at end of file |