diff options
author | Christian Riesen <chris.riesen@gmail.com> | 2013-07-03 16:01:54 +0200 |
---|---|---|
committer | Christian Riesen <chris.riesen@gmail.com> | 2013-07-03 16:01:54 +0200 |
commit | 4bfaf4fa4a2f1069bf5b6606be93ea1de1f20a1c (patch) | |
tree | 2b2cd13c7d775c44c0565b634030b8a663a8b8ee | |
parent | 08fc53c33952ce200e44a4f0cd5adc0c17dd65b1 (diff) | |
download | otp-4bfaf4fa4a2f1069bf5b6606be93ea1de1f20a1c.zip otp-4bfaf4fa4a2f1069bf5b6606be93ea1de1f20a1c.tar.gz otp-4bfaf4fa4a2f1069bf5b6606be93ea1de1f20a1c.tar.bz2 |
Alter checkTotp to allow timedrift to be set as optional parameter1.2
-rw-r--r-- | README.md | 10 | ||||
-rw-r--r-- | src/Otp/GoogleAuthenticator.php | 218 | ||||
-rw-r--r-- | src/Otp/Otp.php | 561 | ||||
-rw-r--r-- | src/Otp/OtpInterface.php | 87 |
4 files changed, 437 insertions, 439 deletions
@@ -14,7 +14,7 @@ Use [composer](http://getcomposer.org/) and require the library in your `compose { "require": { - "christian-riesen/otp": "1.1.*", + "christian-riesen/otp": "1.*", } } @@ -46,8 +46,8 @@ $otp = new Otp(); // $key is a 6 digit number, coming from the User // Assuming this is present and sanitized -// Allows for some timedrift (2 keys before and -// 2 keys after the present one) +// Allows for a 1 code time drift by default +// Third parameter can alter that behavior if ($otp->checkTotp(Base32::decode($secret), $key)) { // Correct key // IMPORTANT! Note this key as being used @@ -73,7 +73,7 @@ Implements hotp according to [RFC4226](https://tools.ietf.org/html/rfc4226) and Class GoogleAuthenticator ------------------------- -Single static function to generate a correct url for the QR code, so you can easy scan it with your device. Google Authenticator is opensource and avaiaible as application for iPhone and Android. This removes the burden to create such an app from the developers of websites by using this set of classes. +Static function class to generate a correct url for the QR code, so you can easy scan it with your device. Google Authenticator is opensource and avaiaible as application for iPhone and Android. This removes the burden to create such an app from the developers of websites by using this set of classes. About ===== @@ -83,6 +83,8 @@ Requirements PHP 5.3.x+ +Uses [Base32 class](https://github.com/ChristianRiesen/base32). + If you want to run the tests, PHPUnit 3.6 or up is required. Author diff --git a/src/Otp/GoogleAuthenticator.php b/src/Otp/GoogleAuthenticator.php index 36ebacb..e9ce3d4 100644 --- a/src/Otp/GoogleAuthenticator.php +++ b/src/Otp/GoogleAuthenticator.php @@ -16,115 +16,115 @@ namespace Otp; class GoogleAuthenticator { - protected static $allowedTypes = array('hotp', 'totp'); - - protected static $height = 200; - protected static $width = 200; - - /** - * Returns the QR code url - * - * Format of encoded url is here: - * https://code.google.com/p/google-authenticator/wiki/KeyUriFormat - * Should be done in a better fashion - * - * @param string $type totp or hotp - * @param string $label Label to display this as to the user - * @param string $secret Base32 encoded secret - * @param integer $counter Required by hotp, otherwise ignored - * @param array $options Optional fields that will be set if present - * - * @return string URL to the QR code - */ - public static function getQrCodeUrl($type, $label, $secret, $counter = null, $options = array()) - { - // two types only.. - if (!in_array($type, self::$allowedTypes)) { - throw new \InvalidArgumentException('Type has to be of allowed types list'); - } - - // Label can't be empty - $label = trim($label); - - if (strlen($label) < 1) { - throw new \InvalidArgumentException('Label has to be one or more printable characters'); - } - - // Secret needs to be here - if (strlen($secret) < 1) { - throw new \InvalidArgumentException('No secret present'); - } - - // check for counter on hotp - if ($type == 'hotp' && is_null($counter)) { - throw new \InvalidArgumentException('Counter required for hotp'); - } - - // This is the base, these are at least required - $otpauth = 'otpauth://' . $type . '/' . $label . '?secret=' . $secret; - - if ($type == 'hotp' && !is_null($counter)) {
- $otpauth .= '&counter=' . $counter;
- } - - // Now check the options array + protected static $allowedTypes = array('hotp', 'totp'); + + protected static $height = 200; + protected static $width = 200; + + /** + * Returns the QR code url + * + * Format of encoded url is here: + * https://code.google.com/p/google-authenticator/wiki/KeyUriFormat + * Should be done in a better fashion + * + * @param string $type totp or hotp + * @param string $label Label to display this as to the user + * @param string $secret Base32 encoded secret + * @param integer $counter Required by hotp, otherwise ignored + * @param array $options Optional fields that will be set if present + * + * @return string URL to the QR code + */ + public static function getQrCodeUrl($type, $label, $secret, $counter = null, $options = array()) + { + // two types only.. + if (!in_array($type, self::$allowedTypes)) { + throw new \InvalidArgumentException('Type has to be of allowed types list'); + } + + // Label can't be empty + $label = trim($label); + + if (strlen($label) < 1) { + throw new \InvalidArgumentException('Label has to be one or more printable characters'); + } + + // Secret needs to be here + if (strlen($secret) < 1) { + throw new \InvalidArgumentException('No secret present'); + } + + // check for counter on hotp + if ($type == 'hotp' && is_null($counter)) { + throw new \InvalidArgumentException('Counter required for hotp'); + } + + // This is the base, these are at least required + $otpauth = 'otpauth://' . $type . '/' . $label . '?secret=' . $secret; + + if ($type == 'hotp' && !is_null($counter)) {
+ $otpauth .= '&counter=' . $counter;
+ } + + // Now check the options array - // algorithm (currently ignored by Authenticator) - // Defaults to SHA1 - if (array_key_exists('algorithm', $options)) { - $otpauth .= '&algorithm=' . $options['algorithm']; - } - - // digits (currently ignored by Authenticator) - // Defaults to 6 - if (array_key_exists('digits', $options)) { - $otpauth .= '&digits=' . $options['digits']; - } - - // period, only for totp (currently ignored by Authenticator) - // Defaults to 30 - if ($type == 'totp' && array_key_exists('period', $options)) { - $otpauth .= '&period=' . $options['period']; - } - - // Width and height can be overwritten - $width = self::$width; - - if (array_key_exists('width', $options) && is_numeric($options['width'])) { - $width = $options['width']; - } - - $height = self::$height; - - if (array_key_exists('height', $options) && is_numeric($options['height'])) { - $height = $options['height']; - } - - $url = 'https://chart.googleapis.com/chart?chs=' . $width . 'x' - . $height . '&cht=qr&chld=M|0&chl=' . urlencode($otpauth); - - return $url; - } + // algorithm (currently ignored by Authenticator) + // Defaults to SHA1 + if (array_key_exists('algorithm', $options)) { + $otpauth .= '&algorithm=' . $options['algorithm']; + } + + // digits (currently ignored by Authenticator) + // Defaults to 6 + if (array_key_exists('digits', $options)) { + $otpauth .= '&digits=' . $options['digits']; + } + + // period, only for totp (currently ignored by Authenticator) + // Defaults to 30 + if ($type == 'totp' && array_key_exists('period', $options)) { + $otpauth .= '&period=' . $options['period']; + } + + // Width and height can be overwritten + $width = self::$width; + + if (array_key_exists('width', $options) && is_numeric($options['width'])) { + $width = $options['width']; + } + + $height = self::$height; + + if (array_key_exists('height', $options) && is_numeric($options['height'])) { + $height = $options['height']; + } + + $url = 'https://chart.googleapis.com/chart?chs=' . $width . 'x' + . $height . '&cht=qr&chld=M|0&chl=' . urlencode($otpauth); + + return $url; + } - /** - * Creates a pseudo random Base32 string - * - * This could decode into anything. It's located here as a small helper - * where code that might need base32 usually also needs something like this. - * - * @param integer $length Exact length of output string - * @return string Base32 encoded random - */ - public static function generateRandom($length = 16) - { - $keys = array_merge(range('A','Z'), range(2,7)); // No padding char - - $string = ''; - - for ($i = 0; $i < $length; $i++) { - $string .= $keys[rand(0,31)]; - } - - return $string; - } + /** + * Creates a pseudo random Base32 string + * + * This could decode into anything. It's located here as a small helper + * where code that might need base32 usually also needs something like this. + * + * @param integer $length Exact length of output string + * @return string Base32 encoded random + */ + public static function generateRandom($length = 16) + { + $keys = array_merge(range('A','Z'), range(2,7)); // No padding char + + $string = ''; + + for ($i = 0; $i < $length; $i++) { + $string .= $keys[rand(0,31)]; + } + + return $string; + } } diff --git a/src/Otp/Otp.php b/src/Otp/Otp.php index 59aee18..02bf90e 100644 --- a/src/Otp/Otp.php +++ b/src/Otp/Otp.php @@ -27,288 +27,285 @@ namespace Otp; class Otp implements OtpInterface { - /** - * The digits the code can have - * - * Either 6 or 8. - * Authenticator does only support 6. - * - * @var integer - */ - protected $digits = 6; - - /** - * Time in seconds one counter period is long - * - * @var integer - */ - protected $period = 30; - - /** - * Range to allow for timedrift - * - * Used in checkTotp only. This value means it will check the current - * totp and the two before it, as well as the two after it. - * @var integer - */ - protected $timerange = 2; - - /** - * Possible algorithms - * - * @var array - */ - protected $allowedAlgorithms = array('sha1', 'sha256', 'sha512'); - - /** - * Currently used algorithm - * - * @var string - */ - protected $algorithm = 'sha1'; - - /* (non-PHPdoc)
- * @see Otp.OtpInterface::hotp()
- */
- public function hotp($secret, $counter)
- {
- if (!is_numeric($counter)) { - throw new \InvalidArgumentException('Counter must be integer'); - } - - $hash = hash_hmac(
- $this->algorithm,
- $this->getBinaryCounter($counter),
- $secret,
- true
- );
-
- return str_pad($this->truncate($hash), $this->digits, '0', STR_PAD_LEFT);
- }
-
- /* (non-PHPdoc)
- * @see Otp.OtpInterface::totp()
- */
- public function totp($secret, $timecounter = null)
- {
- if (is_null($timecounter)) {
- $timecounter = $this->getTimecounter();
- }
-
- return $this->hotp($secret, $timecounter);
- }
-
- /* (non-PHPdoc)
- * @see Otp.OtpInterface::checkHotp()
- */
- public function checkHotp($secret, $counter, $key)
- {
- return $this->safeCompare($this->hotp($secret, $counter), $key);
- }
-
- /* (non-PHPdoc)
- * @see Otp.OtpInterface::checkTotp()
- */
- public function checkTotp($secret, $key)
- {
- // Counter comes from time now
- // Also we check the current timestamp as well as previous and future ones
- // according to $timerange
- $timecounter = $this->getTimecounter();
-
- $start = $timecounter - ($this->timerange);
- $end = $timecounter + ($this->timerange);
-
- // We first try the current, as it is the most likely to work
- if ($this->safeCompare($this->totp($secret, $timecounter), $key)) {
- return true;
- }
-
- // Well, that didn't work, so try the others
- for ($t = $start; $t <= $end; $t = $t + 1) {
- if ($t == $timecounter) {
- // Already tried that one
- continue;
- }
-
- if ($this->safeCompare($this->totp($secret, $t), $key)) {
- return true;
- }
- }
-
- // if none worked, then return false
- return false;
- } - - /** - * Changing the used algorithm for hashing - * - * Can only be one of the algorithms in the allowedAlgorithms property. - * - * @param string $algorithm - * @throws \InvalidArgumentException - * @return \Otp\Otp - */ - - /*
- * This has been disabled since it does not bring the expected results
- * according to the RFC test vectors for sha256 or sha512.
- * Until that is fixed, the algorithm simply stays at sha1.
- * Google Authenticator does not support sha256 and sha512 at the moment. - * - - public function setAlgorithm($algorithm) - { - if (!in_array($algorithm, $this->allowedAlgorithms)) { - throw new \InvalidArgumentException('Not an allowed algorithm: ' . $algorithm); - } - - $this->algorithm = $algorithm; - - return $this; - } - // */ - - /** - * Get the algorithms name (lowercase) - * - * @return string - */ - public function getAlgorithm() - { - return $this->algorithm; - } - - /** - * Setting period lenght for totp - * - * @param integer $period - * @throws \InvalidArgumentException - * @return \Otp\Otp - */ - public function setPeriod($period) - { - if (!is_int($period)) { - throw new \InvalidArgumentException('Period must be an integer'); - } - - $this->period = $period; - - return $this; - } - - /** - * Returns the set period value - * - * @return integer - */ - public function getPeriod()
- {
- return $this->period;
- }
- - /** - * Setting number of otp digits - * - * @param integer $digits Number of digits for the otp (6 or 8) - * @throws \InvalidArgumentException - * @return \Otp\Otp - */ - public function setDigits($digits) - { - if (!in_array($digits, array(6, 8))) { - throw new \InvalidArgumentException('Digits must be 6 or 8'); - } - - $this->digits = $digits; - - return $this; - } - - /** - * Returns number of digits in the otp - * - * @return integer - */ - public function getDigits()
- {
- return $this->digits;
- } - - /** - * Generates a binary counter for hashing - * - * Warning: Not 2038 safe. Maybe until then pack supports 64bit. - * - * @param integer $counter Counter in integer form - * @return string Binary string - */ - protected function getBinaryCounter($counter) - { - return pack('N*', 0) . pack('N*', $counter); - } - - /** - * Generating time counter - * - * This is the time divided by 30 by default. - * - * @return integer Time counter - */ - protected function getTimecounter() - {
- return floor(time() / $this->period);
- } - - /** - * Creates the basic number for otp from hash - * - * This number is left padded with zeros to the required length by the - * calling function. - * - * @param string $hash hmac hash - * @return number - */ - protected function truncate($hash) - { - $offset = ord($hash[19]) & 0xf;
-
- return ( - ((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, $this->digits); - } - - /** - * Safely compares two inputs - * - * Assumed inputs are numbers and strings. - * Compares them in a time linear manner. No matter how much you guess - * correct of the partial content, it does not change the time it takes to - * run the entire comparison. - * - * @param mixed $a - * @param mixed $b - * @return boolean - */ - protected function safeCompare($a, $b) - { - $sha1a = sha1($a); - $sha1b = sha1($b); - - // Now the compare is always the same length. Even considering minute - // time differences in sha1 creation, all you know is that a longer - // input takes longer to hash, not how long the actual compared value is - $result = 0; -
- for ($i = 0; $i < 40; $i++) {
- $result |= ord($sha1a[$i]) ^ ord($sha1b[$i]);
- } -
- return $result == 0; - } + /** + * The digits the code can have + * + * Either 6 or 8. + * Authenticator does only support 6. + * + * @var integer + */ + protected $digits = 6; + + /** + * Time in seconds one counter period is long + * + * @var integer + */ + protected $period = 30; + + /** + * Possible algorithms + * + * @var array + */ + protected $allowedAlgorithms = array('sha1', 'sha256', 'sha512'); + + /** + * Currently used algorithm + * + * @var string + */ + protected $algorithm = 'sha1'; + + /* (non-PHPdoc) + * @see Otp.OtpInterface::hotp() + */ + public function hotp($secret, $counter) + { + if (!is_numeric($counter)) { + throw new \InvalidArgumentException('Counter must be integer'); + } + + $hash = hash_hmac( + $this->algorithm, + $this->getBinaryCounter($counter), + $secret, + true + ); + + return str_pad($this->truncate($hash), $this->digits, '0', STR_PAD_LEFT); + } + + /* (non-PHPdoc) + * @see Otp.OtpInterface::totp() + */ + public function totp($secret, $timecounter = null) + { + if (is_null($timecounter)) { + $timecounter = $this->getTimecounter(); + } + + return $this->hotp($secret, $timecounter); + } + + /* (non-PHPdoc) + * @see Otp.OtpInterface::checkHotp() + */ + public function checkHotp($secret, $counter, $key) + { + return $this->safeCompare($this->hotp($secret, $counter), $key); + } + + /* (non-PHPdoc) + * @see Otp.OtpInterface::checkTotp() + */ + public function checkTotp($secret, $key, $timedrift = 1) + { + if (!is_numeric($timedrift) || $timedrift < 0) { + throw new \InvalidArgumentException('Invalid timedrift supplied'); + } + // Counter comes from time now + // Also we check the current timestamp as well as previous and future ones + // according to $timerange + $timecounter = $this->getTimecounter(); + + $start = $timecounter - ($timedrift); + $end = $timecounter + ($timedrift); + + // We first try the current, as it is the most likely to work + if ($this->safeCompare($this->totp($secret, $timecounter), $key)) { + return true; + } elseif ($timedrift == 0) { + // When timedrift is 0, this is the end of the checks + return false; + } + + // Well, that didn't work, so try the others + for ($t = $start; $t <= $end; $t = $t + 1) { + if ($t == $timecounter) { + // Already tried that one + continue; + } + + if ($this->safeCompare($this->totp($secret, $t), $key)) { + return true; + } + } + + // if none worked, then return false + return false; + } + + /** + * Changing the used algorithm for hashing + * + * Can only be one of the algorithms in the allowedAlgorithms property. + * + * @param string $algorithm + * @throws \InvalidArgumentException + * @return \Otp\Otp + */ + + /* + * This has been disabled since it does not bring the expected results + * according to the RFC test vectors for sha256 or sha512. + * Until that is fixed, the algorithm simply stays at sha1. + * Google Authenticator does not support sha256 and sha512 at the moment. + * + + public function setAlgorithm($algorithm) + { + if (!in_array($algorithm, $this->allowedAlgorithms)) { + throw new \InvalidArgumentException('Not an allowed algorithm: ' . $algorithm); + } + + $this->algorithm = $algorithm; + + return $this; + } + // */ + + /** + * Get the algorithms name (lowercase) + * + * @return string + */ + public function getAlgorithm() + { + return $this->algorithm; + } + + /** + * Setting period lenght for totp + * + * @param integer $period + * @throws \InvalidArgumentException + * @return \Otp\Otp + */ + public function setPeriod($period) + { + if (!is_int($period)) { + throw new \InvalidArgumentException('Period must be an integer'); + } + + $this->period = $period; + + return $this; + } + + /** + * Returns the set period value + * + * @return integer + */ + public function getPeriod() + { + return $this->period; + } + + /** + * Setting number of otp digits + * + * @param integer $digits Number of digits for the otp (6 or 8) + * @throws \InvalidArgumentException + * @return \Otp\Otp + */ + public function setDigits($digits) + { + if (!in_array($digits, array(6, 8))) { + throw new \InvalidArgumentException('Digits must be 6 or 8'); + } + + $this->digits = $digits; + + return $this; + } + + /** + * Returns number of digits in the otp + * + * @return integer + */ + public function getDigits() + { + return $this->digits; + } + + /** + * Generates a binary counter for hashing + * + * Warning: Not 2038 safe. Maybe until then pack supports 64bit. + * + * @param integer $counter Counter in integer form + * @return string Binary string + */ + protected function getBinaryCounter($counter) + { + return pack('N*', 0) . pack('N*', $counter); + } + + /** + * Generating time counter + * + * This is the time divided by 30 by default. + * + * @return integer Time counter + */ + protected function getTimecounter() + { + return floor(time() / $this->period); + } + + /** + * Creates the basic number for otp from hash + * + * This number is left padded with zeros to the required length by the + * calling function. + * + * @param string $hash hmac hash + * @return number + */ + protected function truncate($hash) + { + $offset = ord($hash[19]) & 0xf; + + return ( + ((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, $this->digits); + } + + /** + * Safely compares two inputs + * + * Assumed inputs are numbers and strings. + * Compares them in a time linear manner. No matter how much you guess + * correct of the partial content, it does not change the time it takes to + * run the entire comparison. + * + * @param mixed $a + * @param mixed $b + * @return boolean + */ + protected function safeCompare($a, $b) + { + $sha1a = sha1($a); + $sha1b = sha1($b); + + // Now the compare is always the same length. Even considering minute + // time differences in sha1 creation, all you know is that a longer + // input takes longer to hash, not how long the actual compared value is + $result = 0; + + for ($i = 0; $i < 40; $i++) { + $result |= ord($sha1a[$i]) ^ ord($sha1b[$i]); + } + + return $result == 0; + } }
\ No newline at end of file diff --git a/src/Otp/OtpInterface.php b/src/Otp/OtpInterface.php index 51dc04e..7ff34f2 100644 --- a/src/Otp/OtpInterface.php +++ b/src/Otp/OtpInterface.php @@ -19,48 +19,47 @@ namespace Otp; interface OtpInterface { - /** - * Returns OTP using the HOTP algorithm - * - * @param string $secret - * @param integer $counter - * @return string One Time Password - */ - function hotp($secret, $counter); - - /** - * Returns OTP using the TOTP algorithm - * - * @param string $secret - * @param integer $timecounter Optional: Uses current time if null - * @return string One Time Password - */ - function totp($secret, $timecounter = null); - - /** - * Checks Hotp against a key - * - * This is a helper function, but is here to ensure the Totp can be checked - * in the same manner. - * - * @param string $secret - * @param integer $counter - * @param string $key - * - * @return boolean If key is correct - */ - function checkHotp($secret, $counter, $key); - - /** - * Checks Totp agains a key - * - * Should implement a time drift check (before and after the actual current - * key) - * - * @param string $secret - * @param integer $key - * - * @return boolean If key is correct - */ - function checkTotp($secret, $key); + /** + * Returns OTP using the HOTP algorithm + * + * @param string $secret + * @param integer $counter + * @return string One Time Password + */ + function hotp($secret, $counter); + + /** + * Returns OTP using the TOTP algorithm + * + * @param string $secret + * @param integer $timecounter Optional: Uses current time if null + * @return string One Time Password + */ + function totp($secret, $timecounter = null); + + /** + * Checks Hotp against a key + * + * This is a helper function, but is here to ensure the Totp can be checked + * in the same manner. + * + * @param string $secret + * @param integer $counter + * @param string $key + * + * @return boolean If key is correct + */ + function checkHotp($secret, $counter, $key); + + /** + * Checks Totp agains a key + * + * + * @param string $secret + * @param integer $key + * @param integer $timedrift + * + * @return boolean If key is correct + */ + function checkTotp($secret, $key, $timedrift = 1); } |