Zeile 1 | Zeile 1 |
---|
<?php
/**
|
<?php
/**
|
* PHP Class for handling Google Authenticator 2-factor authentication
| * 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
|
* * @author Michael Kliewe * @copyright 2012 Michael Kliewe * @license http://www.opensource.org/licenses/bsd-license.php BSD License
|
| *
|
* @link http://www.phpgangsta.de/ */
|
* @link http://www.phpgangsta.de/ */
|
| |
class PHPGangsta_GoogleAuthenticator { protected $_codeLength = 6;
| class PHPGangsta_GoogleAuthenticator { protected $_codeLength = 6;
|
Zeile 18 | Zeile 18 |
---|
* 16 characters, randomly chosen from the allowed base32 characters. * * @param int $secretLength
|
* 16 characters, randomly chosen from the allowed base32 characters. * * @param int $secretLength
|
| *
|
* @return string */ public function createSecret($secretLength = 16) { $validChars = $this->_getBase32LookupTable();
|
* @return string */ public function createSecret($secretLength = 16) { $validChars = $this->_getBase32LookupTable();
|
unset($validChars[32]);
| // Valid secret lengths are 80 to 640 bits if ($secretLength < 16 || $secretLength > 128) { throw new Exception('Bad secret length'); }
|
$secret = '';
|
$secret = '';
|
for ($i = 0; $i < $secretLength; $i++) { $secret .= $validChars[array_rand($validChars)];
| $rnd = false; if (function_exists('random_bytes')) { $rnd = random_bytes($secretLength); } elseif (function_exists('mcrypt_create_iv')) { $rnd = mcrypt_create_iv($secretLength, MCRYPT_DEV_URANDOM); } elseif (function_exists('openssl_random_pseudo_bytes')) { $rnd = openssl_random_pseudo_bytes($secretLength, $cryptoStrong); if (!$cryptoStrong) { $rnd = false; } } if ($rnd !== false) { for ($i = 0; $i < $secretLength; ++$i) { $secret .= $validChars[ord($rnd[$i]) & 31]; } } else { throw new Exception('No source of secure random');
|
}
|
}
|
|
|
return $secret; }
/**
|
return $secret; }
/**
|
* Calculate the code, with given secret and point in time * * @param string $secret
| * Calculate the code, with given secret and point in time. * * @param string $secret
|
* @param int|null $timeSlice
|
* @param int|null $timeSlice
|
| *
|
* @return string */ public function getCode($secret, $timeSlice = null) { if ($timeSlice === null) { $timeSlice = floor(time() / 30);
|
* @return string */ public function getCode($secret, $timeSlice = null) { if ($timeSlice === null) { $timeSlice = floor(time() / 30);
|
}
$secretkey = $this->_base32Decode($secret);
| }
$secretkey = $this->_base32Decode($secret);
|
// Pack time into binary string $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
| // Pack time into binary string $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
|
Zeile 63 | Zeile 84 |
---|
$value = $value & 0x7FFFFFFF;
$modulo = pow(10, $this->_codeLength);
|
$value = $value & 0x7FFFFFFF;
$modulo = pow(10, $this->_codeLength);
|
|
|
return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT); }
/**
|
return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT); }
/**
|
* Get QR-Code URL for image, from google charts
| * Get QR-Code URL for image, from google charts.
|
* * @param string $name * @param string $secret
|
* * @param string $name * @param string $secret
|
| * @param string $title * @param array $params *
|
* @return string */
|
* @return string */
|
public function getQRCodeGoogleUrl($name, $secret) {
| public function getQRCodeGoogleUrl($name, $secret, $title = null, $params = array()) { $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';
|
$urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
|
$urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
|
return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.'';
| if (isset($title)) { $urlencoded .= urlencode('&issuer='.urlencode($title)); }
return 'https://api.qrserver.com/v1/create-qr-code/?data='.$urlencoded.'&size='.$width.'x'.$height.'&ecc='.$level;
|
}
/**
|
}
/**
|
* Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now
| * 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)
| |
* @return bool
|
* @return bool
|
*/ public function verifyCode($secret, $code, $discrepancy = 1) { $currentTimeSlice = floor(time() / 30);
| */ public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null) { if ($currentTimeSlice === null) { $currentTimeSlice = floor(time() / 30); }
if (strlen($code) != 6) { return false; }
|
|
|
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
| for ($i = -$discrepancy; $i <= $discrepancy; ++$i) {
|
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
|
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
|
if ($calculatedCode == $code ) {
| if ($this->timingSafeEquals($calculatedCode, $code)) {
|
return true; } }
return false; }
|
return true; } }
return false; }
|
|
|
/**
|
/**
|
* Set the code length, should be >=6 *
| * Set the code length, should be >=6. *
|
* @param int $length
|
* @param int $length
|
| *
|
* @return PHPGangsta_GoogleAuthenticator */ public function setCodeLength($length) { $this->_codeLength = $length;
|
* @return PHPGangsta_GoogleAuthenticator */ public function setCodeLength($length) { $this->_codeLength = $length;
|
|
|
return $this; }
/**
|
return $this; }
/**
|
* Helper class to decode base32
| * Helper class to decode base32.
|
* * @param $secret
|
* * @param $secret
|
| *
|
* @return bool|string */ protected function _base32Decode($secret) {
|
* @return bool|string */ protected function _base32Decode($secret) {
|
if (empty($secret)) return '';
| if (empty($secret)) { return ''; }
|
$base32chars = $this->_getBase32LookupTable(); $base32charsFlipped = array_flip($base32chars);
$paddingCharCount = substr_count($secret, $base32chars[32]); $allowedValues = array(6, 4, 3, 1, 0);
|
$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 (!in_array($paddingCharCount, $allowedValues)) { return false; } for ($i = 0; $i < 4; ++$i) {
|
if ($paddingCharCount == $allowedValues[$i] &&
|
if ($paddingCharCount == $allowedValues[$i] &&
|
substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false;
| substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) { return false; }
|
}
|
}
|
$secret = str_replace('=','', $secret);
| $secret = str_replace('=', '', $secret);
|
$secret = str_split($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++) {
| $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);
|
$x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
|
}
| }
|
$eightBits = str_split($x, 8);
|
$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:"";
| for ($z = 0; $z < count($eightBits); ++$z) { $binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : '';
|
} }
|
} }
|
|
|
return $binaryString;
|
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 *
| }
/** * Get array with all 32 characters for decoding from/encoding to base32. *
|
* @return array
|
* @return array
|
*/
| */
|
protected function _getBase32LookupTable()
|
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
|
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
| '=', // padding char
|
);
|
);
|
| }
/** * A timing safe equals comparison * more info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html. * * @param string $safeString The internal (safe) value to be checked * @param string $userString The user submitted (unsafe) value * * @return bool True if the two strings are identical */ private function timingSafeEquals($safeString, $userString) { if (function_exists('hash_equals')) { return hash_equals($safeString, $userString); } $safeLen = strlen($safeString); $userLen = strlen($userString);
if ($userLen != $safeLen) { return false; }
$result = 0;
for ($i = 0; $i < $userLen; ++$i) { $result |= (ord($safeString[$i]) ^ ord($userString[$i])); }
// They are only identical strings if $result is exactly 0... return $result === 0;
|
} }
| } }
|