diff options
Diffstat (limited to 'vendor/spomky-labs/otphp/src/OTP.php')
| -rw-r--r-- | vendor/spomky-labs/otphp/src/OTP.php | 120 |
1 files changed, 78 insertions, 42 deletions
diff --git a/vendor/spomky-labs/otphp/src/OTP.php b/vendor/spomky-labs/otphp/src/OTP.php index 932bcf97e..f4c242c8f 100644 --- a/vendor/spomky-labs/otphp/src/OTP.php +++ b/vendor/spomky-labs/otphp/src/OTP.php @@ -2,32 +2,30 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; -use Assert\Assertion; +use Exception; +use InvalidArgumentException; use ParagonIE\ConstantTime\Base32; use RuntimeException; -use function Safe\ksort; -use function Safe\sprintf; +use function assert; +use function chr; +use function count; +use function is_string; +use const STR_PAD_LEFT; abstract class OTP implements OTPInterface { use ParameterTrait; - protected function __construct(?string $secret, string $digest, int $digits) + private const DEFAULT_SECRET_SIZE = 64; + + /** + * @param non-empty-string $secret + */ + protected function __construct(string $secret) { $this->setSecret($secret); - $this->setDigest($digest); - $this->setDigits($digits); } public function getQrCodeUri(string $uri, string $placeholder): string @@ -38,32 +36,52 @@ abstract class OTP implements OTPInterface } /** + * @param 0|positive-int $input + */ + public function at(int $input): string + { + return $this->generateOTP($input); + } + + /** + * @return non-empty-string + */ + final protected static function generateSecret(): string + { + return Base32::encodeUpper(random_bytes(self::DEFAULT_SECRET_SIZE)); + } + + /** * The OTP at the specified input. + * + * @param 0|positive-int $input + * + * @return non-empty-string */ protected function generateOTP(int $input): string { $hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true); + $unpacked = unpack('C*', $hash); + $unpacked !== false || throw new InvalidArgumentException('Invalid data.'); + $hmac = array_values($unpacked); - $hmac = array_values(unpack('C*', $hash)); - - $offset = ($hmac[\count($hmac) - 1] & 0xF); - $code = ($hmac[$offset + 0] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF); + $offset = ($hmac[count($hmac) - 1] & 0xF); + $code = ($hmac[$offset] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF); $otp = $code % (10 ** $this->getDigits()); return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT); } - public function at(int $timestamp): string - { - return $this->generateOTP($timestamp); - } - /** - * @param array<string, mixed> $options + * @param array<non-empty-string, mixed> $options */ protected function filterOptions(array &$options): void { - foreach (['algorithm' => 'sha1', 'period' => 30, 'digits' => 6] as $key => $default) { + foreach ([ + 'algorithm' => 'sha1', + 'period' => 30, + 'digits' => 6, + ] as $key => $default) { if (isset($options[$key]) && $default === $options[$key]) { unset($options[$key]); } @@ -73,42 +91,60 @@ abstract class OTP implements OTPInterface } /** - * @param array<string, mixed> $options + * @param non-empty-string $type + * @param array<non-empty-string, mixed> $options + * + * @return non-empty-string */ protected function generateURI(string $type, array $options): string { $label = $this->getLabel(); - Assertion::string($label, 'The label is not set.'); - Assertion::false($this->hasColon($label), 'Label must not contain a colon.'); - $options = array_merge($options, $this->getParameters()); + is_string($label) || throw new InvalidArgumentException('The label is not set.'); + $this->hasColon($label) === false || throw new InvalidArgumentException('Label must not contain a colon.'); + $options = [...$options, ...$this->getParameters()]; $this->filterOptions($options); - $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options)); + $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&')); + + return sprintf( + 'otpauth://%s/%s?%s', + $type, + rawurlencode(($this->getIssuer() !== null ? $this->getIssuer() . ':' : '') . $label), + $params + ); + } - return sprintf('otpauth://%s/%s?%s', $type, rawurlencode((null !== $this->getIssuer() ? $this->getIssuer().':' : '').$label), $params); + /** + * @param non-empty-string $safe + * @param non-empty-string $user + */ + protected function compareOTP(string $safe, string $user): bool + { + return hash_equals($safe, $user); } + /** + * @return non-empty-string + */ private function getDecodedSecret(): string { try { - return Base32::decodeUpper($this->getSecret()); - } catch (\Exception $e) { + $decoded = Base32::decodeUpper($this->getSecret()); + } catch (Exception) { throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?'); } + assert($decoded !== ''); + + return $decoded; } private function intToByteString(int $int): string { $result = []; - while (0 !== $int) { - $result[] = \chr($int & 0xFF); + while ($int !== 0) { + $result[] = chr($int & 0xFF); $int >>= 8; } - return str_pad(implode(array_reverse($result)), 8, "\000", STR_PAD_LEFT); - } - - protected function compareOTP(string $safe, string $user): bool - { - return hash_equals($safe, $user); + return str_pad(implode('', array_reverse($result)), 8, "\000", STR_PAD_LEFT); } } |