vendor/sonata-project/google-authenticator/src/GoogleAuthenticator.php line 144

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of the Sonata Project package.
  5.  *
  6.  * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace Sonata\GoogleAuthenticator;
  12. /**
  13.  * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
  14.  */
  15. final class GoogleAuthenticator implements GoogleAuthenticatorInterface
  16. {
  17.     /**
  18.      * @var int
  19.      */
  20.     private $passCodeLength;
  21.     /**
  22.      * @var int
  23.      */
  24.     private $secretLength;
  25.     /**
  26.      * @var int
  27.      */
  28.     private $pinModulo;
  29.     /**
  30.      * @var \DateTimeInterface
  31.      */
  32.     private $instanceTime;
  33.     /**
  34.      * @var int
  35.      */
  36.     private $codePeriod;
  37.     /**
  38.      * @var int
  39.      */
  40.     private $periodSize 30;
  41.     public function __construct(int $passCodeLength 6int $secretLength 10, ?\DateTimeInterface $instanceTime nullint $codePeriod 30)
  42.     {
  43.         /*
  44.          * codePeriod is the duration in seconds that the code is valid.
  45.          * periodSize is the length of a period to calculate periods since Unix epoch.
  46.          * periodSize cannot be larger than the codePeriod.
  47.          */
  48.         $this->passCodeLength $passCodeLength;
  49.         $this->secretLength $secretLength;
  50.         $this->codePeriod $codePeriod;
  51.         $this->periodSize $codePeriod $this->periodSize $codePeriod $this->periodSize;
  52.         $this->pinModulo 10 ** $passCodeLength;
  53.         $this->instanceTime $instanceTime ?? new \DateTimeImmutable();
  54.     }
  55.     /**
  56.      * @param string $secret
  57.      * @param string $code
  58.      * @param int    $discrepancy
  59.      */
  60.     public function checkCode($secret$code$discrepancy 1): bool
  61.     {
  62.         /**
  63.          * Discrepancy is the factor of periodSize ($discrepancy * $periodSize) allowed on either side of the
  64.          * given codePeriod. For example, if a code with codePeriod = 60 is generated at 10:00:00, a discrepancy
  65.          * of 1 will allow a periodSize of 30 seconds on either side of the codePeriod resulting in a valid code
  66.          * from 09:59:30 to 10:00:29.
  67.          *
  68.          * The result of each comparison is stored as a timestamp here instead of using a guard clause
  69.          * (https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html). This is to implement
  70.          * constant time comparison to make side-channel attacks harder. See
  71.          * https://cryptocoding.net/index.php/Coding_rules#Compare_secret_strings_in_constant_time for details.
  72.          * Each comparison uses hash_equals() instead of an operator to implement constant time equality comparison
  73.          * for each code.
  74.          */
  75.         $periods floor($this->codePeriod $this->periodSize);
  76.         $result 0;
  77.         for ($i = -$discrepancy$i $periods $discrepancy; ++$i) {
  78.             $dateTime = new \DateTimeImmutable('@'.($this->instanceTime->getTimestamp() - ($i $this->periodSize)));
  79.             $result hash_equals($this->getCode($secret$dateTime), $code) ? $dateTime->getTimestamp() : $result;
  80.         }
  81.         return $result 0;
  82.     }
  83.     /**
  84.      * NEXT_MAJOR: add the interface typehint to $time and remove deprecation.
  85.      *
  86.      * @param string                                   $secret
  87.      * @param float|string|int|\DateTimeInterface|null $time
  88.      */
  89.     public function getCode($secret/* \DateTimeInterface */ $time null): string
  90.     {
  91.         if (null === $time) {
  92.             $time $this->instanceTime;
  93.         }
  94.         if ($time instanceof \DateTimeInterface) {
  95.             $timeForCode floor($time->getTimestamp() / $this->periodSize);
  96.         } else {
  97.             @trigger_error(
  98.                 'Passing anything other than null or a DateTimeInterface to $time is deprecated as of 2.0 '.
  99.                 'and will not be possible as of 3.0.',
  100.                 \E_USER_DEPRECATED
  101.             );
  102.             $timeForCode $time;
  103.         }
  104.         $base32 = new FixedBitNotation(5'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'truetrue);
  105.         $secret $base32->decode($secret);
  106.         $timeForCode str_pad(pack('N'$timeForCode), 8, \chr(0), \STR_PAD_LEFT);
  107.         $hash hash_hmac('sha1'$timeForCode$secrettrue);
  108.         $offset = \ord(substr($hash, -1));
  109.         $offset &= 0xF;
  110.         $truncatedHash $this->hashToInt($hash$offset) & 0x7FFFFFFF;
  111.         return str_pad((string) ($truncatedHash $this->pinModulo), $this->passCodeLength'0', \STR_PAD_LEFT);
  112.     }
  113.     /**
  114.      * NEXT_MAJOR: Remove this method.
  115.      *
  116.      * @param string $user
  117.      * @param string $hostname
  118.      * @param string $secret
  119.      *
  120.      * @deprecated deprecated as of 2.1 and will be removed in 3.0. Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead.
  121.      */
  122.     public function getUrl($user$hostname$secret): string
  123.     {
  124.         @trigger_error(sprintf(
  125.             'Using %s() is deprecated as of 2.1 and will be removed in 3.0. '.
  126.             'Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead.',
  127.             __METHOD__
  128.         ), \E_USER_DEPRECATED);
  129.         $issuer = \func_get_args()[3] ?? null;
  130.         $accountName sprintf('%s@%s'$user$hostname);
  131.         // manually concat the issuer to avoid a change in URL
  132.         $url GoogleQrUrl::generate($accountName$secret);
  133.         if ($issuer) {
  134.             $url .= '%26issuer%3D'.$issuer;
  135.         }
  136.         return $url;
  137.     }
  138.     public function generateSecret(): string
  139.     {
  140.         return (new FixedBitNotation(5'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'truetrue))
  141.             ->encode(random_bytes($this->secretLength));
  142.     }
  143.     private function hashToInt(string $bytesint $start): int
  144.     {
  145.         return unpack('N'substr(substr($bytes$start), 04))[1];
  146.     }
  147. }
  148. // NEXT_MAJOR: Remove class alias
  149. class_alias('Sonata\GoogleAuthenticator\GoogleAuthenticator''Google\Authenticator\GoogleAuthenticator'false);