aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/spomky-labs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/spomky-labs')
-rw-r--r--vendor/spomky-labs/otphp/LICENSE20
-rw-r--r--vendor/spomky-labs/otphp/README.md42
-rw-r--r--vendor/spomky-labs/otphp/SECURITY.md87
-rw-r--r--vendor/spomky-labs/otphp/composer.json57
-rw-r--r--vendor/spomky-labs/otphp/src/Factory.php85
-rw-r--r--vendor/spomky-labs/otphp/src/FactoryInterface.php14
-rw-r--r--vendor/spomky-labs/otphp/src/HOTP.php124
-rw-r--r--vendor/spomky-labs/otphp/src/HOTPInterface.php34
-rw-r--r--vendor/spomky-labs/otphp/src/OTP.php129
-rw-r--r--vendor/spomky-labs/otphp/src/OTPInterface.php110
-rw-r--r--vendor/spomky-labs/otphp/src/ParameterTrait.php188
-rw-r--r--vendor/spomky-labs/otphp/src/TOTP.php172
-rw-r--r--vendor/spomky-labs/otphp/src/TOTPInterface.php47
-rw-r--r--vendor/spomky-labs/otphp/src/Url.php88
14 files changed, 1197 insertions, 0 deletions
diff --git a/vendor/spomky-labs/otphp/LICENSE b/vendor/spomky-labs/otphp/LICENSE
new file mode 100644
index 000000000..e6a673e34
--- /dev/null
+++ b/vendor/spomky-labs/otphp/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014-2016 Florent Morselli
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/spomky-labs/otphp/README.md b/vendor/spomky-labs/otphp/README.md
new file mode 100644
index 000000000..b23282ea1
--- /dev/null
+++ b/vendor/spomky-labs/otphp/README.md
@@ -0,0 +1,42 @@
+TOTP / HOTP library in PHP
+==========================
+
+![Build Status](https://github.com/spomky-labs/otphp/workflows/Integrate/badge.svg)
+
+[![Latest Stable Version](https://poser.pugx.org/spomky-labs/otphp/v/stable.png)](https://packagist.org/packages/spomky-labs/otphp)
+[![Total Downloads](https://poser.pugx.org/spomky-labs/otphp/downloads.png)](https://packagist.org/packages/spomky-labs/otphp)
+[![Latest Unstable Version](https://poser.pugx.org/spomky-labs/otphp/v/unstable.png)](https://packagist.org/packages/spomky-labs/otphp)
+[![License](https://poser.pugx.org/spomky-labs/otphp/license.png)](https://packagist.org/packages/spomky-labs/otphp)
+
+A php library for generating one-time passwords according to [RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226) (HOTP Algorithm) and [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238) (TOTP Algorithm)
+
+This library is compatible with Google Authenticator apps available for Android and iPhone.
+It is also compatible with other applications such as [FreeOTP](https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp) for example.
+
+# Documentation
+
+The documentation of this project is available in the [*doc* folder](doc/index.md).
+
+# Support
+
+I bring solutions to your problems and answer your questions.
+
+If you really love that project, and the work I have done or if you want I prioritize your issues, then you can help me out for a couple of :beers: or more!
+
+[Become a sponsor](https://github.com/sponsors/Spomky)
+
+Or
+
+[![Become a Patreon](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/FlorentMorselli)
+
+## Contributing
+
+Requests for new features, bug fixed and all other ideas to make this project useful are welcome.
+
+Please report all issues in [the repository bug tracker](hhttps://github.com/Spomky-Labs/otphp/issues).
+
+Also make sure to [follow these best practices](.github/CONTRIBUTING.md).
+
+## Licence
+
+This software is release under the [MIT licence](LICENSE).
diff --git a/vendor/spomky-labs/otphp/SECURITY.md b/vendor/spomky-labs/otphp/SECURITY.md
new file mode 100644
index 000000000..706ad9a32
--- /dev/null
+++ b/vendor/spomky-labs/otphp/SECURITY.md
@@ -0,0 +1,87 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| ------- |----------------------------------------|
+| 11.0.x | :white_check_mark: |
+| 10.0.x | :white_check_mark: (security fix only) |
+| < 10.0 | :x: |
+
+## Reporting a Vulnerability
+
+Please email `security@spomky-labs.com`.
+If deemed necessary, you can encrypt your message using one of the following GPG key
+
+```
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+xjMEXTsJVxYJKwYBBAHaRw8BAQdAZCS93eHRx97V+LQbAWuAaeKIdUZ9YIkn
+QH5pQ7dDU0TNMWNvbnRhY3RAc3BvbWt5LWxhYnMuY29tIDxjb250YWN0QHNw
+b21reS1sYWJzLmNvbT7CdwQQFgoAHwUCXTsJVwYLCQcIAwIEFQgKAgMWAgEC
+GQECGwMCHgEACgkQG6hbCDSDj+1/tgEAoy11uHvDV7kkG/iN2/0ylV72hU8y
+c/xoqGd7qFaKD6ABANcthlg63OrQVTf0dUPOT9Y2BJpOOA88JJWgILtuUPIO
+zjgEXTsJVxIKKwYBBAGXVQEFAQEHQKiX7nldkmICePhzwReZnBPmjpsmNt7V
+Y8xHdICKsr8cAwEIB8JhBBgWCAAJBQJdOwlXAhsMAAoJEBuoWwg0g4/t0KgA
+/31ucb/bL/MGpWFrpSjTs6uQhZWlBmcFoeMhwCYepIpZAQDd65UBqFDKXJWv
+Xy3zoMQQzD9Z6fUATnFrWkzjHwhvDQ==
+=j4dw
+-----END PGP PUBLIC KEY BLOCK-----
+```
+
+
+```
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+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
+BUqlX5YJHneD9Q8Sfz/HKDQDCqg=
+=o+4z
+-----END PGP PUBLIC KEY BLOCK-----
+```
diff --git a/vendor/spomky-labs/otphp/composer.json b/vendor/spomky-labs/otphp/composer.json
new file mode 100644
index 000000000..30db9729d
--- /dev/null
+++ b/vendor/spomky-labs/otphp/composer.json
@@ -0,0 +1,57 @@
+{
+ "name": "spomky-labs/otphp",
+ "type": "library",
+ "description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator",
+ "license": "MIT",
+ "keywords": ["otp", "hotp", "totp", "RFC 4226", "RFC 6238", "Google Authenticator", "FreeOTP"],
+ "homepage": "https://github.com/Spomky-Labs/otphp",
+ "authors": [
+ {
+ "name": "Florent Morselli",
+ "homepage": "https://github.com/Spomky"
+ },
+ {
+ "name": "All contributors",
+ "homepage": "https://github.com/Spomky-Labs/otphp/contributors"
+ }
+ ],
+ "require": {
+ "php": "^8.1",
+ "ext-mbstring": "*",
+ "paragonie/constant_time_encoding": "^2.0"
+ },
+ "require-dev": {
+ "ekino/phpstan-banned-code": "^1.0",
+ "infection/infection": "^0.26",
+ "php-parallel-lint/php-parallel-lint": "^1.3",
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-deprecation-rules": "^1.0",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.0",
+ "phpunit/phpunit": "^9.5.26",
+ "qossmic/deptrac-shim": "^1.0",
+ "rector/rector": "^0.14",
+ "symfony/phpunit-bridge": "^6.1",
+ "symplify/easy-coding-standard": "^11.0"
+ },
+ "autoload": {
+ "psr-4": { "OTPHP\\": "src/" }
+ },
+ "autoload-dev": {
+ "psr-4": { "OTPHP\\Test\\": "tests/" }
+ },
+ "config": {
+ "allow-plugins": {
+ "phpstan/extension-installer": true,
+ "infection/extension-installer": true,
+ "composer/package-versions-deprecated": true,
+ "symfony/flex": true,
+ "symfony/runtime": true
+ },
+ "optimize-autoloader": true,
+ "preferred-install": {
+ "*": "dist"
+ },
+ "sort-packages": true
+ }
+}
diff --git a/vendor/spomky-labs/otphp/src/Factory.php b/vendor/spomky-labs/otphp/src/Factory.php
new file mode 100644
index 000000000..d5c60cc34
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/Factory.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OTPHP;
+
+use function count;
+use InvalidArgumentException;
+use Throwable;
+
+/**
+ * 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
+ {
+ try {
+ $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);
+ }
+
+ $otp = self::createOTP($parsed_url);
+
+ self::populateOTP($otp, $parsed_url);
+
+ return $otp;
+ }
+
+ private static function populateParameters(OTPInterface $otp, Url $data): void
+ {
+ foreach ($data->getQuery() as $key => $value) {
+ $otp->setParameter($key, $value);
+ }
+ }
+
+ private static function populateOTP(OTPInterface $otp, Url $data): void
+ {
+ self::populateParameters($otp, $data);
+ $result = explode(':', rawurldecode(mb_substr($data->getPath(), 1)));
+
+ if (count($result) < 2) {
+ $otp->setIssuerIncludedAsParameter(false);
+
+ return;
+ }
+
+ if ($otp->getIssuer() !== null) {
+ $result[0] === $otp->getIssuer() || throw new InvalidArgumentException(
+ 'Invalid OTP: invalid issuer in parameter'
+ );
+ $otp->setIssuerIncludedAsParameter(true);
+ }
+ $otp->setIssuer($result[0]);
+ }
+
+ private static function createOTP(Url $parsed_url): OTPInterface
+ {
+ switch ($parsed_url->getHost()) {
+ case 'totp':
+ $totp = TOTP::createFromSecret($parsed_url->getSecret());
+ $totp->setLabel(self::getLabel($parsed_url->getPath()));
+
+ return $totp;
+ case 'hotp':
+ $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->getHost()));
+ }
+ }
+
+ private static function getLabel(string $data): string
+ {
+ $result = explode(':', rawurldecode(mb_substr($data, 1)));
+
+ return count($result) === 2 ? $result[1] : $result[0];
+ }
+}
diff --git a/vendor/spomky-labs/otphp/src/FactoryInterface.php b/vendor/spomky-labs/otphp/src/FactoryInterface.php
new file mode 100644
index 000000000..74386adeb
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/FactoryInterface.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+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.
+ */
+ 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
new file mode 100644
index 000000000..aa5a22754
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/HOTP.php
@@ -0,0 +1,124 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OTPHP;
+
+use InvalidArgumentException;
+use function is_int;
+
+/**
+ * @see \OTPHP\Test\HOTPTest
+ */
+final class HOTP extends OTP implements HOTPInterface
+{
+ 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 createFromSecret(string $secret): self
+ {
+ $htop = new self($secret);
+ $htop->setCounter(self::DEFAULT_COUNTER);
+ $htop->setDigest(self::DEFAULT_DIGEST);
+ $htop->setDigits(self::DEFAULT_DIGITS);
+
+ return $htop;
+ }
+
+ public static function generate(): self
+ {
+ return self::createFromSecret(self::generateSecret());
+ }
+
+ public function getCounter(): int
+ {
+ $value = $this->getParameter('counter');
+ is_int($value) || throw new InvalidArgumentException('Invalid "counter" parameter.');
+
+ return $value;
+ }
+
+ public function getProvisioningUri(): string
+ {
+ return $this->generateURI('hotp', [
+ 'counter' => $this->getCounter(),
+ ]);
+ }
+
+ /**
+ * If the counter is not provided, the OTP is verified at the actual counter.
+ */
+ public function verify(string $otp, null|int $counter = null, null|int $window = null): bool
+ {
+ $counter >= 0 || throw new InvalidArgumentException('The counter must be at least 0.');
+
+ if ($counter === null) {
+ $counter = $this->getCounter();
+ } elseif ($counter < $this->getCounter()) {
+ return false;
+ }
+
+ return $this->verifyOtpWithWindow($otp, $counter, $window);
+ }
+
+ public function setCounter(int $counter): void
+ {
+ $this->setParameter('counter', $counter);
+ }
+
+ /**
+ * @return array<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);
+ }
+
+ private function getWindow(null|int $window): int
+ {
+ return abs($window ?? self::DEFAULT_WINDOW);
+ }
+
+ private function verifyOtpWithWindow(string $otp, int $counter, null|int $window): bool
+ {
+ $window = $this->getWindow($window);
+
+ for ($i = $counter; $i <= $counter + $window; ++$i) {
+ if ($this->compareOTP($this->at($i), $otp)) {
+ $this->updateCounter($i + 1);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/vendor/spomky-labs/otphp/src/HOTPInterface.php b/vendor/spomky-labs/otphp/src/HOTPInterface.php
new file mode 100644
index 000000000..853e76c07
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/HOTPInterface.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OTPHP;
+
+interface HOTPInterface extends OTPInterface
+{
+ public const DEFAULT_COUNTER = 0;
+
+ /**
+ * The initial counter (a positive integer).
+ */
+ public function getCounter(): int;
+
+ /**
+ * 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 non-empty-string $digest
+ *
+ * @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
+ */
+ 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/OTP.php b/vendor/spomky-labs/otphp/src/OTP.php
new file mode 100644
index 000000000..2cba067b3
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/OTP.php
@@ -0,0 +1,129 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OTPHP;
+
+use function chr;
+use function count;
+use Exception;
+use InvalidArgumentException;
+use function is_string;
+use ParagonIE\ConstantTime\Base32;
+use RuntimeException;
+use const STR_PAD_LEFT;
+
+abstract class OTP implements OTPInterface
+{
+ use ParameterTrait;
+
+ private const DEFAULT_SECRET_SIZE = 64;
+
+ /**
+ * @param non-empty-string $secret
+ */
+ protected function __construct(string $secret)
+ {
+ $this->setSecret($secret);
+ }
+
+ public function getQrCodeUri(string $uri, string $placeholder): string
+ {
+ $provisioning_uri = urlencode($this->getProvisioningUri());
+
+ return str_replace($placeholder, $provisioning_uri, $uri);
+ }
+
+ 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.
+ */
+ 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);
+
+ $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);
+ }
+
+ /**
+ * @param array<string, mixed> $options
+ */
+ protected function filterOptions(array &$options): void
+ {
+ foreach ([
+ 'algorithm' => 'sha1',
+ 'period' => 30,
+ 'digits' => 6,
+ ] as $key => $default) {
+ if (isset($options[$key]) && $default === $options[$key]) {
+ unset($options[$key]);
+ }
+ }
+
+ ksort($options);
+ }
+
+ /**
+ * @param array<string, mixed> $options
+ */
+ protected function generateURI(string $type, array $options): string
+ {
+ $label = $this->getLabel();
+ 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));
+
+ return sprintf(
+ 'otpauth://%s/%s?%s',
+ $type,
+ rawurlencode(($this->getIssuer() !== null ? $this->getIssuer() . ':' : '') . $label),
+ $params
+ );
+ }
+
+ protected function compareOTP(string $safe, string $user): bool
+ {
+ return hash_equals($safe, $user);
+ }
+
+ private function getDecodedSecret(): string
+ {
+ try {
+ return Base32::decodeUpper($this->getSecret());
+ } catch (Exception) {
+ throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?');
+ }
+ }
+
+ private function intToByteString(int $int): string
+ {
+ $result = [];
+ while ($int !== 0) {
+ $result[] = chr($int & 0xFF);
+ $int >>= 8;
+ }
+
+ 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
new file mode 100644
index 000000000..3b27f5456
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/OTPInterface.php
@@ -0,0 +1,110 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OTPHP;
+
+interface OTPInterface
+{
+ public const DEFAULT_DIGITS = 6;
+
+ public const DEFAULT_DIGEST = 'sha1';
+
+ /**
+ * Create a OTP object from an existing secret.
+ *
+ * @param non-empty-string $secret
+ */
+ public static function createFromSecret(string $secret): self;
+
+ /**
+ * Create a new OTP object. A random 64 bytes secret will be generated.
+ */
+ public static function generate(): self;
+
+ /**
+ * @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;
+
+ /**
+ * @return 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.
+ */
+ public function verify(string $otp, null|int $input = null, null|int $window = null): bool;
+
+ /**
+ * @return string The secret of the OTP
+ */
+ public function getSecret(): string;
+
+ /**
+ * @param string $label The label of the OTP
+ */
+ public function setLabel(string $label): void;
+
+ /**
+ * @return string|null The label of the OTP
+ */
+ public function getLabel(): null|string;
+
+ /**
+ * @return string|null The issuer
+ */
+ public function getIssuer(): ?string;
+
+ public function setIssuer(string $issuer): void;
+
+ /**
+ * @return bool If true, the issuer will be added as a parameter in the provisioning URI
+ */
+ public function isIssuerIncludedAsParameter(): bool;
+
+ public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void;
+
+ /**
+ * @return 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'
+ */
+ public function getDigest(): string;
+
+ public function getParameter(string $parameter): mixed;
+
+ public function hasParameter(string $parameter): bool;
+
+ /**
+ * @return array<string, mixed>
+ */
+ public function getParameters(): array;
+
+ public function setParameter(string $parameter, mixed $value): void;
+
+ /**
+ * Get the provisioning URI.
+ */
+ 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
+ */
+ 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
new file mode 100644
index 000000000..b05092351
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/ParameterTrait.php
@@ -0,0 +1,188 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OTPHP;
+
+use function array_key_exists;
+use function in_array;
+use InvalidArgumentException;
+use function is_int;
+use function is_string;
+
+trait ParameterTrait
+{
+ /**
+ * @var array<string, mixed>
+ */
+ private array $parameters = [];
+
+ private null|string $issuer = null;
+
+ private null|string $label = null;
+
+ private bool $issuer_included_as_parameter = true;
+
+ /**
+ * @return array<string, mixed>
+ */
+ public function getParameters(): array
+ {
+ $parameters = $this->parameters;
+
+ if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) {
+ $parameters['issuer'] = $this->getIssuer();
+ }
+
+ return $parameters;
+ }
+
+ public function getSecret(): string
+ {
+ $value = $this->getParameter('secret');
+ is_string($value) || throw new InvalidArgumentException('Invalid "secret" parameter.');
+
+ return $value;
+ }
+
+ public function getLabel(): null|string
+ {
+ return $this->label;
+ }
+
+ public function setLabel(string $label): void
+ {
+ $this->setParameter('label', $label);
+ }
+
+ public function getIssuer(): null|string
+ {
+ return $this->issuer;
+ }
+
+ public function setIssuer(string $issuer): void
+ {
+ $this->setParameter('issuer', $issuer);
+ }
+
+ public function isIssuerIncludedAsParameter(): bool
+ {
+ return $this->issuer_included_as_parameter;
+ }
+
+ public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void
+ {
+ $this->issuer_included_as_parameter = $issuer_included_as_parameter;
+ }
+
+ public function getDigits(): int
+ {
+ $value = $this->getParameter('digits');
+ is_int($value) || throw new InvalidArgumentException('Invalid "digits" parameter.');
+
+ return $value;
+ }
+
+ public function getDigest(): string
+ {
+ $value = $this->getParameter('algorithm');
+ is_string($value) || throw new InvalidArgumentException('Invalid "algorithm" parameter.');
+
+ return $value;
+ }
+
+ public function hasParameter(string $parameter): bool
+ {
+ return array_key_exists($parameter, $this->parameters);
+ }
+
+ public function getParameter(string $parameter): mixed
+ {
+ if ($this->hasParameter($parameter)) {
+ return $this->getParameters()[$parameter];
+ }
+
+ throw new InvalidArgumentException(sprintf('Parameter "%s" does not exist', $parameter));
+ }
+
+ public function setParameter(string $parameter, mixed $value): void
+ {
+ $map = $this->getParameterMap();
+
+ if (array_key_exists($parameter, $map) === true) {
+ $callback = $map[$parameter];
+ $value = $callback($value);
+ }
+
+ if (property_exists($this, $parameter)) {
+ $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, callable>
+ */
+ protected function getParameterMap(): array
+ {
+ return [
+ 'label' => function ($value) {
+ $this->hasColon($value) === false || throw new InvalidArgumentException(
+ 'Label must not contain a colon.'
+ );
+
+ return $value;
+ },
+ 'secret' => static fn ($value): string => mb_strtoupper(trim((string) $value, '=')),
+ 'algorithm' => static function ($value): string {
+ $value = mb_strtolower($value);
+ in_array($value, hash_algos(), true) || throw new InvalidArgumentException(sprintf(
+ 'The "%s" digest is not supported.',
+ $value
+ ));
+
+ return $value;
+ },
+ 'digits' => static function ($value): int {
+ $value > 0 || throw new InvalidArgumentException('Digits must be at least 1.');
+
+ return (int) $value;
+ },
+ 'issuer' => function ($value) {
+ $this->hasColon($value) === false || throw new InvalidArgumentException(
+ 'Issuer must not contain a colon.'
+ );
+
+ return $value;
+ },
+ ];
+ }
+
+ private function hasColon(string $value): bool
+ {
+ $colons = [':', '%3A', '%3a'];
+ foreach ($colons as $colon) {
+ if (str_contains($value, $colon)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/vendor/spomky-labs/otphp/src/TOTP.php b/vendor/spomky-labs/otphp/src/TOTP.php
new file mode 100644
index 000000000..e9bce9e14
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/TOTP.php
@@ -0,0 +1,172 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OTPHP;
+
+use InvalidArgumentException;
+use function is_int;
+
+/**
+ * @see \OTPHP\Test\TOTPTest
+ */
+final class TOTP extends OTP implements TOTPInterface
+{
+ 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
+ ): self {
+ $totp = $secret !== null
+ ? self::createFromSecret($secret)
+ : self::generate()
+ ;
+ $totp->setPeriod($period);
+ $totp->setDigest($digest);
+ $totp->setDigits($digits);
+ $totp->setEpoch($epoch);
+
+ return $totp;
+ }
+
+ public static function createFromSecret(string $secret): self
+ {
+ $totp = new self($secret);
+ $totp->setPeriod(self::DEFAULT_PERIOD);
+ $totp->setDigest(self::DEFAULT_DIGEST);
+ $totp->setDigits(self::DEFAULT_DIGITS);
+ $totp->setEpoch(self::DEFAULT_EPOCH);
+
+ return $totp;
+ }
+
+ public static function generate(): self
+ {
+ return self::createFromSecret(self::generateSecret());
+ }
+
+ public function getPeriod(): int
+ {
+ $value = $this->getParameter('period');
+ is_int($value) || throw new InvalidArgumentException('Invalid "period" parameter.');
+
+ return $value;
+ }
+
+ public function getEpoch(): int
+ {
+ $value = $this->getParameter('epoch');
+ is_int($value) || throw new InvalidArgumentException('Invalid "epoch" parameter.');
+
+ return $value;
+ }
+
+ public function expiresIn(): int
+ {
+ $period = $this->getPeriod();
+
+ return $period - (time() % $this->getPeriod());
+ }
+
+ public function at(int $input): string
+ {
+ return $this->generateOTP($this->timecode($input));
+ }
+
+ public function now(): string
+ {
+ return $this->at(time());
+ }
+
+ /**
+ * 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.
+ */
+ public function verify(string $otp, null|int $timestamp = null, null|int $leeway = null): bool
+ {
+ $timestamp ??= time();
+ $timestamp >= 0 || throw new InvalidArgumentException('Timestamp must be at least 0.');
+
+ if ($leeway === null) {
+ return $this->compareOTP($this->at($timestamp), $otp);
+ }
+
+ $leeway = abs($leeway);
+ $leeway < $this->getPeriod() || throw new InvalidArgumentException(
+ 'The leeway must be lower than the TOTP period'
+ );
+
+ return $this->compareOTP($this->at($timestamp - $leeway), $otp)
+ || $this->compareOTP($this->at($timestamp), $otp)
+ || $this->compareOTP($this->at($timestamp + $leeway), $otp);
+ }
+
+ public function getProvisioningUri(): string
+ {
+ $params = [];
+ if ($this->getPeriod() !== 30) {
+ $params['period'] = $this->getPeriod();
+ }
+
+ if ($this->getEpoch() !== 0) {
+ $params['epoch'] = $this->getEpoch();
+ }
+
+ return $this->generateURI('totp', $params);
+ }
+
+ public function setPeriod(int $period): void
+ {
+ $this->setParameter('period', $period);
+ }
+
+ public function setEpoch(int $epoch): void
+ {
+ $this->setParameter('epoch', $epoch);
+ }
+
+ /**
+ * @return array<string, callable>
+ */
+ protected function getParameterMap(): array
+ {
+ return array_merge(
+ 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
+ */
+ protected function filterOptions(array &$options): void
+ {
+ parent::filterOptions($options);
+
+ if (isset($options['epoch']) && $options['epoch'] === 0) {
+ unset($options['epoch']);
+ }
+
+ ksort($options);
+ }
+
+ private function timecode(int $timestamp): int
+ {
+ return (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
+ }
+}
diff --git a/vendor/spomky-labs/otphp/src/TOTPInterface.php b/vendor/spomky-labs/otphp/src/TOTPInterface.php
new file mode 100644
index 000000000..afb54e8e9
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/TOTPInterface.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+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 non-empty-string $digest
+ *
+ * @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
+ */
+ 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.
+ */
+ public function now(): string;
+
+ /**
+ * Get the period of time for OTP generation (a non-null positive integer, in second).
+ */
+ 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..56ad979c5
--- /dev/null
+++ b/vendor/spomky-labs/otphp/src/Url.php
@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OTPHP;
+
+use function array_key_exists;
+use InvalidArgumentException;
+use function is_string;
+
+/**
+ * @internal
+ */
+final class Url
+{
+ /**
+ * @param non-empty-string $secret
+ * @param array<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
+ ) {
+ }
+
+ public function getScheme(): string
+ {
+ return $this->scheme;
+ }
+
+ public function getHost(): string
+ {
+ return $this->host;
+ }
+
+ public function getPath(): string
+ {
+ return $this->path;
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public function getSecret(): string
+ {
+ return $this->secret;
+ }
+
+ /**
+ * @return array<string, mixed>
+ */
+ public function getQuery(): array
+ {
+ return $this->query;
+ }
+
+ 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'
+ );
+ is_string($parsed_url[$key]) || 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);
+ }
+}