diff options
author | Andrew Dolgov <[email protected]> | 2021-02-26 19:16:17 +0300 |
---|---|---|
committer | Andrew Dolgov <[email protected]> | 2021-02-26 19:16:17 +0300 |
commit | 3fd785654372d493c031d9b541ab33a881023a32 (patch) | |
tree | 0a76cb410217074378de3d7012b95754cd3c7e6f /vendor/spomky-labs/otphp/src | |
parent | bc4475b6698f5a74e475674aa7af43253c459892 (diff) |
* switch to composer for qrcode and otp dependencies
* move most OTP-related stuff into userhelper
* remove old phpqrcode and otphp libraries
Diffstat (limited to 'vendor/spomky-labs/otphp/src')
-rw-r--r-- | vendor/spomky-labs/otphp/src/Factory.php | 115 | ||||
-rw-r--r-- | vendor/spomky-labs/otphp/src/FactoryInterface.php | 23 | ||||
-rw-r--r-- | vendor/spomky-labs/otphp/src/HOTP.php | 103 | ||||
-rw-r--r-- | vendor/spomky-labs/otphp/src/HOTPInterface.php | 29 | ||||
-rw-r--r-- | vendor/spomky-labs/otphp/src/OTP.php | 114 | ||||
-rw-r--r-- | vendor/spomky-labs/otphp/src/OTPInterface.php | 97 | ||||
-rw-r--r-- | vendor/spomky-labs/otphp/src/ParameterTrait.php | 196 | ||||
-rw-r--r-- | vendor/spomky-labs/otphp/src/TOTP.php | 159 | ||||
-rw-r--r-- | vendor/spomky-labs/otphp/src/TOTPInterface.php | 36 |
9 files changed, 872 insertions, 0 deletions
diff --git a/vendor/spomky-labs/otphp/src/Factory.php b/vendor/spomky-labs/otphp/src/Factory.php new file mode 100644 index 000000000..70df63945 --- /dev/null +++ b/vendor/spomky-labs/otphp/src/Factory.php @@ -0,0 +1,115 @@ +<?php + +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 Throwable; + +/** + * This class is used to load OTP object from a provisioning Uri. + */ +final class Factory implements FactoryInterface +{ + public static function loadFromProvisioningUri(string $uri): OTPInterface + { + try { + $parsed_url = parse_url($uri); + } 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); + + $otp = self::createOTP($parsed_url); + + self::populateOTP($otp, $parsed_url); + + return $otp; + } + + /** + * @param array<string, mixed> $data + */ + private static function populateParameters(OTPInterface &$otp, array $data): void + { + foreach ($data['query'] as $key => $value) { + $otp->setParameter($key, $value); + } + } + + /** + * @param array<string, mixed> $data + */ + private static function populateOTP(OTPInterface &$otp, array $data): void + { + self::populateParameters($otp, $data); + $result = explode(':', rawurldecode(mb_substr($data['path'], 1))); + + if (2 > \count($result)) { + $otp->setIssuerIncludedAsParameter(false); + + return; + } + + if (null !== $otp->getIssuer()) { + Assertion::eq($result[0], $otp->getIssuer(), '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'); + } + + /** + * @param array<string, mixed> $parsed_url + */ + private static function createOTP(array $parsed_url): OTPInterface + { + switch ($parsed_url['host']) { + case 'totp': + $totp = TOTP::create($parsed_url['query']['secret']); + $totp->setLabel(self::getLabel($parsed_url['path'])); + + return $totp; + case 'hotp': + $hotp = HOTP::create($parsed_url['query']['secret']); + $hotp->setLabel(self::getLabel($parsed_url['path'])); + + return $hotp; + default: + throw new InvalidArgumentException(sprintf('Unsupported "%s" OTP type', $parsed_url['host'])); + } + } + + private static function getLabel(string $data): string + { + $result = explode(':', rawurldecode(mb_substr($data, 1))); + + return 2 === \count($result) ? $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..00acc2d04 --- /dev/null +++ b/vendor/spomky-labs/otphp/src/FactoryInterface.php @@ -0,0 +1,23 @@ +<?php + +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. + */ + 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..a2f4a2395 --- /dev/null +++ b/vendor/spomky-labs/otphp/src/HOTP.php @@ -0,0 +1,103 @@ +<?php + +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; + +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); + } + + public static function create(?string $secret = null, int $counter = 0, string $digest = 'sha1', int $digits = 6): HOTPInterface + { + return new self($secret, $counter, $digest, $digits); + } + + protected function setCounter(int $counter): void + { + $this->setParameter('counter', $counter); + } + + public function getCounter(): int + { + return $this->getParameter('counter'); + } + + private function updateCounter(int $counter): void + { + $this->setCounter($counter); + } + + 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, ?int $counter = null, ?int $window = null): bool + { + Assertion::greaterOrEqualThan($counter, 0, 'The counter must be at least 0.'); + + if (null === $counter) { + $counter = $this->getCounter(); + } elseif ($counter < $this->getCounter()) { + return false; + } + + return $this->verifyOtpWithWindow($otp, $counter, $window); + } + + private function getWindow(?int $window): int + { + return abs($window ?? 0); + } + + private function verifyOtpWithWindow(string $otp, int $counter, ?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; + } + + /** + * @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 new file mode 100644 index 000000000..336ce1055 --- /dev/null +++ b/vendor/spomky-labs/otphp/src/HOTPInterface.php @@ -0,0 +1,29 @@ +<?php + +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 +{ + /** + * The initial counter (a positive integer). + */ + public function getCounter(): int; + + /** + * Create a new TOTP object. + * + * If the secret is null, a random 64 bytes secret will be generated. + */ + public static function create(?string $secret = null, int $counter = 0, string $digest = 'sha1', int $digits = 6): self; +} diff --git a/vendor/spomky-labs/otphp/src/OTP.php b/vendor/spomky-labs/otphp/src/OTP.php new file mode 100644 index 000000000..932bcf97e --- /dev/null +++ b/vendor/spomky-labs/otphp/src/OTP.php @@ -0,0 +1,114 @@ +<?php + +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 ParagonIE\ConstantTime\Base32; +use RuntimeException; +use function Safe\ksort; +use function Safe\sprintf; + +abstract class OTP implements OTPInterface +{ + use ParameterTrait; + + protected function __construct(?string $secret, string $digest, int $digits) + { + $this->setSecret($secret); + $this->setDigest($digest); + $this->setDigits($digits); + } + + public function getQrCodeUri(string $uri, string $placeholder): string + { + $provisioning_uri = urlencode($this->getProvisioningUri()); + + return str_replace($placeholder, $provisioning_uri, $uri); + } + + /** + * The OTP at the specified input. + */ + protected function generateOTP(int $input): string + { + $hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true); + + $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); + $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 + */ + 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(); + 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()); + $this->filterOptions($options); + $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options)); + + return sprintf('otpauth://%s/%s?%s', $type, rawurlencode((null !== $this->getIssuer() ? $this->getIssuer().':' : '').$label), $params); + } + + private function getDecodedSecret(): string + { + try { + return Base32::decodeUpper($this->getSecret()); + } catch (\Exception $e) { + throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?'); + } + } + + private function intToByteString(int $int): string + { + $result = []; + while (0 !== $int) { + $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); + } +} diff --git a/vendor/spomky-labs/otphp/src/OTPInterface.php b/vendor/spomky-labs/otphp/src/OTPInterface.php new file mode 100644 index 000000000..66e163d5d --- /dev/null +++ b/vendor/spomky-labs/otphp/src/OTPInterface.php @@ -0,0 +1,97 @@ +<?php + +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 +{ + /** + * @return string Return the OTP at the specified timestamp + */ + public function at(int $timestamp): 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, ?int $input = 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(): ?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; + + /** + * @return mixed|null + */ + public function getParameter(string $parameter); + + public function hasParameter(string $parameter): bool; + + /** + * @return array<string, mixed> + */ + public function getParameters(): array; + + /** + * @param mixed|null $value + */ + public function setParameter(string $parameter, $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..69fa774db --- /dev/null +++ b/vendor/spomky-labs/otphp/src/ParameterTrait.php @@ -0,0 +1,196 @@ +<?php + +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; + +trait ParameterTrait +{ + /** + * @var array<string, mixed> + */ + private $parameters = []; + + /** + * @var string|null + */ + private $issuer; + + /** + * @var string|null + */ + private $label; + + /** + * @var bool + */ + private $issuer_included_as_parameter = true; + + /** + * @return array<string, mixed> + */ + public function getParameters(): array + { + $parameters = $this->parameters; + + if (null !== $this->getIssuer() && true === $this->isIssuerIncludedAsParameter()) { + $parameters['issuer'] = $this->getIssuer(); + } + + return $parameters; + } + + public function getSecret(): string + { + return $this->getParameter('secret'); + } + + private function setSecret(?string $secret): void + { + $this->setParameter('secret', $secret); + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): void + { + $this->setParameter('label', $label); + } + + public function getIssuer(): ?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 + { + return $this->getParameter('digits'); + } + + private function setDigits(int $digits): void + { + $this->setParameter('digits', $digits); + } + + public function getDigest(): string + { + return $this->getParameter('algorithm'); + } + + private function setDigest(string $digest): void + { + $this->setParameter('algorithm', $digest); + } + + public function hasParameter(string $parameter): bool + { + return \array_key_exists($parameter, $this->parameters); + } + + public function getParameter(string $parameter) + { + if ($this->hasParameter($parameter)) { + return $this->getParameters()[$parameter]; + } + + throw new InvalidArgumentException(sprintf('Parameter "%s" does not exist', $parameter)); + } + + public function setParameter(string $parameter, $value): void + { + $map = $this->getParameterMap(); + + if (true === \array_key_exists($parameter, $map)) { + $callback = $map[$parameter]; + $value = $callback($value); + } + + if (property_exists($this, $parameter)) { + $this->$parameter = $value; + } else { + $this->parameters[$parameter] = $value; + } + } + + /** + * @return array<string, mixed> + */ + protected function getParameterMap(): array + { + return [ + 'label' => function ($value) { + Assertion::false($this->hasColon($value), '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 { + $value = mb_strtolower($value); + Assertion::inArray($value, hash_algos(), sprintf('The "%s" digest is not supported.', $value)); + + return $value; + }, + 'digits' => function ($value): int { + Assertion::greaterThan($value, 0, 'Digits must be at least 1.'); + + return (int) $value; + }, + 'issuer' => function ($value) { + Assertion::false($this->hasColon($value), 'Issuer must not contain a colon.'); + + return $value; + }, + ]; + } + + private function hasColon(string $value): bool + { + $colons = [':', '%3A', '%3a']; + foreach ($colons as $colon) { + if (false !== mb_strpos($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..588b37f17 --- /dev/null +++ b/vendor/spomky-labs/otphp/src/TOTP.php @@ -0,0 +1,159 @@ +<?php + +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; + +final class TOTP extends OTP implements TOTPInterface +{ + protected function __construct(?string $secret, int $period, string $digest, int $digits, int $epoch = 0) + { + parent::__construct($secret, $digest, $digits); + $this->setPeriod($period); + $this->setEpoch($epoch); + } + + 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); + } + + protected function setPeriod(int $period): void + { + $this->setParameter('period', $period); + } + + public function getPeriod(): int + { + return $this->getParameter('period'); + } + + private function setEpoch(int $epoch): void + { + $this->setParameter('epoch', $epoch); + } + + public function getEpoch(): int + { + return $this->getParameter('epoch'); + } + + public function at(int $timestamp): string + { + return $this->generateOTP($this->timecode($timestamp)); + } + + public function now(): string + { + return $this->at(time()); + } + + /** + * If no timestamp is provided, the OTP is verified at the actual timestamp. + */ + public function verify(string $otp, ?int $timestamp = null, ?int $window = null): bool + { + $timestamp = $this->getTimestamp($timestamp); + + if (null === $window) { + return $this->compareOTP($this->at($timestamp), $otp); + } + + return $this->verifyOtpWithWindow($otp, $timestamp, $window); + } + + private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): 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); + + if ($valid) { + return true; + } + } + + return false; + } + + private function getTimestamp(?int $timestamp): int + { + $timestamp = $timestamp ?? time(); + Assertion::greaterOrEqualThan($timestamp, 0, 'Timestamp must be at least 0.'); + + return $timestamp; + } + + public function getProvisioningUri(): string + { + $params = []; + if (30 !== $this->getPeriod()) { + $params['period'] = $this->getPeriod(); + } + + if (0 !== $this->getEpoch()) { + $params['epoch'] = $this->getEpoch(); + } + + return $this->generateURI('totp', $params); + } + + private function timecode(int $timestamp): int + { + return (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod()); + } + + /** + * @return array<string, mixed> + */ + 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; + } + + /** + * @param array<string, mixed> $options + */ + protected function filterOptions(array &$options): void + { + parent::filterOptions($options); + + if (isset($options['epoch']) && 0 === $options['epoch']) { + unset($options['epoch']); + } + + ksort($options); + } +} diff --git a/vendor/spomky-labs/otphp/src/TOTPInterface.php b/vendor/spomky-labs/otphp/src/TOTPInterface.php new file mode 100644 index 000000000..a19fe7c0b --- /dev/null +++ b/vendor/spomky-labs/otphp/src/TOTPInterface.php @@ -0,0 +1,36 @@ +<?php + +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 +{ + /** + * Create a new TOTP object. + * + * If the secret is null, a random 64 bytes secret will be generated. + */ + public static function create(?string $secret = null, int $period = 30, string $digest = 'sha1', int $digits = 6): self; + + /** + * 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 getEpoch(): int; +} |