diff --git a/PHPGangsta/GoogleAuthenticator.php b/PHPGangsta/GoogleAuthenticator.php index bf7d116..487f07f 100644 --- a/PHPGangsta/GoogleAuthenticator.php +++ b/PHPGangsta/GoogleAuthenticator.php @@ -53,25 +53,30 @@ public function createSecret($secretLength = 16) } /** - * Calculate the code, with given secret and point in time. + * Calculate the code, with given secret, point in time and + * hmac algorithm. * - * @param string $secret - * @param int|null $timeSlice + * @param string $secret + * @param int|null $timeSlice + * @param string|null $algo * * @return string */ - public function getCode($secret, $timeSlice = null) + public function getCode($secret, $timeSlice = null, $algo = null) { if ($timeSlice === null) { $timeSlice = floor(time() / 30); } + if ($algo === null) { + $algo = 'SHA1'; + } $secretkey = $this->_base32Decode($secret); // Pack time into binary string $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice); // Hash it with users secret key - $hm = hash_hmac('SHA1', $time, $secretkey, true); + $hm = hash_hmac($algo, $time, $secretkey, true); // Use last nipple of result as index/offset $offset = ord(substr($hm, -1)) & 0x0F; // grab 4 bytes of the result @@ -103,8 +108,9 @@ public function getQRCodeGoogleUrl($name, $secret, $title = null, $params = arra $width = !empty($params['width']) && (int) $params['width'] > 0 ? (int) $params['width'] : 200; $height = !empty($params['height']) && (int) $params['height'] > 0 ? (int) $params['height'] : 200; $level = !empty($params['level']) && array_search($params['level'], array('L', 'M', 'Q', 'H')) !== false ? $params['level'] : 'M'; + $algo = !empty($params['algo']) ? strtoupper($params['algo']) : 'SHA1'; - $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.''); + $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.($algo != 'SHA1' ? '&algorithm='.$algo : '')); if (isset($title)) { $urlencoded .= urlencode('&issuer='.urlencode($title)); } @@ -115,25 +121,26 @@ public function getQRCodeGoogleUrl($name, $secret, $title = null, $params = arra /** * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now. * - * @param string $secret - * @param string $code - * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after) - * @param int|null $currentTimeSlice time slice if we want use other that time() + * @param string $secret + * @param string $code + * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after) + * @param int|null $currentTimeSlice time slice if we want use other that time() + * @param string|null $algo Algorithm to use to validate code with. * * @return bool */ - public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null) + public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null, $algo = null) { if ($currentTimeSlice === null) { $currentTimeSlice = floor(time() / 30); } - if (strlen($code) != 6) { + if (strlen($code) != $this->_codeLength) { return false; } for ($i = -$discrepancy; $i <= $discrepancy; ++$i) { - $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i); + $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i, $algo); if ($this->timingSafeEquals($calculatedCode, $code)) { return true; } diff --git a/tests/GoogleAuthenticatorTest.php b/tests/GoogleAuthenticatorTest.php index 4e13b5c..240b5b3 100644 --- a/tests/GoogleAuthenticatorTest.php +++ b/tests/GoogleAuthenticatorTest.php @@ -2,6 +2,12 @@ require_once __DIR__.'/../vendor/autoload.php'; +if (!class_exists('PHPUnit_Framework_TestCase') && class_exists('\PHPUnit\Framework\TestCase')) { + class PHPUnit_Framework_TestCase extends \PHPUnit\Framework\TestCase + { + } +} + class GoogleAuthenticatorTest extends PHPUnit_Framework_TestCase { /* @var $googleAuthenticator PHPGangsta_GoogleAuthenticator */ @@ -14,11 +20,32 @@ protected function setUp() public function codeProvider() { - // Secret, time, code + // Secret, timeSlice, code, codeLength, algo return array( array('SECRET', '0', '200470'), array('SECRET', '1385909245', '780018'), array('SECRET', '1378934578', '705013'), + + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', '1', '94287082', 8, 'SHA1'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', '37037036', '07081804', 8, 'SHA1'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', '37037037', '14050471', 8, 'SHA1'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', '41152263', '89005924', 8, 'SHA1'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', '66666666', '69279037', 8, 'SHA1'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', '666666666', '65353130', 8, 'SHA1'), + + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', '1', '46119246', 8, 'SHA256'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', '37037036', '68084774', 8, 'SHA256'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', '37037037', '67062674', 8, 'SHA256'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', '41152263', '91819424', 8, 'SHA256'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', '66666666', '90698825', 8, 'SHA256'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', '666666666', '77737706', 8, 'SHA256'), + + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', '1', '90693936', 8, 'SHA512'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', '37037036', '25091201', 8, 'SHA512'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', '37037037', '99943326', 8, 'SHA512'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', '41152263', '93441116', 8, 'SHA512'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', '66666666', '38618901', 8, 'SHA512'), + array('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', '666666666', '47863826', 8, 'SHA512'), ); } @@ -51,9 +78,10 @@ public function testCreateSecretLengthCanBeSpecified() /** * @dataProvider codeProvider */ - public function testGetCodeReturnsCorrectValues($secret, $timeSlice, $code) + public function testGetCodeReturnsCorrectValues($secret, $timeSlice, $code, $length = 6, $algo = 'SHA1') { - $generatedCode = $this->googleAuthenticator->getCode($secret, $timeSlice); + $this->googleAuthenticator->setCodeLength($length); + $generatedCode = $this->googleAuthenticator->getCode($secret, $timeSlice, $algo); $this->assertEquals($code, $generatedCode); } @@ -108,4 +136,11 @@ public function testSetCodeLength() $this->assertInstanceOf('PHPGangsta_GoogleAuthenticator', $result); } + + public function testValidateCorrectCodeLength() + { + $secret = 'SECRET'; + $this->googleAuthenticator->setCodeLength(8); + $this->assertEquals(true, $this->googleAuthenticator->verifyCode($secret, $this->googleAuthenticator->getCode($secret))); + } }