| 1 |
<?php |
| 2 |
namespace dologin\lib; |
| 3 |
|
| 4 |
/** |
| 5 |
* PHP Class for handling Google Authenticator 2-factor authentication. |
| 6 |
* |
| 7 |
* @author Michael Kliewe |
| 8 |
* @copyright 2012 Michael Kliewe |
| 9 |
* @license http://www.opensource.org/licenses/bsd-license.php BSD License |
| 10 |
* |
| 11 |
* @link http://www.phpgangsta.de/ |
| 12 |
*/ |
| 13 |
class Two_FA_Lib { |
| 14 |
|
| 15 |
protected $_codeLength = 6; |
| 16 |
|
| 17 |
/** |
| 18 |
* Create new secret. |
| 19 |
* 16 characters, randomly chosen from the allowed base32 characters. |
| 20 |
* |
| 21 |
* @param int $secretLength |
| 22 |
* |
| 23 |
* @return string |
| 24 |
*/ |
| 25 |
public function createSecret( $secretLength = 16 ) { |
| 26 |
$validChars = $this->_getBase32LookupTable(); |
| 27 |
|
| 28 |
// Valid secret lengths are 80 to 640 bits |
| 29 |
if ( $secretLength < 16 || $secretLength > 128 ) { |
| 30 |
throw new \Exception( 'Bad secret length' ); |
| 31 |
} |
| 32 |
$secret = ''; |
| 33 |
$rnd = false; |
| 34 |
if ( function_exists( 'random_bytes' ) ) { |
| 35 |
$rnd = random_bytes( $secretLength ); |
| 36 |
} elseif ( function_exists( 'mcrypt_create_iv' ) ) { |
| 37 |
$rnd = mcrypt_create_iv( $secretLength, MCRYPT_DEV_URANDOM ); |
| 38 |
} elseif ( function_exists( 'openssl_random_pseudo_bytes' ) ) { |
| 39 |
$rnd = openssl_random_pseudo_bytes( $secretLength, $cryptoStrong ); |
| 40 |
if ( ! $cryptoStrong ) { |
| 41 |
$rnd = false; |
| 42 |
} |
| 43 |
} |
| 44 |
if ( $rnd !== false ) { |
| 45 |
for ( $i = 0; $i < $secretLength; ++$i ) { |
| 46 |
$secret .= $validChars[ ord( $rnd[ $i ] ) & 31 ]; |
| 47 |
} |
| 48 |
} else { |
| 49 |
throw new \Exception( 'No source of secure random' ); |
| 50 |
} |
| 51 |
|
| 52 |
return $secret; |
| 53 |
} |
| 54 |
|
| 55 |
/** |
| 56 |
* Calculate the code, with given secret and point in time. |
| 57 |
* |
| 58 |
* @param string $secret |
| 59 |
* @param int|null $timeSlice |
| 60 |
* |
| 61 |
* @return string |
| 62 |
*/ |
| 63 |
public function getCode( $secret, $timeSlice = null ) { |
| 64 |
if ( $timeSlice === null ) { |
| 65 |
$timeSlice = floor( time() / 30 ); |
| 66 |
} |
| 67 |
|
| 68 |
$secretkey = $this->_base32Decode( $secret ); |
| 69 |
|
| 70 |
// Pack time into binary string |
| 71 |
$time = chr( 0 ) . chr( 0 ) . chr( 0 ) . chr( 0 ) . pack( 'N*', $timeSlice ); |
| 72 |
// Hash it with users secret key |
| 73 |
$hm = hash_hmac( 'SHA1', $time, $secretkey, true ); |
| 74 |
// Use last nipple of result as index/offset |
| 75 |
$offset = ord( substr( $hm, -1 ) ) & 0x0F; |
| 76 |
// grab 4 bytes of the result |
| 77 |
$hashpart = substr( $hm, $offset, 4 ); |
| 78 |
|
| 79 |
// Unpak binary value |
| 80 |
$value = unpack( 'N', $hashpart ); |
| 81 |
$value = $value[1]; |
| 82 |
// Only 32 bits |
| 83 |
$value = $value & 0x7FFFFFFF; |
| 84 |
|
| 85 |
$modulo = pow( 10, $this->_codeLength ); |
| 86 |
|
| 87 |
return str_pad( $value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT ); |
| 88 |
} |
| 89 |
|
| 90 |
/** |
| 91 |
* Get QR-Code URL for image, from google charts. |
| 92 |
* |
| 93 |
* @param string $name |
| 94 |
* @param string $secret |
| 95 |
* @param string $title |
| 96 |
* @param array $params |
| 97 |
* |
| 98 |
* @return string |
| 99 |
*/ |
| 100 |
public function getQRCodeGoogleUrl( $name, $secret, $title = null, $params = array() ) { |
| 101 |
$width = ! empty( $params['width'] ) && (int) $params['width'] > 0 ? (int) $params['width'] : 200; |
| 102 |
$height = ! empty( $params['height'] ) && (int) $params['height'] > 0 ? (int) $params['height'] : 200; |
| 103 |
$level = ! empty( $params['level'] ) && array_search( $params['level'], array( 'L', 'M', 'Q', 'H' ) ) !== false ? $params['level'] : 'M'; |
| 104 |
|
| 105 |
$urlencoded = 'otpauth://totp/' . $name . '?secret=' . $secret; |
| 106 |
if ( isset( $title ) ) { |
| 107 |
$urlencoded .= '&issuer=' . rawurlencode( $title ); |
| 108 |
} |
| 109 |
|
| 110 |
return $urlencoded; |
| 111 |
} |
| 112 |
|
| 113 |
/** |
| 114 |
* Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now. |
| 115 |
* |
| 116 |
* @param string $secret |
| 117 |
* @param string $code |
| 118 |
* @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after) |
| 119 |
* @param int|null $currentTimeSlice time slice if we want use other that time() |
| 120 |
* |
| 121 |
* @return bool |
| 122 |
*/ |
| 123 |
public function verifyCode( $secret, $code, $discrepancy = 1, $currentTimeSlice = null ) { |
| 124 |
return false !== $this->findValidTimeSlice( $secret, $code, $discrepancy, $currentTimeSlice ); |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* Return the matched time slice so callers can block TOTP replay. |
| 129 |
* |
| 130 |
* @param string $secret |
| 131 |
* @param string $code |
| 132 |
* @param int $discrepancy |
| 133 |
* @param int|null $currentTimeSlice |
| 134 |
* |
| 135 |
* @return int|false |
| 136 |
*/ |
| 137 |
public function findValidTimeSlice( $secret, $code, $discrepancy = 1, $currentTimeSlice = null ) { |
| 138 |
if ( $currentTimeSlice === null ) { |
| 139 |
$currentTimeSlice = (int) floor( time() / 30 ); |
| 140 |
} |
| 141 |
|
| 142 |
if ( strlen( $code ) != 6 ) { |
| 143 |
return false; |
| 144 |
} |
| 145 |
|
| 146 |
for ( $i = -$discrepancy; $i <= $discrepancy; ++$i ) { |
| 147 |
$matched_slice = (int) $currentTimeSlice + $i; |
| 148 |
$calculatedCode = $this->getCode( $secret, $matched_slice ); |
| 149 |
if ( $this->timingSafeEquals( $calculatedCode, $code ) ) { |
| 150 |
return $matched_slice; |
| 151 |
} |
| 152 |
} |
| 153 |
|
| 154 |
return false; |
| 155 |
} |
| 156 |
|
| 157 |
/** |
| 158 |
* Set the code length, should be >=6. |
| 159 |
* |
| 160 |
* @param int $length |
| 161 |
* |
| 162 |
* @return PHPGangsta_GoogleAuthenticator |
| 163 |
*/ |
| 164 |
public function setCodeLength( $length ) { |
| 165 |
$this->_codeLength = $length; |
| 166 |
|
| 167 |
return $this; |
| 168 |
} |
| 169 |
|
| 170 |
/** |
| 171 |
* Helper class to decode base32. |
| 172 |
* |
| 173 |
* @param $secret |
| 174 |
* |
| 175 |
* @return bool|string |
| 176 |
*/ |
| 177 |
protected function _base32Decode( $secret ) { |
| 178 |
if ( empty( $secret ) ) { |
| 179 |
return ''; |
| 180 |
} |
| 181 |
|
| 182 |
$base32chars = $this->_getBase32LookupTable(); |
| 183 |
$base32charsFlipped = array_flip( $base32chars ); |
| 184 |
|
| 185 |
$paddingCharCount = substr_count( $secret, $base32chars[32] ); |
| 186 |
$allowedValues = array( 6, 4, 3, 1, 0 ); |
| 187 |
if ( ! in_array( $paddingCharCount, $allowedValues ) ) { |
| 188 |
return false; |
| 189 |
} |
| 190 |
for ( $i = 0; $i < 4; ++$i ) { |
| 191 |
if ( $paddingCharCount == $allowedValues[ $i ] && |
| 192 |
substr( $secret, -( $allowedValues[ $i ] ) ) != str_repeat( $base32chars[32], $allowedValues[ $i ] ) ) { |
| 193 |
return false; |
| 194 |
} |
| 195 |
} |
| 196 |
$secret = str_replace( '=', '', $secret ); |
| 197 |
$secret = str_split( $secret ); |
| 198 |
$binaryString = ''; |
| 199 |
for ( $i = 0; $i < count( $secret ); $i = $i + 8 ) { |
| 200 |
$x = ''; |
| 201 |
if ( ! in_array( $secret[ $i ], $base32chars ) ) { |
| 202 |
return false; |
| 203 |
} |
| 204 |
for ( $j = 0; $j < 8; ++$j ) { |
| 205 |
$x .= str_pad( base_convert( @$base32charsFlipped[ @$secret[ $i + $j ] ], 10, 2 ), 5, '0', STR_PAD_LEFT ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- vendored PHPGangsta TOTP library. |
| 206 |
} |
| 207 |
$eightBits = str_split( $x, 8 ); |
| 208 |
for ( $z = 0; $z < count( $eightBits ); ++$z ) { |
| 209 |
$binaryString .= ( ( $y = chr( base_convert( $eightBits[ $z ], 2, 10 ) ) ) || ord( $y ) == 48 ) ? $y : ''; // phpcs:ignore WordPress.CodeAnalysis.AssignmentInTernaryCondition.FoundInTernaryCondition -- vendored library. |
| 210 |
} |
| 211 |
} |
| 212 |
|
| 213 |
return $binaryString; |
| 214 |
} |
| 215 |
|
| 216 |
/** |
| 217 |
* Get array with all 32 characters for decoding from/encoding to base32. |
| 218 |
* |
| 219 |
* @return array |
| 220 |
*/ |
| 221 |
protected function _getBase32LookupTable() { |
| 222 |
return array( |
| 223 |
'A', |
| 224 |
'B', |
| 225 |
'C', |
| 226 |
'D', |
| 227 |
'E', |
| 228 |
'F', |
| 229 |
'G', |
| 230 |
'H', // 7 |
| 231 |
'I', |
| 232 |
'J', |
| 233 |
'K', |
| 234 |
'L', |
| 235 |
'M', |
| 236 |
'N', |
| 237 |
'O', |
| 238 |
'P', // 15 |
| 239 |
'Q', |
| 240 |
'R', |
| 241 |
'S', |
| 242 |
'T', |
| 243 |
'U', |
| 244 |
'V', |
| 245 |
'W', |
| 246 |
'X', // 23 |
| 247 |
'Y', |
| 248 |
'Z', |
| 249 |
'2', |
| 250 |
'3', |
| 251 |
'4', |
| 252 |
'5', |
| 253 |
'6', |
| 254 |
'7', // 31 |
| 255 |
'=', // padding char |
| 256 |
); |
| 257 |
} |
| 258 |
|
| 259 |
/** |
| 260 |
* A timing safe equals comparison |
| 261 |
* more info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html. |
| 262 |
* |
| 263 |
* @param string $safeString The internal (safe) value to be checked |
| 264 |
* @param string $userString The user submitted (unsafe) value |
| 265 |
* |
| 266 |
* @return bool True if the two strings are identical |
| 267 |
*/ |
| 268 |
private function timingSafeEquals( $safeString, $userString ) { |
| 269 |
if ( function_exists( 'hash_equals' ) ) { |
| 270 |
return hash_equals( $safeString, $userString ); |
| 271 |
} |
| 272 |
$safeLen = strlen( $safeString ); |
| 273 |
$userLen = strlen( $userString ); |
| 274 |
|
| 275 |
if ( $userLen != $safeLen ) { |
| 276 |
return false; |
| 277 |
} |
| 278 |
|
| 279 |
$result = 0; |
| 280 |
|
| 281 |
for ( $i = 0; $i < $userLen; ++$i ) { |
| 282 |
$result |= ( ord( $safeString[ $i ] ) ^ ord( $userString[ $i ] ) ); |
| 283 |
} |
| 284 |
|
| 285 |
// They are only identical strings if $result is exactly 0... |
| 286 |
return $result === 0; |
| 287 |
} |
| 288 |
} |
| 289 |
|