albertlast /
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; |
||
|
0 ignored issues
–
show
|
|||
| 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
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. 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; |
||
|
0 ignored issues
–
show
It seems like
$seconds can also be of type double or string. However, the property $refreshSeconds is declared as type integer. Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
Loading history...
|
|||
| 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
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. Loading history...
|
|||
| 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 | ?> |
||
|
0 ignored issues
–
show
It is not recommended to use PHP's closing tag
?> in files other than templates.
Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore. A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever. Loading history...
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountIdthat can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theidproperty of an instance of theAccountclass. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.