| <?php |
| require_once 'lib/TwoFactorAuth.php'; |
| require_once 'lib/TwoFactorAuthException.php'; |
| |
| require_once 'lib/Providers/Qr/IQRCodeProvider.php'; |
| require_once 'lib/Providers/Qr/BaseHTTPQRCodeProvider.php'; |
| require_once 'lib/Providers/Qr/ImageChartsQRCodeProvider.php'; |
| require_once 'lib/Providers/Qr/QRException.php'; |
| |
| require_once 'lib/Providers/Rng/IRNGProvider.php'; |
| require_once 'lib/Providers/Rng/RNGException.php'; |
| require_once 'lib/Providers/Rng/CSRNGProvider.php'; |
| require_once 'lib/Providers/Rng/MCryptRNGProvider.php'; |
| require_once 'lib/Providers/Rng/OpenSSLRNGProvider.php'; |
| require_once 'lib/Providers/Rng/HashRNGProvider.php'; |
| require_once 'lib/Providers/Rng/RNGException.php'; |
| |
| require_once 'lib/Providers/Time/ITimeProvider.php'; |
| require_once 'lib/Providers/Time/LocalMachineTimeProvider.php'; |
| require_once 'lib/Providers/Time/HttpTimeProvider.php'; |
| require_once 'lib/Providers/Time/NTPTimeProvider.php'; |
| require_once 'lib/Providers/Time/TimeException.php'; |
| |
| use RobThree\Auth\TwoFactorAuth; |
| use RobThree\Auth\Providers\Qr\IQRCodeProvider; |
| use RobThree\Auth\Providers\Rng\IRNGProvider; |
| use RobThree\Auth\Providers\Time\ITimeProvider; |
| |
| |
| class TwoFactorAuthTest extends PHPUnit_Framework_TestCase |
| { |
| /** |
| * @expectedException \RobThree\Auth\TwoFactorAuthException |
| */ |
| public function testConstructorThrowsOnInvalidDigits() { |
| |
| new TwoFactorAuth('Test', 0); |
| } |
| |
| /** |
| * @expectedException \RobThree\Auth\TwoFactorAuthException |
| */ |
| public function testConstructorThrowsOnInvalidPeriod() { |
| |
| new TwoFactorAuth('Test', 6, 0); |
| } |
| |
| /** |
| * @expectedException \RobThree\Auth\TwoFactorAuthException |
| */ |
| public function testConstructorThrowsOnInvalidAlgorithm() { |
| |
| new TwoFactorAuth('Test', 6, 30, 'xxx'); |
| } |
| |
| public function testGetCodeReturnsCorrectResults() { |
| |
| $tfa = new TwoFactorAuth('Test'); |
| $this->assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216)); |
| $this->assertEquals('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0)); |
| } |
| |
| /** |
| * @expectedException \RobThree\Auth\TwoFactorAuthException |
| */ |
| public function testCreateSecretThrowsOnInsecureRNGProvider() { |
| $rng = new TestRNGProvider(); |
| |
| $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); |
| $tfa->createSecret(); |
| } |
| |
| public function testCreateSecretOverrideSecureDoesNotThrowOnInsecureRNG() { |
| $rng = new TestRNGProvider(); |
| |
| $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); |
| $this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret(80, false)); |
| } |
| |
| public function testCreateSecretDoesNotThrowOnSecureRNGProvider() { |
| $rng = new TestRNGProvider(true); |
| |
| $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); |
| $this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret()); |
| } |
| |
| public function testCreateSecretGeneratesDesiredAmountOfEntropy() { |
| $rng = new TestRNGProvider(true); |
| |
| $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); |
| $this->assertEquals('A', $tfa->createSecret(5)); |
| $this->assertEquals('AB', $tfa->createSecret(6)); |
| $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $tfa->createSecret(128)); |
| $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(160)); |
| $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(320)); |
| $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567A', $tfa->createSecret(321)); |
| } |
| |
| public function testEnsureCorrectTimeDoesNotThrowForCorrectTime() { |
| $tpr1 = new TestTimeProvider(123); |
| $tpr2 = new TestTimeProvider(128); |
| |
| $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1); |
| $tfa->ensureCorrectTime(array($tpr2)); // 128 - 123 = 5 => within default leniency |
| } |
| |
| /** |
| * @expectedException \RobThree\Auth\TwoFactorAuthException |
| */ |
| public function testEnsureCorrectTimeThrowsOnIncorrectTime() { |
| $tpr1 = new TestTimeProvider(123); |
| $tpr2 = new TestTimeProvider(124); |
| |
| $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1); |
| $tfa->ensureCorrectTime(array($tpr2), 0); // We force a leniency of 0, 124-123 = 1 so this should throw |
| } |
| |
| |
| public function testEnsureDefaultTimeProviderReturnsCorrectTime() { |
| $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1'); |
| $tfa->ensureCorrectTime(array(new TestTimeProvider(time())), 1); // Use a leniency of 1, should the time change between both time() calls |
| } |
| |
| public function testEnsureAllTimeProvidersReturnCorrectTime() { |
| $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1'); |
| $tfa->ensureCorrectTime(array( |
| new RobThree\Auth\Providers\Time\NTPTimeProvider(), // Uses pool.ntp.org by default |
| //new RobThree\Auth\Providers\Time\NTPTimeProvider('time.google.com'), // Somehow time.google.com and time.windows.com make travis timeout?? |
| new RobThree\Auth\Providers\Time\HttpTimeProvider(), // Uses google.com by default |
| new RobThree\Auth\Providers\Time\HttpTimeProvider('https://github.com'), |
| new RobThree\Auth\Providers\Time\HttpTimeProvider('https://yahoo.com'), |
| )); |
| } |
| |
| public function testVerifyCodeWorksCorrectly() { |
| |
| $tfa = new TwoFactorAuth('Test', 6, 30); |
| $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190)); |
| $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy |
| $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy |
| $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy |
| |
| $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 0)); //Test discrepancy |
| $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 35)); //Test discrepancy |
| $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 35)); //Test discrepancy |
| |
| $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy |
| $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy |
| |
| $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy |
| $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy |
| } |
| |
| public function testVerifyCorrectTimeSliceIsReturned() { |
| $tfa = new TwoFactorAuth('Test', 6, 30); |
| |
| // We test with discrepancy 3 (so total of 7 codes: c-3, c-2, c-1, c, c+1, c+2, c+3 |
| // Ensure each corresponding timeslice is returned correctly |
| $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '534113', 3, 1426847190, $timeslice1)); |
| $this->assertEquals(47561570, $timeslice1); |
| $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '819652', 3, 1426847190, $timeslice2)); |
| $this->assertEquals(47561571, $timeslice2); |
| $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '915954', 3, 1426847190, $timeslice3)); |
| $this->assertEquals(47561572, $timeslice3); |
| $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 3, 1426847190, $timeslice4)); |
| $this->assertEquals(47561573, $timeslice4); |
| $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '348401', 3, 1426847190, $timeslice5)); |
| $this->assertEquals(47561574, $timeslice5); |
| $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '648525', 3, 1426847190, $timeslice6)); |
| $this->assertEquals(47561575, $timeslice6); |
| $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '170645', 3, 1426847190, $timeslice7)); |
| $this->assertEquals(47561576, $timeslice7); |
| |
| // Incorrect code should return false and a 0 timeslice |
| $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '111111', 3, 1426847190, $timeslice8)); |
| $this->assertEquals(0, $timeslice8); |
| } |
| |
| public function testTotpUriIsCorrect() { |
| $qr = new TestQrProvider(); |
| |
| $tfa = new TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr); |
| $data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE')); |
| $this->assertEquals('test/test', $data['mimetype']); |
| $this->assertEquals('base64', $data['encoding']); |
| $this->assertEquals('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=Test%26Issuer&period=30&algorithm=SHA1&digits=6@200', $data['data']); |
| } |
| |
| /** |
| * @expectedException \RobThree\Auth\TwoFactorAuthException |
| */ |
| public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize() { |
| $qr = new TestQrProvider(); |
| |
| $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', $qr); |
| $tfa->getQRCodeImageAsDataUri('Test', 'VMR466AB62ZBOKHE', 0); |
| } |
| |
| /** |
| * @expectedException \RobThree\Auth\TwoFactorAuthException |
| */ |
| public function testGetCodeThrowsOnInvalidBase32String1() { |
| $tfa = new TwoFactorAuth('Test'); |
| $tfa->getCode('FOO1BAR8BAZ9'); //1, 8 & 9 are invalid chars |
| } |
| |
| /** |
| * @expectedException \RobThree\Auth\TwoFactorAuthException |
| */ |
| public function testGetCodeThrowsOnInvalidBase32String2() { |
| $tfa = new TwoFactorAuth('Test'); |
| $tfa->getCode('mzxw6==='); //Lowercase |
| } |
| |
| public function testKnownBase32DecodeTestVectors() { |
| // We usually don't test internals (e.g. privates) but since we rely heavily on base32 decoding and don't want |
| // to expose this method nor do we want to give people the possibility of implementing / providing their own base32 |
| // decoding/decoder (as we do with Rng/QR providers for example) we simply test the private base32Decode() method |
| // with some known testvectors **only** to ensure base32 decoding works correctly following RFC's so there won't |
| // be any bugs hiding in there. We **could** 'fool' ourselves by calling the public getCode() method (which uses |
| // base32decode internally) and then make sure getCode's output (in digits) equals expected output since that would |
| // mean the base32Decode() works as expected but that **could** hide some subtle bug(s) in decoding the base32 string. |
| |
| // "In general, you don't want to break any encapsulation for the sake of testing (or as Mom used to say, "don't |
| // expose your privates!"). Most of the time, you should be able to test a class by exercising its public methods." |
| // Dave Thomas and Andy Hunt -- "Pragmatic Unit Testing |
| $tfa = new TwoFactorAuth('Test'); |
| |
| $method = new ReflectionMethod('RobThree\Auth\TwoFactorAuth', 'base32Decode'); |
| $method->setAccessible(true); |
| |
| // Test vectors from: https://tools.ietf.org/html/rfc4648#page-12 |
| $this->assertEquals('', $method->invoke($tfa, '')); |
| $this->assertEquals('f', $method->invoke($tfa, 'MY======')); |
| $this->assertEquals('fo', $method->invoke($tfa, 'MZXQ====')); |
| $this->assertEquals('foo', $method->invoke($tfa, 'MZXW6===')); |
| $this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ=')); |
| $this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB')); |
| $this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI======')); |
| } |
| |
| public function testKnownBase32DecodeUnpaddedTestVectors() { |
| // See testKnownBase32DecodeTestVectors() for the rationale behind testing the private base32Decode() method. |
| // This test ensures that strings without the padding-char ('=') are also decoded correctly. |
| // https://tools.ietf.org/html/rfc4648#page-4: |
| // "In some circumstances, the use of padding ("=") in base-encoded data is not required or used." |
| $tfa = new TwoFactorAuth('Test'); |
| |
| $method = new ReflectionMethod('RobThree\Auth\TwoFactorAuth', 'base32Decode'); |
| $method->setAccessible(true); |
| |
| // Test vectors from: https://tools.ietf.org/html/rfc4648#page-12 |
| $this->assertEquals('', $method->invoke($tfa, '')); |
| $this->assertEquals('f', $method->invoke($tfa, 'MY')); |
| $this->assertEquals('fo', $method->invoke($tfa, 'MZXQ')); |
| $this->assertEquals('foo', $method->invoke($tfa, 'MZXW6')); |
| $this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ')); |
| $this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB')); |
| $this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI')); |
| } |
| |
| |
| public function testKnownTestVectors_sha1() { |
| //Known test vectors for SHA1: https://tools.ietf.org/html/rfc6238#page-15 |
| $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'; //== base32encode('12345678901234567890') |
| $tfa = new TwoFactorAuth('Test', 8, 30, 'sha1'); |
| $this->assertEquals('94287082', $tfa->getCode($secret, 59)); |
| $this->assertEquals('07081804', $tfa->getCode($secret, 1111111109)); |
| $this->assertEquals('14050471', $tfa->getCode($secret, 1111111111)); |
| $this->assertEquals('89005924', $tfa->getCode($secret, 1234567890)); |
| $this->assertEquals('69279037', $tfa->getCode($secret, 2000000000)); |
| $this->assertEquals('65353130', $tfa->getCode($secret, 20000000000)); |
| } |
| |
| public function testKnownTestVectors_sha256() { |
| //Known test vectors for SHA256: https://tools.ietf.org/html/rfc6238#page-15 |
| $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA'; //== base32encode('12345678901234567890123456789012') |
| $tfa = new TwoFactorAuth('Test', 8, 30, 'sha256'); |
| $this->assertEquals('46119246', $tfa->getCode($secret, 59)); |
| $this->assertEquals('68084774', $tfa->getCode($secret, 1111111109)); |
| $this->assertEquals('67062674', $tfa->getCode($secret, 1111111111)); |
| $this->assertEquals('91819424', $tfa->getCode($secret, 1234567890)); |
| $this->assertEquals('90698825', $tfa->getCode($secret, 2000000000)); |
| $this->assertEquals('77737706', $tfa->getCode($secret, 20000000000)); |
| } |
| |
| public function testKnownTestVectors_sha512() { |
| //Known test vectors for SHA512: https://tools.ietf.org/html/rfc6238#page-15 |
| $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA'; //== base32encode('1234567890123456789012345678901234567890123456789012345678901234') |
| $tfa = new TwoFactorAuth('Test', 8, 30, 'sha512'); |
| $this->assertEquals('90693936', $tfa->getCode($secret, 59)); |
| $this->assertEquals('25091201', $tfa->getCode($secret, 1111111109)); |
| $this->assertEquals('99943326', $tfa->getCode($secret, 1111111111)); |
| $this->assertEquals('93441116', $tfa->getCode($secret, 1234567890)); |
| $this->assertEquals('38618901', $tfa->getCode($secret, 2000000000)); |
| $this->assertEquals('47863826', $tfa->getCode($secret, 20000000000)); |
| } |
| |
| /** |
| * @requires function random_bytes |
| */ |
| public function testCSRNGProvidersReturnExpectedNumberOfBytes() { |
| $rng = new \RobThree\Auth\Providers\Rng\CSRNGProvider(); |
| foreach ($this->getRngTestLengths() as $l) |
| $this->assertEquals($l, strlen($rng->getRandomBytes($l))); |
| $this->assertEquals(true, $rng->isCryptographicallySecure()); |
| } |
| |
| /** |
| * @requires function hash_algos |
| * @requires function hash |
| */ |
| public function testHashRNGProvidersReturnExpectedNumberOfBytes() { |
| $rng = new \RobThree\Auth\Providers\Rng\HashRNGProvider(); |
| foreach ($this->getRngTestLengths() as $l) |
| $this->assertEquals($l, strlen($rng->getRandomBytes($l))); |
| $this->assertEquals(false, $rng->isCryptographicallySecure()); |
| } |
| |
| /** |
| * @requires function mcrypt_create_iv |
| */ |
| public function testMCryptRNGProvidersReturnExpectedNumberOfBytes() { |
| if (function_exists('mcrypt_create_iv')) { |
| $rng = new \RobThree\Auth\Providers\Rng\MCryptRNGProvider(); |
| foreach ($this->getRngTestLengths() as $l) |
| $this->assertEquals($l, strlen($rng->getRandomBytes($l))); |
| $this->assertEquals(true, $rng->isCryptographicallySecure()); |
| } |
| } |
| |
| /** |
| * @requires function openssl_random_pseudo_bytes |
| */ |
| public function testStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes() { |
| $rng = new \RobThree\Auth\Providers\Rng\OpenSSLRNGProvider(true); |
| foreach ($this->getRngTestLengths() as $l) |
| $this->assertEquals($l, strlen($rng->getRandomBytes($l))); |
| $this->assertEquals(true, $rng->isCryptographicallySecure()); |
| } |
| |
| /** |
| * @requires function openssl_random_pseudo_bytes |
| */ |
| public function testNonStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes() { |
| $rng = new \RobThree\Auth\Providers\Rng\OpenSSLRNGProvider(false); |
| foreach ($this->getRngTestLengths() as $l) |
| $this->assertEquals($l, strlen($rng->getRandomBytes($l))); |
| $this->assertEquals(false, $rng->isCryptographicallySecure()); |
| } |
| |
| |
| private function getRngTestLengths() { |
| return array(1, 16, 32, 256); |
| } |
| |
| private function DecodeDataUri($datauri) { |
| if (preg_match('/data:(?P<mimetype>[\w\.\-\/]+);(?P<encoding>\w+),(?P<data>.*)/', $datauri, $m) === 1) { |
| return array( |
| 'mimetype' => $m['mimetype'], |
| 'encoding' => $m['encoding'], |
| 'data' => base64_decode($m['data']) |
| ); |
| } |
| return null; |
| } |
| } |
| |
| class TestRNGProvider implements IRNGProvider { |
| private $isSecure; |
| |
| function __construct($isSecure = false) { |
| $this->isSecure = $isSecure; |
| } |
| |
| public function getRandomBytes($bytecount) { |
| $result = ''; |
| for ($i=0; $i<$bytecount; $i++) |
| $result.=chr($i); |
| return $result; |
| |
| } |
| |
| public function isCryptographicallySecure() { |
| return $this->isSecure; |
| } |
| } |
| |
| class TestQrProvider implements IQRCodeProvider { |
| public function getQRCodeImage($qrtext, $size) { |
| return $qrtext . '@' . $size; |
| } |
| |
| public function getMimeType() { |
| return 'test/test'; |
| } |
| } |
| |
| class TestTimeProvider implements ITimeProvider { |
| private $time; |
| |
| function __construct($time) { |
| $this->time = $time; |
| } |
| |
| public function getTime() { |
| return $this->time; |
| } |
| } |