summaryrefslogtreecommitdiff
path: root/vendor/spomky-labs/otphp/src
diff options
context:
space:
mode:
authorwn_ <invalid@email.com>2024-12-09 17:58:28 +0000
committerwn_ <invalid@email.com>2024-12-09 17:58:28 +0000
commitf6a8facfd4bfc40025c069eebc37094d826aff58 (patch)
tree871aab0d8adafe736d954cae1783c260699c0ec3 /vendor/spomky-labs/otphp/src
parentcd2c10f9f71409df24fc74c1bbd7d5ddbf48d991 (diff)
Bump 'spomky-labs/otphp' to 11.3.x.
This is mainly for PHP 8.4 compatibility.
Diffstat (limited to 'vendor/spomky-labs/otphp/src')
-rw-r--r--vendor/spomky-labs/otphp/src/Factory.php97
-rw-r--r--vendor/spomky-labs/otphp/src/FactoryInterface.php15
-rw-r--r--vendor/spomky-labs/otphp/src/HOTP.php128
-rw-r--r--vendor/spomky-labs/otphp/src/HOTPInterface.php29
-rw-r--r--vendor/spomky-labs/otphp/src/InternalClock.php19
-rw-r--r--vendor/spomky-labs/otphp/src/OTP.php120
-rw-r--r--vendor/spomky-labs/otphp/src/OTPInterface.php91
-rw-r--r--vendor/spomky-labs/otphp/src/ParameterTrait.php132
-rw-r--r--vendor/spomky-labs/otphp/src/TOTP.php218
-rw-r--r--vendor/spomky-labs/otphp/src/TOTPInterface.php35
-rw-r--r--vendor/spomky-labs/otphp/src/Url.php102
11 files changed, 638 insertions, 348 deletions
diff --git a/vendor/spomky-labs/otphp/src/Factory.php b/vendor/spomky-labs/otphp/src/Factory.php
index 70df63945..4bf41a84a 100644
--- a/vendor/spomky-labs/otphp/src/Factory.php
+++ b/vendor/spomky-labs/otphp/src/Factory.php
@@ -2,114 +2,103 @@
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 InvalidArgumentException;
-use function Safe\parse_url;
-use function Safe\sprintf;
+use Psr\Clock\ClockInterface;
use Throwable;
+use function assert;
+use function count;
/**
* This class is used to load OTP object from a provisioning Uri.
+ *
+ * @see \OTPHP\Test\FactoryTest
*/
final class Factory implements FactoryInterface
{
- public static function loadFromProvisioningUri(string $uri): OTPInterface
+ public static function loadFromProvisioningUri(string $uri, ?ClockInterface $clock = null): OTPInterface
{
try {
- $parsed_url = parse_url($uri);
+ $parsed_url = Url::fromString($uri);
+ $parsed_url->getScheme() === 'otpauth' || throw new InvalidArgumentException('Invalid scheme.');
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Not a valid OTP provisioning URI', $throwable->getCode(), $throwable);
}
- Assertion::isArray($parsed_url, 'Not a valid OTP provisioning URI');
- self::checkData($parsed_url);
+ if ($clock === null) {
+ trigger_deprecation(
+ 'spomky-labs/otphp',
+ '11.3.0',
+ 'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".'
+ );
+ $clock = new InternalClock();
+ }
- $otp = self::createOTP($parsed_url);
+ $otp = self::createOTP($parsed_url, $clock);
self::populateOTP($otp, $parsed_url);
return $otp;
}
- /**
- * @param array<string, mixed> $data
- */
- private static function populateParameters(OTPInterface &$otp, array $data): void
+ private static function populateParameters(OTPInterface $otp, Url $data): void
{
- foreach ($data['query'] as $key => $value) {
+ foreach ($data->getQuery() as $key => $value) {
$otp->setParameter($key, $value);
}
}
- /**
- * @param array<string, mixed> $data
- */
- private static function populateOTP(OTPInterface &$otp, array $data): void
+ private static function populateOTP(OTPInterface $otp, Url $data): void
{
self::populateParameters($otp, $data);
- $result = explode(':', rawurldecode(mb_substr($data['path'], 1)));
+ $result = explode(':', rawurldecode(mb_substr($data->getPath(), 1)));
- if (2 > \count($result)) {
+ if (count($result) < 2) {
$otp->setIssuerIncludedAsParameter(false);
return;
}
- if (null !== $otp->getIssuer()) {
- Assertion::eq($result[0], $otp->getIssuer(), 'Invalid OTP: invalid issuer in parameter');
+ if ($otp->getIssuer() !== null) {
+ $result[0] === $otp->getIssuer() || throw new InvalidArgumentException(
+ 'Invalid OTP: invalid issuer in parameter'
+ );
$otp->setIssuerIncludedAsParameter(true);
}
- $otp->setIssuer($result[0]);
- }
- /**
- * @param array<string, mixed> $data
- */
- private static function checkData(array &$data): void
- {
- foreach (['scheme', 'host', 'path', 'query'] as $key) {
- Assertion::keyExists($data, $key, 'Not a valid OTP provisioning URI');
- }
- Assertion::eq('otpauth', $data['scheme'], 'Not a valid OTP provisioning URI');
- parse_str($data['query'], $data['query']);
- Assertion::keyExists($data['query'], 'secret', 'Not a valid OTP provisioning URI');
+ assert($result[0] !== '');
+
+ $otp->setIssuer($result[0]);
}
- /**
- * @param array<string, mixed> $parsed_url
- */
- private static function createOTP(array $parsed_url): OTPInterface
+ private static function createOTP(Url $parsed_url, ClockInterface $clock): OTPInterface
{
- switch ($parsed_url['host']) {
+ switch ($parsed_url->getHost()) {
case 'totp':
- $totp = TOTP::create($parsed_url['query']['secret']);
- $totp->setLabel(self::getLabel($parsed_url['path']));
+ $totp = TOTP::createFromSecret($parsed_url->getSecret(), $clock);
+ $totp->setLabel(self::getLabel($parsed_url->getPath()));
return $totp;
case 'hotp':
- $hotp = HOTP::create($parsed_url['query']['secret']);
- $hotp->setLabel(self::getLabel($parsed_url['path']));
+ $hotp = HOTP::createFromSecret($parsed_url->getSecret());
+ $hotp->setLabel(self::getLabel($parsed_url->getPath()));
return $hotp;
default:
- throw new InvalidArgumentException(sprintf('Unsupported "%s" OTP type', $parsed_url['host']));
+ throw new InvalidArgumentException(sprintf('Unsupported "%s" OTP type', $parsed_url->getHost()));
}
}
+ /**
+ * @param non-empty-string $data
+ * @return non-empty-string
+ */
private static function getLabel(string $data): string
{
$result = explode(':', rawurldecode(mb_substr($data, 1)));
+ $label = count($result) === 2 ? $result[1] : $result[0];
+ assert($label !== '');
- return 2 === \count($result) ? $result[1] : $result[0];
+ return $label;
}
}
diff --git a/vendor/spomky-labs/otphp/src/FactoryInterface.php b/vendor/spomky-labs/otphp/src/FactoryInterface.php
index 00acc2d04..dd14e45f9 100644
--- a/vendor/spomky-labs/otphp/src/FactoryInterface.php
+++ b/vendor/spomky-labs/otphp/src/FactoryInterface.php
@@ -2,22 +2,15 @@
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;
interface FactoryInterface
{
/**
- * This method is the unique public method of the class.
- * It can load a provisioning Uri and convert it into an OTP object.
+ * This method is the unique public method of the class. It can load a provisioning Uri and convert it into an OTP
+ * object.
+ *
+ * @param non-empty-string $uri
*/
public static function loadFromProvisioningUri(string $uri): OTPInterface;
}
diff --git a/vendor/spomky-labs/otphp/src/HOTP.php b/vendor/spomky-labs/otphp/src/HOTP.php
index a2f4a2395..835de35f3 100644
--- a/vendor/spomky-labs/otphp/src/HOTP.php
+++ b/vendor/spomky-labs/otphp/src/HOTP.php
@@ -2,60 +2,78 @@
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 InvalidArgumentException;
+use function is_int;
+/**
+ * @see \OTPHP\Test\HOTPTest
+ */
final class HOTP extends OTP implements HOTPInterface
{
- protected function __construct(?string $secret, int $counter, string $digest, int $digits)
- {
- parent::__construct($secret, $digest, $digits);
- $this->setCounter($counter);
+ private const DEFAULT_WINDOW = 0;
+
+ public static function create(
+ null|string $secret = null,
+ int $counter = self::DEFAULT_COUNTER,
+ string $digest = self::DEFAULT_DIGEST,
+ int $digits = self::DEFAULT_DIGITS
+ ): self {
+ $htop = $secret !== null
+ ? self::createFromSecret($secret)
+ : self::generate()
+ ;
+ $htop->setCounter($counter);
+ $htop->setDigest($digest);
+ $htop->setDigits($digits);
+
+ return $htop;
}
- public static function create(?string $secret = null, int $counter = 0, string $digest = 'sha1', int $digits = 6): HOTPInterface
+ public static function createFromSecret(string $secret): self
{
- return new self($secret, $counter, $digest, $digits);
+ $htop = new self($secret);
+ $htop->setCounter(self::DEFAULT_COUNTER);
+ $htop->setDigest(self::DEFAULT_DIGEST);
+ $htop->setDigits(self::DEFAULT_DIGITS);
+
+ return $htop;
}
- protected function setCounter(int $counter): void
+ public static function generate(): self
{
- $this->setParameter('counter', $counter);
+ return self::createFromSecret(self::generateSecret());
}
+ /**
+ * @return 0|positive-int
+ */
public function getCounter(): int
{
- return $this->getParameter('counter');
- }
+ $value = $this->getParameter('counter');
+ (is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "counter" parameter.');
- private function updateCounter(int $counter): void
- {
- $this->setCounter($counter);
+ return $value;
}
public function getProvisioningUri(): string
{
- return $this->generateURI('hotp', ['counter' => $this->getCounter()]);
+ return $this->generateURI('hotp', [
+ 'counter' => $this->getCounter(),
+ ]);
}
/**
* If the counter is not provided, the OTP is verified at the actual counter.
+ *
+ * @param null|0|positive-int $counter
*/
- public function verify(string $otp, ?int $counter = null, ?int $window = null): bool
+ public function verify(string $otp, null|int $counter = null, null|int $window = null): bool
{
- Assertion::greaterOrEqualThan($counter, 0, 'The counter must be at least 0.');
+ $counter >= 0 || throw new InvalidArgumentException('The counter must be at least 0.');
- if (null === $counter) {
+ if ($counter === null) {
$counter = $this->getCounter();
} elseif ($counter < $this->getCounter()) {
return false;
@@ -64,12 +82,45 @@ final class HOTP extends OTP implements HOTPInterface
return $this->verifyOtpWithWindow($otp, $counter, $window);
}
- private function getWindow(?int $window): int
+ public function setCounter(int $counter): void
+ {
+ $this->setParameter('counter', $counter);
+ }
+
+ /**
+ * @return array<non-empty-string, callable>
+ */
+ protected function getParameterMap(): array
+ {
+ return [...parent::getParameterMap(), ...[
+ 'counter' => static function (mixed $value): int {
+ $value = (int) $value;
+ $value >= 0 || throw new InvalidArgumentException('Counter must be at least 0.');
+
+ return $value;
+ },
+ ]];
+ }
+
+ private function updateCounter(int $counter): void
+ {
+ $this->setCounter($counter);
+ }
+
+ /**
+ * @param null|0|positive-int $window
+ */
+ private function getWindow(null|int $window): int
{
- return abs($window ?? 0);
+ return abs($window ?? self::DEFAULT_WINDOW);
}
- private function verifyOtpWithWindow(string $otp, int $counter, ?int $window): bool
+ /**
+ * @param non-empty-string $otp
+ * @param 0|positive-int $counter
+ * @param null|0|positive-int $window
+ */
+ private function verifyOtpWithWindow(string $otp, int $counter, null|int $window): bool
{
$window = $this->getWindow($window);
@@ -83,21 +134,4 @@ final class HOTP extends OTP implements HOTPInterface
return false;
}
-
- /**
- * @return array<string, mixed>
- */
- protected function getParameterMap(): array
- {
- $v = array_merge(
- parent::getParameterMap(),
- ['counter' => function ($value): int {
- Assertion::greaterOrEqualThan((int) $value, 0, 'Counter must be at least 0.');
-
- return (int) $value;
- }]
- );
-
- return $v;
- }
}
diff --git a/vendor/spomky-labs/otphp/src/HOTPInterface.php b/vendor/spomky-labs/otphp/src/HOTPInterface.php
index 336ce1055..915569a03 100644
--- a/vendor/spomky-labs/otphp/src/HOTPInterface.php
+++ b/vendor/spomky-labs/otphp/src/HOTPInterface.php
@@ -2,28 +2,35 @@
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;
interface HOTPInterface extends OTPInterface
{
+ public const DEFAULT_COUNTER = 0;
+
/**
* The initial counter (a positive integer).
*/
public function getCounter(): int;
/**
- * Create a new TOTP object.
+ * Create a new HOTP object.
*
* If the secret is null, a random 64 bytes secret will be generated.
+ *
+ * @param null|non-empty-string $secret
+ * @param 0|positive-int $counter
+ * @param non-empty-string $digest
+ * @param positive-int $digits
+ *
+ * @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
*/
- public static function create(?string $secret = null, int $counter = 0, string $digest = 'sha1', int $digits = 6): self;
+ public static function create(
+ null|string $secret = null,
+ int $counter = 0,
+ string $digest = 'sha1',
+ int $digits = 6
+ ): self;
+
+ public function setCounter(int $counter): void;
}
diff --git a/vendor/spomky-labs/otphp/src/InternalClock.php b/vendor/spomky-labs/otphp/src/InternalClock.php
new file mode 100644
index 000000000..8be469318
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/InternalClock.php
@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OTPHP;
+
+use DateTimeImmutable;
+use Psr\Clock\ClockInterface;
+
+/**
+ * @internal
+ */
+final class InternalClock implements ClockInterface
+{
+ public function now(): DateTimeImmutable
+ {
+ return new DateTimeImmutable();
+ }
+}
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);
}
}
diff --git a/vendor/spomky-labs/otphp/src/OTPInterface.php b/vendor/spomky-labs/otphp/src/OTPInterface.php
index 66e163d5d..39ce4acd0 100644
--- a/vendor/spomky-labs/otphp/src/OTPInterface.php
+++ b/vendor/spomky-labs/otphp/src/OTPInterface.php
@@ -2,50 +2,80 @@
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;
interface OTPInterface
{
+ public const DEFAULT_DIGITS = 6;
+
+ public const DEFAULT_DIGEST = 'sha1';
+
/**
- * @return string Return the OTP at the specified timestamp
+ * Create a OTP object from an existing secret.
+ *
+ * @param non-empty-string $secret
*/
- public function at(int $timestamp): string;
+ public static function createFromSecret(string $secret): self;
/**
- * Verify that the OTP is valid with the specified input.
- * If no input is provided, the input is set to a default value or false is returned.
+ * Create a new OTP object. A random 64 bytes secret will be generated.
*/
- public function verify(string $otp, ?int $input = null, ?int $window = null): bool;
+ public static function generate(): self;
/**
- * @return string The secret of the OTP
+ * @param non-empty-string $secret
+ */
+ public function setSecret(string $secret): void;
+
+ public function setDigits(int $digits): void;
+
+ /**
+ * @param non-empty-string $digest
+ */
+ public function setDigest(string $digest): void;
+
+ /**
+ * Generate the OTP at the specified input.
+ *
+ * @param 0|positive-int $input
+ *
+ * @return non-empty-string Return the OTP at the specified timestamp
+ */
+ public function at(int $input): string;
+
+ /**
+ * Verify that the OTP is valid with the specified input. If no input is provided, the input is set to a default
+ * value or false is returned.
+ *
+ * @param non-empty-string $otp
+ * @param null|0|positive-int $input
+ * @param null|0|positive-int $window
+ */
+ public function verify(string $otp, null|int $input = null, null|int $window = null): bool;
+
+ /**
+ * @return non-empty-string The secret of the OTP
*/
public function getSecret(): string;
/**
- * @param string $label The label of the OTP
+ * @param non-empty-string $label The label of the OTP
*/
public function setLabel(string $label): void;
/**
- * @return string|null The label of the OTP
+ * @return non-empty-string|null The label of the OTP
*/
- public function getLabel(): ?string;
+ public function getLabel(): null|string;
/**
- * @return string|null The issuer
+ * @return non-empty-string|null The issuer
*/
public function getIssuer(): ?string;
+ /**
+ * @param non-empty-string $issuer
+ */
public function setIssuer(string $issuer): void;
/**
@@ -56,42 +86,47 @@ interface OTPInterface
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void;
/**
- * @return int Number of digits in the OTP
+ * @return positive-int Number of digits in the OTP
*/
public function getDigits(): int;
/**
- * @return string Digest algorithm used to calculate the OTP. Possible values are 'md5', 'sha1', 'sha256' and 'sha512'
+ * @return non-empty-string Digest algorithm used to calculate the OTP. Possible values are 'md5', 'sha1', 'sha256' and 'sha512'
*/
public function getDigest(): string;
/**
- * @return mixed|null
+ * @param non-empty-string $parameter
*/
- public function getParameter(string $parameter);
+ public function getParameter(string $parameter): mixed;
+ /**
+ * @param non-empty-string $parameter
+ */
public function hasParameter(string $parameter): bool;
/**
- * @return array<string, mixed>
+ * @return array<non-empty-string, mixed>
*/
public function getParameters(): array;
/**
- * @param mixed|null $value
+ * @param non-empty-string $parameter
*/
- public function setParameter(string $parameter, $value): void;
+ public function setParameter(string $parameter, mixed $value): void;
/**
* Get the provisioning URI.
+ *
+ * @return non-empty-string
*/
public function getProvisioningUri(): string;
/**
* Get the provisioning URI.
*
- * @param string $uri The Uri of the QRCode generator with all parameters. This Uri MUST contain a placeholder that will be replaced by the method.
- * @param string $placeholder the placeholder to be replaced in the QR Code generator URI
+ * @param non-empty-string $uri The Uri of the QRCode generator with all parameters. This Uri MUST contain a placeholder that will be replaced by the method.
+ * @param non-empty-string $placeholder the placeholder to be replaced in the QR Code generator URI
*/
public function getQrCodeUri(string $uri, string $placeholder): string;
}
diff --git a/vendor/spomky-labs/otphp/src/ParameterTrait.php b/vendor/spomky-labs/otphp/src/ParameterTrait.php
index 326109da3..dc92861c4 100644
--- a/vendor/spomky-labs/otphp/src/ParameterTrait.php
+++ b/vendor/spomky-labs/otphp/src/ParameterTrait.php
@@ -2,52 +2,42 @@
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 InvalidArgumentException;
-use ParagonIE\ConstantTime\Base32;
-use function Safe\sprintf;
+use function array_key_exists;
+use function assert;
+use function in_array;
+use function is_int;
+use function is_string;
trait ParameterTrait
{
/**
- * @var array<string, mixed>
+ * @var array<non-empty-string, mixed>
*/
- private $parameters = [];
+ private array $parameters = [];
/**
- * @var string|null
+ * @var non-empty-string|null
*/
- private $issuer;
+ private null|string $issuer = null;
/**
- * @var string|null
+ * @var non-empty-string|null
*/
- private $label;
+ private null|string $label = null;
- /**
- * @var bool
- */
- private $issuer_included_as_parameter = true;
+ private bool $issuer_included_as_parameter = true;
/**
- * @return array<string, mixed>
+ * @return array<non-empty-string, mixed>
*/
public function getParameters(): array
{
$parameters = $this->parameters;
- if (null !== $this->getIssuer() && true === $this->isIssuerIncludedAsParameter()) {
+ if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) {
$parameters['issuer'] = $this->getIssuer();
}
@@ -56,15 +46,13 @@ trait ParameterTrait
public function getSecret(): string
{
- return $this->getParameter('secret');
- }
+ $value = $this->getParameter('secret');
+ (is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "secret" parameter.');
- public function setSecret(?string $secret): void
- {
- $this->setParameter('secret', $secret);
+ return $value;
}
- public function getLabel(): ?string
+ public function getLabel(): null|string
{
return $this->label;
}
@@ -74,7 +62,7 @@ trait ParameterTrait
$this->setParameter('label', $label);
}
- public function getIssuer(): ?string
+ public function getIssuer(): null|string
{
return $this->issuer;
}
@@ -96,30 +84,26 @@ trait ParameterTrait
public function getDigits(): int
{
- return $this->getParameter('digits');
- }
+ $value = $this->getParameter('digits');
+ (is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "digits" parameter.');
- private function setDigits(int $digits): void
- {
- $this->setParameter('digits', $digits);
+ return $value;
}
public function getDigest(): string
{
- return $this->getParameter('algorithm');
- }
+ $value = $this->getParameter('algorithm');
+ (is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "algorithm" parameter.');
- private function setDigest(string $digest): void
- {
- $this->setParameter('algorithm', $digest);
+ return $value;
}
public function hasParameter(string $parameter): bool
{
- return \array_key_exists($parameter, $this->parameters);
+ return array_key_exists($parameter, $this->parameters);
}
- public function getParameter(string $parameter)
+ public function getParameter(string $parameter): mixed
{
if ($this->hasParameter($parameter)) {
return $this->getParameters()[$parameter];
@@ -128,65 +112,85 @@ trait ParameterTrait
throw new InvalidArgumentException(sprintf('Parameter "%s" does not exist', $parameter));
}
- public function setParameter(string $parameter, $value): void
+ public function setParameter(string $parameter, mixed $value): void
{
$map = $this->getParameterMap();
- if (true === \array_key_exists($parameter, $map)) {
+ if (array_key_exists($parameter, $map) === true) {
$callback = $map[$parameter];
$value = $callback($value);
}
if (property_exists($this, $parameter)) {
- $this->$parameter = $value;
+ $this->{$parameter} = $value;
} else {
$this->parameters[$parameter] = $value;
}
}
+ public function setSecret(string $secret): void
+ {
+ $this->setParameter('secret', $secret);
+ }
+
+ public function setDigits(int $digits): void
+ {
+ $this->setParameter('digits', $digits);
+ }
+
+ public function setDigest(string $digest): void
+ {
+ $this->setParameter('algorithm', $digest);
+ }
+
/**
- * @return array<string, mixed>
+ * @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
return [
- 'label' => function ($value) {
- Assertion::false($this->hasColon($value), 'Label must not contain a colon.');
+ 'label' => function (string $value): string {
+ assert($value !== '');
+ $this->hasColon($value) === false || throw new InvalidArgumentException(
+ 'Label must not contain a colon.'
+ );
return $value;
},
- 'secret' => function ($value): string {
- if (null === $value) {
- $value = Base32::encodeUpper(random_bytes(64));
- }
- $value = trim(mb_strtoupper($value), '=');
-
- return $value;
- },
- 'algorithm' => function ($value): string {
+ 'secret' => static fn (string $value): string => mb_strtoupper(trim($value, '=')),
+ 'algorithm' => static function (string $value): string {
$value = mb_strtolower($value);
- Assertion::inArray($value, hash_algos(), sprintf('The "%s" digest is not supported.', $value));
+ in_array($value, hash_algos(), true) || throw new InvalidArgumentException(sprintf(
+ 'The "%s" digest is not supported.',
+ $value
+ ));
return $value;
},
- 'digits' => function ($value): int {
- Assertion::greaterThan($value, 0, 'Digits must be at least 1.');
+ 'digits' => static function ($value): int {
+ $value > 0 || throw new InvalidArgumentException('Digits must be at least 1.');
return (int) $value;
},
- 'issuer' => function ($value) {
- Assertion::false($this->hasColon($value), 'Issuer must not contain a colon.');
+ 'issuer' => function (string $value): string {
+ assert($value !== '');
+ $this->hasColon($value) === false || throw new InvalidArgumentException(
+ 'Issuer must not contain a colon.'
+ );
return $value;
},
];
}
+ /**
+ * @param non-empty-string $value
+ */
private function hasColon(string $value): bool
{
$colons = [':', '%3A', '%3a'];
foreach ($colons as $colon) {
- if (false !== mb_strpos($value, $colon)) {
+ if (str_contains($value, $colon)) {
return true;
}
}
diff --git a/vendor/spomky-labs/otphp/src/TOTP.php b/vendor/spomky-labs/otphp/src/TOTP.php
index 588b37f17..035e04f95 100644
--- a/vendor/spomky-labs/otphp/src/TOTP.php
+++ b/vendor/spomky-labs/otphp/src/TOTP.php
@@ -2,158 +2,214 @@
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 function Safe\ksort;
+use InvalidArgumentException;
+use Psr\Clock\ClockInterface;
+use function assert;
+use function is_int;
+/**
+ * @see \OTPHP\Test\TOTPTest
+ */
final class TOTP extends OTP implements TOTPInterface
{
- protected function __construct(?string $secret, int $period, string $digest, int $digits, int $epoch = 0)
+ private readonly ClockInterface $clock;
+
+ public function __construct(string $secret, ?ClockInterface $clock = null)
{
- parent::__construct($secret, $digest, $digits);
- $this->setPeriod($period);
- $this->setEpoch($epoch);
+ parent::__construct($secret);
+ if ($clock === null) {
+ trigger_deprecation(
+ 'spomky-labs/otphp',
+ '11.3.0',
+ 'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".'
+ );
+ $clock = new InternalClock();
+ }
+
+ $this->clock = $clock;
}
- public static function create(?string $secret = null, int $period = 30, string $digest = 'sha1', int $digits = 6, int $epoch = 0): TOTPInterface
- {
- return new self($secret, $period, $digest, $digits, $epoch);
+ public static function create(
+ null|string $secret = null,
+ int $period = self::DEFAULT_PERIOD,
+ string $digest = self::DEFAULT_DIGEST,
+ int $digits = self::DEFAULT_DIGITS,
+ int $epoch = self::DEFAULT_EPOCH,
+ ?ClockInterface $clock = null
+ ): self {
+ $totp = $secret !== null
+ ? self::createFromSecret($secret, $clock)
+ : self::generate($clock)
+ ;
+ $totp->setPeriod($period);
+ $totp->setDigest($digest);
+ $totp->setDigits($digits);
+ $totp->setEpoch($epoch);
+
+ return $totp;
}
- protected function setPeriod(int $period): void
+ public static function createFromSecret(string $secret, ?ClockInterface $clock = null): self
{
- $this->setParameter('period', $period);
+ $totp = new self($secret, $clock);
+ $totp->setPeriod(self::DEFAULT_PERIOD);
+ $totp->setDigest(self::DEFAULT_DIGEST);
+ $totp->setDigits(self::DEFAULT_DIGITS);
+ $totp->setEpoch(self::DEFAULT_EPOCH);
+
+ return $totp;
}
- public function getPeriod(): int
+ public static function generate(?ClockInterface $clock = null): self
{
- return $this->getParameter('period');
+ return self::createFromSecret(self::generateSecret(), $clock);
}
- private function setEpoch(int $epoch): void
+ public function getPeriod(): int
{
- $this->setParameter('epoch', $epoch);
+ $value = $this->getParameter('period');
+ (is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "period" parameter.');
+
+ return $value;
}
public function getEpoch(): int
{
- return $this->getParameter('epoch');
- }
+ $value = $this->getParameter('epoch');
+ (is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "epoch" parameter.');
- public function at(int $timestamp): string
- {
- return $this->generateOTP($this->timecode($timestamp));
+ return $value;
}
- public function now(): string
+ public function expiresIn(): int
{
- return $this->at(time());
+ $period = $this->getPeriod();
+
+ return $period - ($this->clock->now()->getTimestamp() % $this->getPeriod());
}
/**
- * If no timestamp is provided, the OTP is verified at the actual timestamp.
+ * The OTP at the specified input.
+ *
+ * @param 0|positive-int $input
*/
- public function verify(string $otp, ?int $timestamp = null, ?int $window = null): bool
+ public function at(int $input): string
{
- $timestamp = $this->getTimestamp($timestamp);
+ return $this->generateOTP($this->timecode($input));
+ }
- if (null === $window) {
- return $this->compareOTP($this->at($timestamp), $otp);
- }
+ public function now(): string
+ {
+ $timestamp = $this->clock->now()
+ ->getTimestamp();
+ assert($timestamp >= 0, 'The timestamp must return a positive integer.');
- return $this->verifyOtpWithWindow($otp, $timestamp, $window);
+ return $this->at($timestamp);
}
- private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): bool
+ /**
+ * If no timestamp is provided, the OTP is verified at the actual timestamp. When used, the leeway parameter will
+ * allow time drift. The passed value is in seconds.
+ *
+ * @param 0|positive-int $timestamp
+ * @param null|0|positive-int $leeway
+ */
+ public function verify(string $otp, null|int $timestamp = null, null|int $leeway = null): bool
{
- $window = abs($window);
-
- for ($i = 0; $i <= $window; ++$i) {
- $next = $i * $this->getPeriod() + $timestamp;
- $previous = -$i * $this->getPeriod() + $timestamp;
- $valid = $this->compareOTP($this->at($next), $otp) ||
- $this->compareOTP($this->at($previous), $otp);
+ $timestamp ??= $this->clock->now()
+ ->getTimestamp();
+ $timestamp >= 0 || throw new InvalidArgumentException('Timestamp must be at least 0.');
- if ($valid) {
- return true;
- }
+ if ($leeway === null) {
+ return $this->compareOTP($this->at($timestamp), $otp);
}
- return false;
- }
-
- private function getTimestamp(?int $timestamp): int
- {
- $timestamp = $timestamp ?? time();
- Assertion::greaterOrEqualThan($timestamp, 0, 'Timestamp must be at least 0.');
+ $leeway = abs($leeway);
+ $leeway < $this->getPeriod() || throw new InvalidArgumentException(
+ 'The leeway must be lower than the TOTP period'
+ );
+ $timestampMinusLeeway = $timestamp - $leeway;
+ $timestampMinusLeeway >= 0 || throw new InvalidArgumentException(
+ 'The timestamp must be greater than or equal to the leeway.'
+ );
- return $timestamp;
+ return $this->compareOTP($this->at($timestampMinusLeeway), $otp)
+ || $this->compareOTP($this->at($timestamp), $otp)
+ || $this->compareOTP($this->at($timestamp + $leeway), $otp);
}
public function getProvisioningUri(): string
{
$params = [];
- if (30 !== $this->getPeriod()) {
+ if ($this->getPeriod() !== 30) {
$params['period'] = $this->getPeriod();
}
- if (0 !== $this->getEpoch()) {
+ if ($this->getEpoch() !== 0) {
$params['epoch'] = $this->getEpoch();
}
return $this->generateURI('totp', $params);
}
- private function timecode(int $timestamp): int
+ public function setPeriod(int $period): void
{
- return (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
+ $this->setParameter('period', $period);
+ }
+
+ public function setEpoch(int $epoch): void
+ {
+ $this->setParameter('epoch', $epoch);
}
/**
- * @return array<string, mixed>
+ * @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
- $v = array_merge(
- parent::getParameterMap(),
- [
- 'period' => function ($value): int {
- Assertion::greaterThan((int) $value, 0, 'Period must be at least 1.');
-
- return (int) $value;
- },
- 'epoch' => function ($value): int {
- Assertion::greaterOrEqualThan((int) $value, 0, 'Epoch must be greater than or equal to 0.');
-
- return (int) $value;
- },
- ]
- );
-
- return $v;
+ return [
+ ...parent::getParameterMap(),
+ 'period' => static function ($value): int {
+ (int) $value > 0 || throw new InvalidArgumentException('Period must be at least 1.');
+
+ return (int) $value;
+ },
+ 'epoch' => static function ($value): int {
+ (int) $value >= 0 || throw new InvalidArgumentException(
+ 'Epoch must be greater than or equal to 0.'
+ );
+
+ return (int) $value;
+ },
+ ];
}
/**
- * @param array<string, mixed> $options
+ * @param array<non-empty-string, mixed> $options
*/
protected function filterOptions(array &$options): void
{
parent::filterOptions($options);
- if (isset($options['epoch']) && 0 === $options['epoch']) {
+ if (isset($options['epoch']) && $options['epoch'] === 0) {
unset($options['epoch']);
}
ksort($options);
}
+
+ /**
+ * @param 0|positive-int $timestamp
+ *
+ * @return 0|positive-int
+ */
+ private function timecode(int $timestamp): int
+ {
+ $timecode = (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
+ assert($timecode >= 0);
+
+ return $timecode;
+ }
}
diff --git a/vendor/spomky-labs/otphp/src/TOTPInterface.php b/vendor/spomky-labs/otphp/src/TOTPInterface.php
index a19fe7c0b..a79fedcce 100644
--- a/vendor/spomky-labs/otphp/src/TOTPInterface.php
+++ b/vendor/spomky-labs/otphp/src/TOTPInterface.php
@@ -2,28 +2,41 @@
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;
interface TOTPInterface extends OTPInterface
{
+ public const DEFAULT_PERIOD = 30;
+
+ public const DEFAULT_EPOCH = 0;
+
/**
* Create a new TOTP object.
*
* If the secret is null, a random 64 bytes secret will be generated.
+ *
+ * @param null|non-empty-string $secret
+ * @param positive-int $period
+ * @param non-empty-string $digest
+ * @param positive-int $digits
+ *
+ * @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
*/
- public static function create(?string $secret = null, int $period = 30, string $digest = 'sha1', int $digits = 6): self;
+ public static function create(
+ null|string $secret = null,
+ int $period = self::DEFAULT_PERIOD,
+ string $digest = self::DEFAULT_DIGEST,
+ int $digits = self::DEFAULT_DIGITS
+ ): self;
+
+ public function setPeriod(int $period): void;
+
+ public function setEpoch(int $epoch): void;
/**
* Return the TOTP at the current time.
+ *
+ * @return non-empty-string
*/
public function now(): string;
@@ -32,5 +45,7 @@ interface TOTPInterface extends OTPInterface
*/
public function getPeriod(): int;
+ public function expiresIn(): int;
+
public function getEpoch(): int;
}
diff --git a/vendor/spomky-labs/otphp/src/Url.php b/vendor/spomky-labs/otphp/src/Url.php
new file mode 100644
index 000000000..e88cd6d29
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/Url.php
@@ -0,0 +1,102 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OTPHP;
+
+use InvalidArgumentException;
+use function array_key_exists;
+use function is_string;
+
+/**
+ * @internal
+ */
+final class Url
+{
+ /**
+ * @param non-empty-string $scheme
+ * @param non-empty-string $host
+ * @param non-empty-string $path
+ * @param non-empty-string $secret
+ * @param array<non-empty-string, mixed> $query
+ */
+ public function __construct(
+ private readonly string $scheme,
+ private readonly string $host,
+ private readonly string $path,
+ private readonly string $secret,
+ private readonly array $query
+ ) {
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public function getScheme(): string
+ {
+ return $this->scheme;
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public function getHost(): string
+ {
+ return $this->host;
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public function getPath(): string
+ {
+ return $this->path;
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public function getSecret(): string
+ {
+ return $this->secret;
+ }
+
+ /**
+ * @return array<non-empty-string, mixed>
+ */
+ public function getQuery(): array
+ {
+ return $this->query;
+ }
+
+ /**
+ * @param non-empty-string $uri
+ */
+ public static function fromString(string $uri): self
+ {
+ $parsed_url = parse_url($uri);
+ $parsed_url !== false || throw new InvalidArgumentException('Invalid URI.');
+ foreach (['scheme', 'host', 'path', 'query'] as $key) {
+ array_key_exists($key, $parsed_url) || throw new InvalidArgumentException(
+ 'Not a valid OTP provisioning URI'
+ );
+ }
+ $scheme = $parsed_url['scheme'] ?? null;
+ $host = $parsed_url['host'] ?? null;
+ $path = $parsed_url['path'] ?? null;
+ $query = $parsed_url['query'] ?? null;
+ $scheme === 'otpauth' || throw new InvalidArgumentException('Not a valid OTP provisioning URI');
+ is_string($host) || throw new InvalidArgumentException('Invalid URI.');
+ is_string($path) || throw new InvalidArgumentException('Invalid URI.');
+ is_string($query) || throw new InvalidArgumentException('Invalid URI.');
+ $parsedQuery = [];
+ parse_str($query, $parsedQuery);
+ array_key_exists('secret', $parsedQuery) || throw new InvalidArgumentException(
+ 'Not a valid OTP provisioning URI'
+ );
+ $secret = $parsedQuery['secret'];
+ unset($parsedQuery['secret']);
+
+ return new self($scheme, $host, $path, $secret, $parsedQuery);
+ }
+}