diff options
author | Thomas Steur <tsteur@users.noreply.github.com> | 2018-12-03 06:27:29 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-12-03 06:27:29 +0300 |
commit | 284bdc0816dd2eff4010e4be42812ff3cc7e25e1 (patch) | |
tree | 88c60d0e72bae97b5467c5ad7693a64dd477bf3e /libs | |
parent | e679e0383496383b00f95fd5fd0e42eed4ca49fe (diff) |
Implement Two Factor Authentication (#13670)
Diffstat (limited to 'libs')
-rw-r--r-- | libs/Authenticator/LICENSE.md | 22 | ||||
-rw-r--r-- | libs/Authenticator/README.md | 85 | ||||
-rw-r--r-- | libs/Authenticator/TwoFactorAuthenticator.php | 195 |
3 files changed, 302 insertions, 0 deletions
diff --git a/libs/Authenticator/LICENSE.md b/libs/Authenticator/LICENSE.md new file mode 100644 index 0000000000..530a37b0ab --- /dev/null +++ b/libs/Authenticator/LICENSE.md @@ -0,0 +1,22 @@ +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: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. 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.
\ No newline at end of file diff --git a/libs/Authenticator/README.md b/libs/Authenticator/README.md new file mode 100644 index 0000000000..fee7a28979 --- /dev/null +++ b/libs/Authenticator/README.md @@ -0,0 +1,85 @@ +Google Authenticator PHP class +============================== + +* Copyright (c) 2012-2016, [http://www.phpgangsta.de](http://www.phpgangsta.de) +* Author: Michael Kliewe, [@PHPGangsta](http://twitter.com/PHPGangsta) and [contributors](https://github.com/PHPGangsta/GoogleAuthenticator/graphs/contributors) +* Licensed under the BSD License. + +[![Build Status](https://travis-ci.org/PHPGangsta/GoogleAuthenticator.png?branch=master)](https://travis-ci.org/PHPGangsta/GoogleAuthenticator) + +This PHP class can be used to interact with the Google Authenticator mobile app for 2-factor-authentication. This class +can generate secrets, generate codes, validate codes and present a QR-Code for scanning the secret. It implements TOTP +according to [RFC6238](https://tools.ietf.org/html/rfc6238) + +For a secure installation you have to make sure that used codes cannot be reused (replay-attack). You also need to +limit the number of verifications, to fight against brute-force attacks. For example you could limit the amount of +verifications to 10 tries within 10 minutes for one IP address (or IPv6 block). It depends on your environment. + +Usage: +------ + +See following example: + +```php +<?php +require_once 'PHPGangsta/GoogleAuthenticator.php'; + +$ga = new PHPGangsta_GoogleAuthenticator(); +$secret = $ga->createSecret(); +echo "Secret is: ".$secret."\n\n"; + +$qrCodeUrl = $ga->getQRCodeGoogleUrl('Blog', $secret); +echo "Google Charts URL for the QR-Code: ".$qrCodeUrl."\n\n"; + +$oneCode = $ga->getCode($secret); +echo "Checking Code '$oneCode' and Secret '$secret':\n"; + +$checkResult = $ga->verifyCode($secret, $oneCode, 2); // 2 = 2*30sec clock tolerance +if ($checkResult) { + echo 'OK'; +} else { + echo 'FAILED'; +} +``` +Running the script provides the following output: +``` +Secret is: OQB6ZZGYHCPSX4AK + +Google Charts URL for the QR-Code: https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/infoATphpgangsta.de%3Fsecret%3DOQB6ZZGYHCPSX4AK + +Checking Code '848634' and Secret 'OQB6ZZGYHCPSX4AK': +OK +``` + +Installation: +------------- + +- Use [Composer](https://getcomposer.org/doc/01-basic-usage.md) to + install the package + +- From project root directory execute following + +```composer install``` + +- [Composer](https://getcomposer.org/doc/01-basic-usage.md) will take care of autoloading + the library. Just include the following at the top of your file + + `require_once __DIR__ . '/../vendor/autoload.php';` + +Run Tests: +---------- + +- All tests are inside `tests` folder. +- Execute `composer install` and then run the tests from project root + directory +- Run as `phpunit tests` from the project root directory + + +ToDo: +----- +- ??? What do you need? + +Notes: +------ + +If you like this script or have some features to add: contact me, visit my blog, fork this project, send pull requests, you know how it works.
\ No newline at end of file diff --git a/libs/Authenticator/TwoFactorAuthenticator.php b/libs/Authenticator/TwoFactorAuthenticator.php new file mode 100644 index 0000000000..fc0b962856 --- /dev/null +++ b/libs/Authenticator/TwoFactorAuthenticator.php @@ -0,0 +1,195 @@ +<?php +/** + * PHP Class for handling Google Authenticator 2-factor authentication + * + * @author Michael Kliewe + * @copyright 2012 Michael Kliewe + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://www.phpgangsta.de/ + * + * small adjustments by @sgiehl / matomo.org + * - renamed class + * - removed method getQRCodeGoogleUrl + */ + +class TwoFactorAuthenticator +{ + protected $_codeLength = 6; + + /** + * Create new secret. + * 16 characters, randomly chosen from the allowed base32 characters. + * + * @param int $secretLength + * @return string + */ + public function createSecret($secretLength = 16) + { + $validChars = $this->_getBase32LookupTable(); + unset($validChars[32]); + + $secret = ''; + for ($i = 0; $i < $secretLength; $i++) { + $secret .= $validChars[array_rand($validChars)]; + } + return $secret; + } + + /** + * Calculate the code, with given secret and point in time + * + * @param string $secret + * @param int|null $timeSlice + * @return string + */ + public function getCode($secret, $timeSlice = null) + { + if ($timeSlice === null) { + $timeSlice = floor(time() / 30); + } + + $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); + // Use last nipple of result as index/offset + $offset = ord(substr($hm, -1)) & 0x0F; + // grab 4 bytes of the result + $hashpart = substr($hm, $offset, 4); + + // Unpak 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); + } + + /** + * 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() + * @return bool + */ + public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null) + { + if ($currentTimeSlice === null) { + $currentTimeSlice = floor(time() / 30); + } + + for ($i = -$discrepancy; $i <= $discrepancy; $i++) { + $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i); + if ($calculatedCode == $code ) { + return true; + } + } + + return false; + } + + /** + * Set the code length, should be >=6 + * + * @param int $length + * @return self + */ + public function setCodeLength($length) + { + $this->_codeLength = $length; + return $this; + } + + /** + * Helper class to decode base32 + * + * @param $secret + * @return bool|string + */ + protected function _base32Decode($secret) + { + if (empty($secret)) return ''; + + $base32chars = $this->_getBase32LookupTable(); + $base32charsFlipped = array_flip($base32chars); + + $paddingCharCount = substr_count($secret, $base32chars[32]); + $allowedValues = array(6, 4, 3, 1, 0); + if (!in_array($paddingCharCount, $allowedValues)) return false; + for ($i = 0; $i < 4; $i++){ + if ($paddingCharCount == $allowedValues[$i] && + substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false; + } + $secret = str_replace('=','', $secret); + $secret = str_split($secret); + $binaryString = ""; + for ($i = 0; $i < count($secret); $i = $i+8) { + $x = ""; + if (!in_array($secret[$i], $base32chars)) return false; + for ($j = 0; $j < 8; $j++) { + $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); + } + $eightBits = str_split($x, 8); + for ($z = 0; $z < count($eightBits); $z++) { + $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:""; + } + } + return $binaryString; + } + + /** + * Helper class to encode base32 + * + * @param string $secret + * @param bool $padding + * @return string + */ + protected function _base32Encode($secret, $padding = true) + { + if (empty($secret)) return ''; + + $base32chars = $this->_getBase32LookupTable(); + + $secret = str_split($secret); + $binaryString = ""; + for ($i = 0; $i < count($secret); $i++) { + $binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT); + } + $fiveBitBinaryArray = str_split($binaryString, 5); + $base32 = ""; + $i = 0; + while ($i < count($fiveBitBinaryArray)) { + $base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)]; + $i++; + } + if ($padding && ($x = strlen($binaryString) % 40) != 0) { + if ($x == 8) $base32 .= str_repeat($base32chars[32], 6); + elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4); + elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3); + elseif ($x == 32) $base32 .= $base32chars[32]; + } + return $base32; + } + + /** + * Get array with all 32 characters for decoding from/encoding to base32 + * + * @return array + */ + protected function _getBase32LookupTable() + { + return array( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 + 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31 + '=' // padding char + ); + } +}
\ No newline at end of file |