diff options
author | Corey Ballou <ballouc@gmail.com> | 2016-08-30 07:50:50 -0400 |
---|---|---|
committer | Corey Ballou <ballouc@gmail.com> | 2016-08-30 07:50:50 -0400 |
commit | d6620b5d55ebfe1e4956827b5c868bf4aed7e1d2 (patch) | |
tree | 7fdf5c874df792b9bd437c358849cb5997073944 | |
download | GoogleAuthenticatorRedux-d6620b5d55ebfe1e4956827b5c868bf4aed7e1d2.zip GoogleAuthenticatorRedux-d6620b5d55ebfe1e4956827b5c868bf4aed7e1d2.tar.gz GoogleAuthenticatorRedux-d6620b5d55ebfe1e4956827b5c868bf4aed7e1d2.tar.bz2 |
Initial commit.
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | .travis.yml | 9 | ||||
-rw-r--r-- | LICENSE.md | 27 | ||||
-rw-r--r-- | README.md | 142 | ||||
-rw-r--r-- | composer.json | 27 | ||||
-rw-r--r-- | example/index.php | 126 | ||||
-rwxr-xr-x | example/server.sh | 5 | ||||
-rw-r--r-- | phpunit.xml | 15 | ||||
-rw-r--r-- | src/CraftBlue/GoogleAuthenticator.php | 304 | ||||
-rw-r--r-- | tests/GoogleAuthenticatorTest.php | 137 |
10 files changed, 795 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9304ad2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +composer.lock +vendor/* diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6bdc434 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + - 7 + +script: phpunit --coverage-text --configuration tests/phpunit.xml diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..1f0757e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,27 @@ +################# +# New Work # +################# + +Copyright (c) 2016, Craft Blue LLC All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +################# +# Original Work # +################# + +Copyright (c) 2012, Michael Kliewe All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..055a493 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +# Google Authenticator Redux PHP Client # + +[](https://travis-ci.org/CraftBlue/GoogleAuthenticator) + +The library provides support for 2-Factor authentication, often referred to as **2FA**. + +The client is intended to be used with the Google Authenticator mobile app +available on both Android and iOS devices. Google Authenticator is merely an implementation of +the algorithm defined in [RFC6238](http://tools.ietf.org/html/rfc6238), better known as +**TOTP: Time-Based One-Time Password Algorithm**. + +The class contains methods which support: + + * The generation of RFC6238 compliant secret keys + * The generation of Base32 codes based on the secret key + * Creation of QR Code image URLs to present to the user for scanning into the Google Authenticator app + * Validation of a user-submitted code against a known secret key + +## Credits ## + +This project is a full revamp of [PHPGangsta](http://www.phpgangsta.de/)'s original [GoogleAuthenticator](https://github.com/PHPGangsta/GoogleAuthenticator) repository +to promote best practices in PHP security and modern PSR-4 standards with Packagist. + +Fixes and improvements include: + +* Prevention of timing attacks +* Proper pseudo-random secret key generation +* Usage of safe string comparison functions +* Usage of a standardized Base32 library +* Improved examples and documentation +* Improved code comments to point out source of +* Validation and sanitization of QR code labels +* Additional parameter support when generating QR codes +* PSR-4 support + +## Installation ## + +The recommended way to install this library is through Composer. + +``` +# install composer if you don't already have it on your machine +curl -sS https://getcomposer.org/installer | php + +# from your project's base directory, run the composer command to install GoogleAuthenticator +php composer.phar require craftblue/google-authenticator-redux +``` + +Ensure you require Composer's autoloader somewhere in your code: +```php +<?php + +require 'vendor/autoload.php'; +``` + +You'll now have access to autoload and use the client in your code: + +```php +<?php + +require 'vendor/autoload.php'; + +$client = new CraftBlue\GoogleAuthenticator(); +``` + + +To get any updates available from GoogleAuthenticator, you can always run: + +``` +php composer.phar update +``` + +## Example Code ## + +```php +<?php + +// if you are using composer, which is the preferred method of autoloading +require_once('./vendor/autoload.php'); + +// create a new secret for a user wishing to enable 2FA +// you will need to store this securely +$secret = $ga->createSecret(); + +// example of generating a QR code to display to the end user +// note that you need to generate a unique label for each user +// in the format of your application or vendor name (a namespace/prefix) +// followed by a colon, followed by a unique identifier for the user such +// as their login email address to your app or their name +$qrCodeUrl = $ga->getQRCodeUrl('MyVendorPrefix:userlogin@gmail.com', $secret); +echo '<img src="' . $qrCodeUrl . '" />'; + +// retrieve an example valid code +// (usually the user would supply this for you from the Google Authenticator app) +$code = $ga->getCode($secret); + +// example of verifying that a code is valid for the secret at this given time +if ($ga->verifyCode($secret, $code, 2)) { + echo 'VERIFIED'; +} else { + echo 'VERIFICATION FAILED'; +} +``` + +## Live Demo ## + +This project contains an `/example` directory which contains a working `index.php` demo. +If you have PHP running on your machine, you can run the example code using +PHP's built in web server following these steps: + +1. Checkout the repository, i.e. `git checkout ____` +1. Navigate to the `example/` directory +1. Start PHP's built in web server for the project by running the included bash script: + ```bash + ./server.sh + ``` +1. Navigate your web browser to `http://127.0.0.1:8000` for the demo +1. View the source code of `index.php` to learn how the demo works + +## Security Considerations ## + +* Secret keys should never be exposed to end users on the client side +* Secret keys should be stored securely in your server-side code +* Secret keys should be unique to each user. Only generate one per user. +* It's recommended you two-way encrypt secret keys via tamper-resistant hardware encryption and +expose them only when required. For example, you should only decrypt the secret key +when verifying the user-submitted OTP code/value. The secret key should be immediately +re-encrypted to limit exposure in your RAM. +* Your client and server should have CSRF protection to prevent against replay attacks. +* Implement rate limiting on your code verification endpoint with either exponential backoff delays +or forced CAPTCHAs to ensure users cannot brute force guess any codes. + +## Running Tests ## + +Depending on if you have PHPUnit available globally on your system or not, you can run the following: + +```bash +# if you have phpunit globally, run this from the base project directory: +phpunit + +# after updating composer, run this from the base project directory: +vendor/bin/phpunit +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9091f5f --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "CraftBlue/google-authenticator-redux", + "description": "A modern take on a PHP implementation of Google Authenticator 2-Factor Authentication focusing on best practices in security.", + "type": "library", + "keywords": ["google", "authenticator", "google-authenticator", "auth", "authentication", "2fa", "two-factor-auth", "security"], + "license": "BSD-4-Clause", + "version":"1.0.0", + "authors": [{ + "name": "Michael Kliewe" + }, + { + "name": "Corey Ballou" + } + ], + "require": { + "christian-riesen/base32": "1.3.*", + "ircmaxell/random-lib": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "autoload": { + "psr-4": { + "CraftBlue\\": "src/CraftBlue/" + } + } +} diff --git a/example/index.php b/example/index.php new file mode 100644 index 0000000..ebbd063 --- /dev/null +++ b/example/index.php @@ -0,0 +1,126 @@ +<?php +// load our composer file +require_once('../vendor/autoload.php'); + +// load the client library +$ga = new \CraftBlue\GoogleAuthenticator(); + +// this demo is insecure and only for demonstration purposes only +// you should NEVER publicly expose your secret key + +// check if we generated a 16 character secret for the user yet +if (!empty($_COOKIE['secret'])) { + $secret = $_COOKIE['secret']; +} else { + $secret = $ga->createSecret(); + setcookie('secret', $secret); +} + +// look for a form submitted code to verify +if (!empty($_POST['code'])) { + $code = $_POST['code']; + $qrCodeUrl = $ga->getQRCodeUrl('example.user@gmail.com', $secret, 'ExampleCompany'); + $isExampleCode = false; +} else { + // retrieve the Google QR code URL based on our secret + $qrCodeUrl = $ga->getQRCodeUrl('example.user@gmail.com', $secret, 'ExampleCompany'); + + // generate an example correct code based on the secret (to be used as an example) + $isExampleCode = true; + $code = $ga->getCode($secret); +} + +// check if the secret matches the code (with 60 second window) +$checkResult = $ga->verifyCode($secret, $code, 2); +?> +<html> + <head> + <title>Google Authenticator PHP Client - Example Usage</title> + <style> + body { font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif; } + blockquote { color:#999;font-style: italic;font-family:Georgia,serif; } + fieldset { padding:20px;border:1px solid #ccc;background:#f9f9f9; } + .container { width: 75%; max-width: 760px; margin: 0 auto; padding: 40px 0; } + .centered { text-align: center; } + .fw { display: inline-block; width: 150px; margin-right: 10px; } + .error { color: #900 } + .success { color: #090 } + </style> + </head> + <body> + <div class="container"> + <h1>Google Authenticator PHP Client - Example Usage</h1> + + <p> + To test, please first install and open the Google Authenticator app on your iPhone or Android device. + </p> + + <p class="centered"> + <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en" target="_blank"><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height="48" style="vertical-align:middle"/></a> + <a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8" target="_blank"><img alt='Available on the App Store' src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5d/Available_on_the_App_Store_%28black%29.png/320px-Available_on_the_App_Store_%28black%29.png" height="32" style="vertical-align:middle;padding:8px;" /></a> + </p> + + <p> + <em class="error"> + CAUTION: This example is for testing purposes only. You should never publicly expose + your secret. Here are <a href="http://tools.ietf.org/html/rfc6238" target="_blank">RFC6238</a>'s security + recommendations: + </em> + </p> + + <blockquote> + <p> + We also RECOMMEND storing the keys securely in the validation system, + and, more specifically, encrypting them using tamper-resistant + hardware encryption and exposing them only when required: for + example, the key is decrypted when needed to verify an OTP value, and + re-encrypted immediately to limit exposure in the RAM to a short + period of time. + </p> + <p> + The key store MUST be in a secure area, to avoid, as much as + possible, direct attack on the validation system and secrets + database. Particularly, access to the key material should be limited + to programs and processes required by the validation system only. + </p> + </blockquote> + + <ul> + <li><strong class="fw">Secret:</strong> <code><?= $secret ?></code></li> + <li><strong class="fw"><?= $isExampleCode ? 'Example ' : '' ?>Code:</strong> <code><?= $code ?></code></li> + <li> + <strong class="fw">Code Verification:</strong> + <?= $checkResult ? '<code class="success">VERIFIED</code>' : '<code class="error">FAILED</code>' ?> + </li> + </ul> + + <h2 style="margin-top:40px">Test Google Authenticator</h2> + + <form method="post" id="verify-app-code"> + <ol> + <li>Open your Google Authenticator app on your mobile device and scan the QR Code below.</li> + <li>Post the 6 digit code generated by Google Authenticator here and submit to verify/authenticate it.</li> + + </ol> + + <p class="centered"> + <img src="<?= $qrCodeUrl ?>" /> + </p> + + <fieldset> + <label>Google Authenticator Code:</label> + <input type="text" name="code" id="code" placeholder="As shown on your app" /> + <button type="submit">Verify Code ›</button> + <input type="hidden" name="secret" id="secret" value="<?= htmlentities($secret) ?>" /> + </fieldset> + + <p> + <small> + If code verification fails after repeated retries, a new secret may have been generated for you. + This means you will have to delete the test entry from your Google Authenticator app and add a new one. + </small> + </p> + </form> + </div> + </body> +</html>
\ No newline at end of file diff --git a/example/server.sh b/example/server.sh new file mode 100755 index 0000000..c96172e --- /dev/null +++ b/example/server.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +php -S 127.0.0.1:8000 + +echo "Open 127.0.0.1:8000 in your browser to test"
\ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d70bdda --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit bootstrap="./vendor/autoload.php" colors="true"> + <testsuites> + <testsuite name="GoogleAuthenticator Test Suite"> + <directory>./tests</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory suffix=".php">src/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/src/CraftBlue/GoogleAuthenticator.php b/src/CraftBlue/GoogleAuthenticator.php new file mode 100644 index 0000000..9b99432 --- /dev/null +++ b/src/CraftBlue/GoogleAuthenticator.php @@ -0,0 +1,304 @@ +<?php + +namespace CraftBlue; + +use RandomLib\Factory as RandomFactory; +use SecurityLib\Strength as SecurityStrength; +use Base32\Base32; + +/** + * A Google Authenticator 2Factor authentication implementation written in PHP which Follows RFC6238. + * + * This work is derived from the GoogleAuthenticator project written by Michael Kliewe, @PHPGangsta. + * This new version focuses on industry best practices in security by utilizing third party libraries + * which specialize in the generation of Base32 strings, cryptographically strong pseudorandom strings, + * and performing safe string comparisons that aren't prone to timing attacks. + * + * @author Michael Kliewe, Corey Ballou + * @copyright 2016 POP! Online LLC + * @license MIT + * @link https://github.com/PHPGangsta/GoogleAuthenticator + * @link http://tools.ietf.org/html/rfc6238 + * @link https://pop.co/ + */ +class GoogleAuthenticator +{ + + /** + * The length of the code you wish to have generated for the user to key in. + * @var int + */ + protected $_codeLength = 6; + + /** + * GoogleAuthenticator constructor. + * + * @access public + * @param int $codeLength + */ + public function __construct($codeLength = 6) + { + $this->setCodeLength($codeLength); + } + + /** + * Create a new Base32 encoded secret. + * + * @param int $length The length of the secret key you wish to generate + * @return string + */ + public function createSecret($length = 16) + { + $factory = new RandomFactory(); + $generator = $factory->getGenerator(new SecurityStrength(SecurityStrength::MEDIUM)); + + return $generator->generateString($length, $this->getBase32Characters()); + } + + /** + * Given a secret and a point in time reference, handle calculating the Google Authenticator code based on + * RFC6238, entitled "TOTP: Time-Based One-Time Password Algorithm." + * + * @access public + * @param string $encodedSecret + * @param int|null $timeSlice A timeSlice represents seconds since the Unix epoch divided by 30 + * @link https://tools.ietf.org/html/rfc6238 + * @return string + */ + public function getCode($encodedSecret, $timeSlice = null) + { + // if we have no time, generate one ourselves + if ($timeSlice === null) { + $timeSlice = floor(time() / 30); + } + + $secretKey = Base32::decode($encodedSecret); + + // 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); + // Use last nipple of result as index/offset + $offset = ord($this->safeSubstr($hm, -1)) & 0x0F; + // grab 4 bytes of the result + $hashpart = $this->safeSubstr($hm, $offset, 4); + // Unpack binary value + $value = unpack('N', $hashpart); + $value = $value[1]; + // Only 32 bits + $value = $value & 0x7FFFFFFF; + $modulo = pow(10, $this->_codeLength); + + return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT); + } + + /** + * Generate a Google Chart QR code url to be used by the Google Authenticator application for snapping a picture + * of the QR code. Please note that a label is used to identify which account a key is associated with. It should + * be unique to a particular user. It contains an account name, which is a URI-encoded string, optionally prefixed + * by an issuer string identifying the provider or service managing that account. This issuer prefix can be used to + * prevent collisions between different accounts with different providers that might be identified using the same + * account name, e.g. the user's email address. + * + * To adjust your QR code image, you can optionally pass the following keys in $params: + * + * - height: The width of your QR code image (integer) + * - width: The height of your QR code image (integer) + * - level: The error correction level allowed by the QR code. Valid values are: + * - L: [Default] Allows recovery of up to 7% data loss + * - M: Allows recovery of up to 15% data loss + * - Q: Allows recovery of up to 25% data loss + * - H: Allows recovery of up to 30% data loss + * + * @access public + * @param string $label + * @param string $secret + * @param string $issuer + * @param array $params User supplied parameters to adjust the output of the QR code + * @link https://github.com/google/google-authenticator/wiki/Key-Uri-Format + * @return string + */ + public function getQRCodeUrl($label, $secret, $issuer = null, $params = array()) + { + $label = $this->validateLabel($label, $issuer); + + $url = 'otpauth://totp/' . $label . '?secret=' . $secret; + + // add the issuer (company namespace) + if (!empty($issuer) && is_string($issuer)) { + $url .= '&issuer=' . rawurlencode($issuer); + } + + $encodedUrl = urlencode($url); + + $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']) && in_array($params['level'], array('L', 'M', 'Q', 'H')) ? $params['level'] : 'M'; + + return 'https://chart.googleapis.com/chart?chs='.$width.'x'.$height.'&chld='.$level.'|0&cht=qr&chl='.$encodedUrl.''; + } + + /** + * Check if the client supplied code is correct by comparing it to the Google secret version. + * + * Note that any codes generated by Authenticator within the following time period will be accepted: + * (current_timestamp - ($tolerance * 30 sec)) to (current timestamp + ($tolerance * 30 sec)) + * + * @access public + * @param string $secret + * @param string $code + * @param int $tolerance The +/- time drift allowed when verifying the authenticator code (in 30sec increments) + * @return bool + */ + public function verifyCode($secret, $code, $tolerance = 1) + { + $timeSlice = floor(time() / 30); + + for ($i = -$tolerance; $i <= $tolerance; $i++) { + if ($this->strCompare($this->getCode($secret, $timeSlice + $i), $code)) { + return true; + } + } + + return false; + } + + /** + * Set the code length to generate for the user. + * + * @access public + * @param int $codeLength + * @return GoogleAuthenticator + */ + public function setCodeLength($codeLength = 6) + { + if (!is_integer($codeLength)) { + throw new Exception('You must set a valid code length.'); + } + + $this->_codeLength = $codeLength; + + return $this; + } + + /** + * Validate that the user supplied label for QR code generation is in the proper format. + * + * @access public + * @param string $label + * @param string $issuer + * @link https://github.com/google/google-authenticator/wiki/Key-Uri-Format#label + * @return string + * @throws \Exception + */ + public function validateLabel($label, $issuer) + { + $msg = + 'Your label cannot contain more than a single colon separating the issuer from the label, i.e. "issuer:label".' . + ' Examples of valid labels are "YourCompany:userlogin@yoursite.com" and "BigCoNamespace:JohnDoe".' . + ' Please read: https://github.com/google/google-authenticator/wiki/Key-Uri-Format#label'; + + // it's possible the label doesn't have an issuer + $hasColon = strpos($label, ':') !== false; + $hasColonEncoded = strpos($label, '%3A') !== false; + if (!$hasColon && !$hasColonEncoded) { + return urlencode($label); + } + + // if we have an issuer and the label has a colon, we need to verify the label's issuer matches + $parts = $hasColon ? explode(':', $label) : explode('%3A', $label); + if (count($parts) != 2) { + throw new Exception($msg); + } + + if (!empty($issuer)) { + if ($parts[0] !== $issuer) { + throw new Exception($msg); + } + + return rawurlencode($parts[0]) . ':' . rawurlencode($parts[1]); + } + + return rawurlencode($label); + } + + /** + * A timing safe string equality check thanks to Anthony Ferrera. + * + * @access protected + * @param string $safe The internal (safe) value to be checked + * @param string $user The user submitted (unsafe) value + * @link http://blog.ircmaxell.com/2014/11/its-all-about-time.html + * @return boolean True if the two strings are identical. + */ + protected function strCompare($safe, $user) + { + if (version_compare(phpversion(), '5.6.0', '>')) { + return hash_equals($safe, $user); + } + + $safeLen = $this->safeStrlen($safe); + $userLen = $this->safeStrlen($user); + + if ($userLen != $safeLen) { + return false; + } + + $result = 0; + + for ($i = 0; $i < $userLen; $i++) { + $result |= (ord($safe[$i]) ^ ord($user[$i])); + } + + // They are only identical strings if $result is exactly 0... + return $result === 0; + } + + /** + * Return the length of a string, even in the presence of mbstring.func_overload. + * + * @access protected + * @param string $string The string we're measuring + * @return int + */ + protected function safeStrlen($string) + { + if (function_exists('mb_strlen')) { + return mb_strlen($string, '8bit'); + } + + return strlen($string); + } + + /** + * Return a string contained within a string, even in the presence of mbstring.func_overload. + * + * @access protected + * @param string $string The string we're searching + * @param int $start What offset should we begin + * @param int|null $length How long should the substring be? (default: the remainder) + * @return string + */ + protected function safeSubstr($string, $start = 0, $length = null) + { + if (function_exists('mb_substr')) { + return mb_substr($string, $start, $length, '8bit'); + } elseif ($length !== null) { + return substr($string, $start, $length); + } + + return substr($string, $start); + } + + /** + * Return the set of allowable characters in a base32 string. + * + * @access protected + * @return string + */ + protected function getBase32Characters() + { + return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + } + +}
\ No newline at end of file diff --git a/tests/GoogleAuthenticatorTest.php b/tests/GoogleAuthenticatorTest.php new file mode 100644 index 0000000..b1700b7 --- /dev/null +++ b/tests/GoogleAuthenticatorTest.php @@ -0,0 +1,137 @@ +<?php + +use POPdotco\GoogleAuthenticator; + +class GoogleAuthenticatorTest extends PHPUnit_Framework_TestCase { + + /** + * @var GoogleAuthenticator + */ + protected $ga; + + /** + * PHPUnit setup. + */ + protected function setUp() + { + $this->ga = new GoogleAuthenticator(); + } + + /** + * + */ + public function testItCanBeInstantiated() + { + $ga = new GoogleAuthenticator(); + $this->assertInstanceOf('POPdotco\GoogleAuthenticator', $ga); + } + + /** + * Ensure that our default secret is 16 characters long. + */ + public function testCreateSecretDefaultsToSixteenCharacters() + { + $secret = $this->ga->createSecret(); + $this->assertEquals(strlen($secret), 16); + } + + /** + * Ensure that the user can create a secret of varying lengths from 0 to 100. + */ + public function testCreateSecretLengthCanBeCustomized() + { + for ($secretLength = 10; $secretLength < 100; $secretLength++) { + $secret = $this->ga->createSecret($secretLength); + $this->assertEquals(strlen($secret), $secretLength); + } + } + + /** + * Ensure that the generated QR code URL is as expected. + */ + public function testgetQRCodeGoogleUrlReturnsCorrectUrl() + { + $secret = 'SECRET'; + $name = 'Test'; + $url = $this->ga->getQRCodeUrl($name, $secret); + + $urlParts = parse_url($url); + parse_str($urlParts['query'], $queryStringArray); + + $this->assertEquals($urlParts['scheme'], 'https'); + $this->assertEquals($urlParts['host'], 'chart.googleapis.com'); + $this->assertEquals($urlParts['path'], '/chart'); + + $expectedChl = 'otpauth://totp/' . $name . '?secret=' . $secret; + + $this->assertEquals($queryStringArray['chl'], $expectedChl); + } + + /** + * Test that we properly verify both a valid and invalid code when using the current timestamp. + */ + public function testVerifyCode() + { + $secret = 'SECRET'; + $code = $this->ga->getCode($secret); + $result = $this->ga->verifyCode($secret, $code); + + $this->assertEquals(true, $result); + + $code = 'INVALIDCODE'; + $result = $this->ga->verifyCode($secret, $code); + + $this->assertEquals(false, $result); + } + + /** + * Ensure that we can adjust the length of the code and still have the instance returned. + */ + public function testSetCodeLength() + { + $result = $this->ga->setCodeLength(6); + + $this->assertInstanceOf('POPdotco\GoogleAuthenticator', $result); + } + + /** + * Validate that a code generated with a specific secret and time slice matches the anticipated output. + * + * @param string $secret The secret value used to hash the code + * @param int $timeSlice The timestamp passed at the time of the code creation + * @param string $code The resulting code that should be generated + * @dataProvider codeProvider + */ + public function testGetCodeReturnsCorrectValues($secret, $timeSlice, $code) + { + $generatedCode = $this->ga->getCode($secret, $timeSlice); + + $this->assertEquals($code, $generatedCode); + } + + public function testSomeShit() + { + $secret = $this->ga->createSecret(); + + $code = $this->ga->getCode($secret); + $result = $this->ga->verifyCode($secret, $code, 1); + + echo print_r($result, true); + } + + /** + * A provider of codes to verify for correctness. + * + * @return array + */ + public function codeProvider() + { + return array( + // secret, time, code + array('SECRET', 0, '200470'), + array('SECRET', 1385909245, '780018'), + array('SECRET', 1378934578, '705013'), + ); + } + +}
\ No newline at end of file |