Yoshi2889 /
SMF2.1
| 1 | <?php |
||
| 2 | |||
| 3 | namespace TOTP; |
||
| 4 | |||
| 5 | /** |
||
| 6 | * A class for generating the codes compatible with the Google Authenticator and similar TOTP |
||
| 7 | * clients. |
||
| 8 | * |
||
| 9 | * NOTE: A lot of the logic from this class has been borrowed from this class: |
||
| 10 | * https://www.idontplaydarts.com/wp-content/uploads/2011/07/ga.php_.txt |
||
| 11 | * |
||
| 12 | * @author Chris Cornutt <[email protected]> |
||
| 13 | * @package GAuth |
||
| 14 | * @license MIT |
||
| 15 | * |
||
| 16 | * Simple Machines Forum (SMF) |
||
| 17 | * |
||
| 18 | * @package SMF |
||
| 19 | * @author Simple Machines http://www.simplemachines.org |
||
| 20 | * @copyright 2019 Simple Machines and individual contributors |
||
| 21 | * @license http://www.simplemachines.org/about/smf/license.php BSD |
||
| 22 | * |
||
| 23 | * @version 2.1 RC2 |
||
| 24 | */ |
||
| 25 | |||
| 26 | /** |
||
| 27 | * Class Auth |
||
| 28 | * |
||
| 29 | * @package TOTP |
||
| 30 | */ |
||
| 31 | class Auth |
||
| 32 | { |
||
| 33 | /** |
||
| 34 | * @var array Internal lookup table |
||
| 35 | */ |
||
| 36 | private $lookup = array(); |
||
| 37 | |||
| 38 | /** |
||
| 39 | * @var string Initialization key |
||
| 40 | */ |
||
| 41 | private $initKey = null; |
||
| 42 | |||
| 43 | /** |
||
| 44 | * @var integer Seconds between key refreshes |
||
| 45 | */ |
||
| 46 | private $refreshSeconds = 30; |
||
| 47 | |||
| 48 | /** |
||
| 49 | * @var integer The length of codes to generate |
||
| 50 | */ |
||
| 51 | private $codeLength = 6; |
||
| 52 | |||
| 53 | /** |
||
| 54 | * @var integer Range plus/minus for "window of opportunity" on allowed codes |
||
| 55 | */ |
||
| 56 | private $range = 2; |
||
| 57 | |||
| 58 | /** |
||
| 59 | * Initialize the object and set up the lookup table |
||
| 60 | * Optionally the Initialization key |
||
| 61 | * |
||
| 62 | * @param string $initKey Initialization key |
||
| 63 | */ |
||
| 64 | public function __construct($initKey = null) |
||
| 65 | { |
||
| 66 | $this->buildLookup(); |
||
| 67 | |||
| 68 | if ($initKey !== null) |
||
| 69 | { |
||
| 70 | $this->setInitKey($initKey); |
||
| 71 | } |
||
| 72 | } |
||
| 73 | |||
| 74 | /** |
||
| 75 | * Build the base32 lookup table |
||
| 76 | */ |
||
| 77 | public function buildLookup() |
||
| 78 | { |
||
| 79 | $lookup = array_combine( |
||
| 80 | array_merge(range('A', 'Z'), range(2, 7)), |
||
| 81 | range(0, 31) |
||
| 82 | ); |
||
| 83 | $this->setLookup($lookup); |
||
| 84 | } |
||
| 85 | |||
| 86 | /** |
||
| 87 | * Get the current "range" value |
||
| 88 | * |
||
| 89 | * @return integer Range value |
||
| 90 | */ |
||
| 91 | public function getRange() |
||
| 92 | { |
||
| 93 | return $this->range; |
||
| 94 | } |
||
| 95 | |||
| 96 | /** |
||
| 97 | * Set the "range" value |
||
| 98 | * |
||
| 99 | * @param integer $range Range value |
||
| 100 | * @return \TOTP\Auth instance |
||
| 101 | */ |
||
| 102 | public function setRange($range) |
||
| 103 | { |
||
| 104 | if (!is_numeric($range)) |
||
| 105 | { |
||
| 106 | throw new \InvalidArgumentException('Invalid window range'); |
||
| 107 | } |
||
| 108 | $this->range = $range; |
||
| 109 | return $this; |
||
| 110 | } |
||
| 111 | |||
| 112 | /** |
||
| 113 | * Set the initialization key for the object |
||
| 114 | * |
||
| 115 | * @param string $key Initialization key |
||
| 116 | * @throws \InvalidArgumentException If hash is not valid base32 |
||
| 117 | * @return \TOTP\Auth instance |
||
| 118 | */ |
||
| 119 | public function setInitKey($key) |
||
| 120 | { |
||
| 121 | if (preg_match('/^[' . implode('', array_keys($this->getLookup())) . ']+$/', $key) == false) |
||
|
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
| 122 | { |
||
| 123 | throw new \InvalidArgumentException('Invalid base32 hash!'); |
||
| 124 | } |
||
| 125 | $this->initKey = $key; |
||
| 126 | return $this; |
||
| 127 | } |
||
| 128 | |||
| 129 | /** |
||
| 130 | * Get the current Initialization key |
||
| 131 | * |
||
| 132 | * @return string Initialization key |
||
| 133 | */ |
||
| 134 | public function getInitKey() |
||
| 135 | { |
||
| 136 | return $this->initKey; |
||
| 137 | } |
||
| 138 | |||
| 139 | /** |
||
| 140 | * Set the contents of the internal lookup table |
||
| 141 | * |
||
| 142 | * @param array $lookup Lookup data set |
||
| 143 | * @throws \InvalidArgumentException If lookup given is not an array |
||
| 144 | * @return \TOTP\Auth instance |
||
| 145 | */ |
||
| 146 | public function setLookup($lookup) |
||
| 147 | { |
||
| 148 | if (!is_array($lookup)) |
||
| 149 | { |
||
| 150 | throw new \InvalidArgumentException('Lookup value must be an array'); |
||
| 151 | } |
||
| 152 | $this->lookup = $lookup; |
||
| 153 | return $this; |
||
| 154 | } |
||
| 155 | |||
| 156 | /** |
||
| 157 | * Get the current lookup data set |
||
| 158 | * |
||
| 159 | * @return array Lookup data |
||
| 160 | */ |
||
| 161 | public function getLookup() |
||
| 162 | { |
||
| 163 | return $this->lookup; |
||
| 164 | } |
||
| 165 | |||
| 166 | /** |
||
| 167 | * Get the number of seconds for code refresh currently set |
||
| 168 | * |
||
| 169 | * @return integer Refresh in seconds |
||
| 170 | */ |
||
| 171 | public function getRefresh() |
||
| 172 | { |
||
| 173 | return $this->refreshSeconds; |
||
| 174 | } |
||
| 175 | |||
| 176 | /** |
||
| 177 | * Set the number of seconds to refresh codes |
||
| 178 | * |
||
| 179 | * @param integer $seconds Seconds to refresh |
||
| 180 | * @throws \InvalidArgumentException If seconds value is not numeric |
||
| 181 | * @return \TOTP\Auth instance |
||
| 182 | */ |
||
| 183 | public function setRefresh($seconds) |
||
| 184 | { |
||
| 185 | if (!is_numeric($seconds)) |
||
| 186 | { |
||
| 187 | throw new \InvalidArgumentException('Seconds must be numeric'); |
||
| 188 | } |
||
| 189 | $this->refreshSeconds = $seconds; |
||
| 190 | return $this; |
||
| 191 | } |
||
| 192 | |||
| 193 | /** |
||
| 194 | * Get the current length for generated codes |
||
| 195 | * |
||
| 196 | * @return integer Code length |
||
| 197 | */ |
||
| 198 | public function getCodeLength() |
||
| 199 | { |
||
| 200 | return $this->codeLength; |
||
| 201 | } |
||
| 202 | |||
| 203 | /** |
||
| 204 | * Set the length of the generated codes |
||
| 205 | * |
||
| 206 | * @param integer $length Code length |
||
| 207 | * @return \TOTP\Auth instance |
||
| 208 | */ |
||
| 209 | public function setCodeLength($length) |
||
| 210 | { |
||
| 211 | $this->codeLength = $length; |
||
| 212 | return $this; |
||
| 213 | } |
||
| 214 | |||
| 215 | /** |
||
| 216 | * Validate the given code |
||
| 217 | * |
||
| 218 | * @param string $code Code entered by user |
||
| 219 | * @param string $initKey Initialization key |
||
| 220 | * @param string $timestamp Timestamp for calculation |
||
| 221 | * @param integer $range Seconds before/after to validate hash against |
||
| 222 | * @throws \InvalidArgumentException If incorrect code length |
||
| 223 | * @return boolean Pass/fail of validation |
||
| 224 | */ |
||
| 225 | public function validateCode($code, $initKey = null, $timestamp = null, $range = null) |
||
| 226 | { |
||
| 227 | if (strlen($code) !== $this->getCodeLength()) |
||
| 228 | { |
||
| 229 | throw new \InvalidArgumentException('Incorrect code length'); |
||
| 230 | } |
||
| 231 | |||
| 232 | $range = ($range == null) ? $this->getRange() : $range; |
||
|
0 ignored issues
–
show
|
|||
| 233 | $timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; |
||
|
0 ignored issues
–
show
|
|||
| 234 | $initKey = ($initKey == null) ? $this->getInitKey() : $initKey; |
||
|
0 ignored issues
–
show
|
|||
| 235 | |||
| 236 | $binary = $this->base32_decode($initKey); |
||
| 237 | |||
| 238 | for ($time = ($timestamp - $range); $time <= ($timestamp + $range); $time++) |
||
| 239 | { |
||
| 240 | if ($this->generateOneTime($binary, $time) == $code) |
||
| 241 | { |
||
| 242 | return true; |
||
| 243 | } |
||
| 244 | } |
||
| 245 | return false; |
||
| 246 | } |
||
| 247 | |||
| 248 | /** |
||
| 249 | * Generate a one-time code |
||
| 250 | * |
||
| 251 | * @param string $initKey Initialization key [optional] |
||
| 252 | * @param string $timestamp Timestamp for calculation [optional] |
||
| 253 | * @return string Generated code/hash |
||
| 254 | */ |
||
| 255 | public function generateOneTime($initKey = null, $timestamp = null) |
||
| 256 | { |
||
| 257 | $initKey = ($initKey == null) ? $this->getInitKey() : $initKey; |
||
|
0 ignored issues
–
show
|
|||
| 258 | $timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; |
||
|
0 ignored issues
–
show
|
|||
| 259 | |||
| 260 | $hash = hash_hmac( |
||
| 261 | 'sha1', |
||
| 262 | pack('N*', 0) . pack('N*', $timestamp), |
||
| 263 | $initKey, |
||
| 264 | true |
||
| 265 | ); |
||
| 266 | |||
| 267 | return str_pad($this->truncateHash($hash), $this->getCodeLength(), '0', STR_PAD_LEFT); |
||
| 268 | } |
||
| 269 | |||
| 270 | /** |
||
| 271 | * Generate a code/hash |
||
| 272 | * Useful for making Initialization codes |
||
| 273 | * |
||
| 274 | * @param integer $length Length for the generated code |
||
| 275 | * @return string Generated code |
||
| 276 | */ |
||
| 277 | public function generateCode($length = 16) |
||
| 278 | { |
||
| 279 | global $smcFunc; |
||
| 280 | |||
| 281 | $lookup = implode('', array_keys($this->getLookup())); |
||
| 282 | $code = ''; |
||
| 283 | |||
| 284 | for ($i = 0; $i < $length; $i++) |
||
| 285 | { |
||
| 286 | $code .= $lookup[$smcFunc['random_int'](0, strlen($lookup) - 1)]; |
||
| 287 | } |
||
| 288 | |||
| 289 | return $code; |
||
| 290 | } |
||
| 291 | |||
| 292 | /** |
||
| 293 | * Generate the timestamp for the calculation |
||
| 294 | * |
||
| 295 | * @return integer Timestamp |
||
| 296 | */ |
||
| 297 | public function generateTimestamp() |
||
| 298 | { |
||
| 299 | return floor(microtime(true) / $this->getRefresh()); |
||
| 300 | } |
||
| 301 | |||
| 302 | /** |
||
| 303 | * Truncate the given hash down to just what we need |
||
| 304 | * |
||
| 305 | * @param string $hash Hash to truncate |
||
| 306 | * @return string Truncated hash value |
||
| 307 | */ |
||
| 308 | public function truncateHash($hash) |
||
| 309 | { |
||
| 310 | $offset = ord($hash[19]) & 0xf; |
||
| 311 | |||
| 312 | return ( |
||
| 313 | ((ord($hash[$offset + 0]) & 0x7f) << 24) | |
||
| 314 | ((ord($hash[$offset + 1]) & 0xff) << 16) | |
||
| 315 | ((ord($hash[$offset + 2]) & 0xff) << 8) | |
||
| 316 | (ord($hash[$offset + 3]) & 0xff) |
||
| 317 | ) % pow(10, $this->getCodeLength()); |
||
| 318 | } |
||
| 319 | |||
| 320 | /** |
||
| 321 | * Base32 decoding function |
||
| 322 | * |
||
| 323 | * @param string $hash The base32-encoded hash |
||
| 324 | * @throws \InvalidArgumentException When hash is not valid |
||
| 325 | * @return string Binary value of hash |
||
| 326 | */ |
||
| 327 | public function base32_decode($hash) |
||
| 328 | { |
||
| 329 | $lookup = $this->getLookup(); |
||
| 330 | |||
| 331 | if (preg_match('/^[' . implode('', array_keys($lookup)) . ']+$/', $hash) == false) |
||
|
0 ignored issues
–
show
|
|||
| 332 | { |
||
| 333 | throw new \InvalidArgumentException('Invalid base32 hash!'); |
||
| 334 | } |
||
| 335 | |||
| 336 | $hash = strtoupper($hash); |
||
| 337 | $buffer = 0; |
||
| 338 | $length = 0; |
||
| 339 | $binary = ''; |
||
| 340 | |||
| 341 | for ($i = 0; $i < strlen($hash); $i++) |
||
| 342 | { |
||
| 343 | $buffer = $buffer << 5; |
||
| 344 | $buffer += $lookup[$hash[$i]]; |
||
| 345 | $length += 5; |
||
| 346 | |||
| 347 | if ($length >= 8) |
||
| 348 | { |
||
| 349 | $length -= 8; |
||
| 350 | $binary .= chr(($buffer & (0xFF << $length)) >> $length); |
||
| 351 | } |
||
| 352 | } |
||
| 353 | |||
| 354 | return $binary; |
||
| 355 | } |
||
| 356 | |||
| 357 | /** |
||
| 358 | * Returns a URL to QR code for embedding the QR code |
||
| 359 | * |
||
| 360 | * @param string $name The name |
||
| 361 | * @param string $code The generated code |
||
| 362 | * @return string The URL to the QR code |
||
| 363 | */ |
||
| 364 | public function getQrCodeUrl($name, $code) |
||
| 365 | { |
||
| 366 | $url = 'otpauth://totp/' . urlencode($name) . '?secret=' . $code; |
||
| 367 | return $url; |
||
| 368 | } |
||
| 369 | } |
||
| 370 | |||
| 371 | ?> |