summaryrefslogtreecommitdiff
path: root/vendor/spomky-labs/otphp/src/OTP.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/spomky-labs/otphp/src/OTP.php')
-rw-r--r--vendor/spomky-labs/otphp/src/OTP.php120
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);
}
}