Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions PHPGangsta/GoogleAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
Expand All @@ -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;
}
Expand Down
41 changes: 38 additions & 3 deletions tests/GoogleAuthenticatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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'),
);
}

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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)));
}
}