summaryrefslogtreecommitdiffstats
path: root/vendor/sonata-project/google-authenticator/src/GoogleAuthenticator.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/sonata-project/google-authenticator/src/GoogleAuthenticator.php')
-rw-r--r--vendor/sonata-project/google-authenticator/src/GoogleAuthenticator.php178
1 files changed, 178 insertions, 0 deletions
diff --git a/vendor/sonata-project/google-authenticator/src/GoogleAuthenticator.php b/vendor/sonata-project/google-authenticator/src/GoogleAuthenticator.php
new file mode 100644
index 0000000..52f0598
--- /dev/null
+++ b/vendor/sonata-project/google-authenticator/src/GoogleAuthenticator.php
@@ -0,0 +1,178 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the Sonata Project package.
+ *
+ * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Sonata\GoogleAuthenticator;
+
+/**
+ * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
+ */
+final class GoogleAuthenticator implements GoogleAuthenticatorInterface
+{
+ /**
+ * @var int
+ */
+ private $passCodeLength;
+
+ /**
+ * @var int
+ */
+ private $secretLength;
+
+ /**
+ * @var int
+ */
+ private $pinModulo;
+
+ /**
+ * @var \DateTimeInterface
+ */
+ private $instanceTime;
+
+ /**
+ * @var int
+ */
+ private $codePeriod;
+
+ /**
+ * @var int
+ */
+ private $periodSize = 30;
+
+ public function __construct(int $passCodeLength = 6, int $secretLength = 10, ?\DateTimeInterface $instanceTime = null, int $codePeriod = 30)
+ {
+ /*
+ * codePeriod is the duration in seconds that the code is valid.
+ * periodSize is the length of a period to calculate periods since Unix epoch.
+ * periodSize cannot be larger than the codePeriod.
+ */
+
+ $this->passCodeLength = $passCodeLength;
+ $this->secretLength = $secretLength;
+ $this->codePeriod = $codePeriod;
+ $this->periodSize = $codePeriod < $this->periodSize ? $codePeriod : $this->periodSize;
+ $this->pinModulo = 10 ** $passCodeLength;
+ $this->instanceTime = $instanceTime ?? new \DateTimeImmutable();
+ }
+
+ /**
+ * @param string $secret
+ * @param string $code
+ * @param int $discrepancy
+ */
+ public function checkCode($secret, $code, $discrepancy = 1): bool
+ {
+ /**
+ * Discrepancy is the factor of periodSize ($discrepancy * $periodSize) allowed on either side of the
+ * given codePeriod. For example, if a code with codePeriod = 60 is generated at 10:00:00, a discrepancy
+ * of 1 will allow a periodSize of 30 seconds on either side of the codePeriod resulting in a valid code
+ * from 09:59:30 to 10:00:29.
+ *
+ * The result of each comparison is stored as a timestamp here instead of using a guard clause
+ * (https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html). This is to implement
+ * constant time comparison to make side-channel attacks harder. See
+ * https://cryptocoding.net/index.php/Coding_rules#Compare_secret_strings_in_constant_time for details.
+ * Each comparison uses hash_equals() instead of an operator to implement constant time equality comparison
+ * for each code.
+ */
+ $periods = floor($this->codePeriod / $this->periodSize);
+
+ $result = 0;
+ for ($i = -$discrepancy; $i < $periods + $discrepancy; ++$i) {
+ $dateTime = new \DateTimeImmutable('@'.($this->instanceTime->getTimestamp() - ($i * $this->periodSize)));
+ $result = hash_equals($this->getCode($secret, $dateTime), $code) ? $dateTime->getTimestamp() : $result;
+ }
+
+ return $result > 0;
+ }
+
+ /**
+ * NEXT_MAJOR: add the interface typehint to $time and remove deprecation.
+ *
+ * @param string $secret
+ * @param float|string|int|\DateTimeInterface|null $time
+ */
+ public function getCode($secret, /* \DateTimeInterface */ $time = null): string
+ {
+ if (null === $time) {
+ $time = $this->instanceTime;
+ }
+
+ if ($time instanceof \DateTimeInterface) {
+ $timeForCode = floor($time->getTimestamp() / $this->periodSize);
+ } else {
+ @trigger_error(
+ 'Passing anything other than null or a DateTimeInterface to $time is deprecated as of 2.0 '.
+ 'and will not be possible as of 3.0.',
+ \E_USER_DEPRECATED
+ );
+ $timeForCode = $time;
+ }
+
+ $base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', true, true);
+ $secret = $base32->decode($secret);
+
+ $timeForCode = str_pad(pack('N', $timeForCode), 8, \chr(0), \STR_PAD_LEFT);
+
+ $hash = hash_hmac('sha1', $timeForCode, $secret, true);
+ $offset = \ord(substr($hash, -1));
+ $offset &= 0xF;
+
+ $truncatedHash = $this->hashToInt($hash, $offset) & 0x7FFFFFFF;
+
+ return str_pad((string) ($truncatedHash % $this->pinModulo), $this->passCodeLength, '0', \STR_PAD_LEFT);
+ }
+
+ /**
+ * NEXT_MAJOR: Remove this method.
+ *
+ * @param string $user
+ * @param string $hostname
+ * @param string $secret
+ *
+ * @deprecated deprecated as of 2.1 and will be removed in 3.0. Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead.
+ */
+ public function getUrl($user, $hostname, $secret): string
+ {
+ @trigger_error(sprintf(
+ 'Using %s() is deprecated as of 2.1 and will be removed in 3.0. '.
+ 'Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead.',
+ __METHOD__
+ ), \E_USER_DEPRECATED);
+
+ $issuer = \func_get_args()[3] ?? null;
+ $accountName = sprintf('%s@%s', $user, $hostname);
+
+ // manually concat the issuer to avoid a change in URL
+ $url = GoogleQrUrl::generate($accountName, $secret);
+
+ if ($issuer) {
+ $url .= '%26issuer%3D'.$issuer;
+ }
+
+ return $url;
+ }
+
+ public function generateSecret(): string
+ {
+ return (new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', true, true))
+ ->encode(random_bytes($this->secretLength));
+ }
+
+ private function hashToInt(string $bytes, int $start): int
+ {
+ return unpack('N', substr(substr($bytes, $start), 0, 4))[1];
+ }
+}
+
+// NEXT_MAJOR: Remove class alias
+class_alias('Sonata\GoogleAuthenticator\GoogleAuthenticator', 'Google\Authenticator\GoogleAuthenticator', false);