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