‪TYPO3CMS  ‪main
Totp.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Base32\Base32;
23 
29 class ‪Totp
30 {
31  private const ‪ALLOWED_ALGOS = ['sha1', 'sha256', 'sha512'];
32  private const ‪MIN_LENGTH = 6;
33  private const ‪MAX_LENGTH = 8;
34 
35  public function ‪__construct(
36  protected readonly string $secret,
37  protected readonly string $algo = 'sha1',
38  protected readonly int $length = 6,
39  protected readonly int $step = 30,
40  protected readonly int $epoch = 0
41  ) {
42  if (!in_array($this->algo, self::ALLOWED_ALGOS, true)) {
43  throw new \InvalidArgumentException(
44  $this->algo . ' is not allowed. Allowed algos are: ' . implode(',', self::ALLOWED_ALGOS),
45  1611748791
46  );
47  }
48 
49  if ($this->length < self::MIN_LENGTH || $this->‪length > self::MAX_LENGTH) {
50  throw new \InvalidArgumentException(
51  $this->‪length . ' is not allowed as TOTP length. Must be between ' . self::MIN_LENGTH . ' and ' . self::MAX_LENGTH,
52  1611748792
53  );
54  }
55  }
56 
63  public function ‪generateTotp(int $counter): string
64  {
65  // Generate a 8-byte counter value (C) from the given counter input
66  $binary = [];
67  while ($counter !== 0) {
68  $binary[] = pack('C*', $counter);
69  $counter >>= 8;
70  }
71  // Implode and fill with NULL values
72  $binary = str_pad(implode(array_reverse($binary)), 8, "\000", STR_PAD_LEFT);
73  // Create a 20-byte hash string (HS) with given algo and decoded shared secret (K)
74  $hash = hash_hmac($this->algo, $binary, $this->‪getDecodedSecret());
75  // Convert hash into hex and generate an array with the decimal values of the hash
76  $hmac = [];
77  foreach (str_split($hash, 2) as $hex) {
78  $hmac[] = hexdec($hex);
79  }
80  // Generate a 4-byte string with dynamic truncation (DT)
81  $offset = $hmac[\count($hmac) - 1] & 0xf;
82  $bits = ((($hmac[$offset + 0] & 0x7f) << 24) | (($hmac[$offset + 1] & 0xff) << 16) | (($hmac[$offset + 2] & 0xff) << 8) | ($hmac[$offset + 3] & 0xff));
83  // Compute the TOTP value by reducing the bits modulo 10^Digits and filling it with zeros '0'
84  return str_pad((string)($bits % (10 ** $this->‪length)), $this->‪length, '0', STR_PAD_LEFT);
85  }
86 
93  public function ‪verifyTotp(string $totp, int $gracePeriod = null): bool
94  {
95  $counter = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp');
96 
97  // If no grace period is given, only check once
98  if ($gracePeriod === null) {
99  return $this->‪compare($totp, $this->‪getTimeCounter($counter));
100  }
101 
102  // Check the token within the given grace period till it can be verified or the grace period is exhausted
103  for ($i = 0; $i < $gracePeriod; ++$i) {
104  $next = $i * $this->step + $counter;
105  $prev = $counter - $i * $this->step;
106  if ($this->‪compare($totp, $this->‪getTimeCounter($next))
107  || $this->‪compare($totp, $this->‪getTimeCounter($prev))
108  ) {
109  return true;
110  }
111  }
112 
113  return false;
114  }
115 
119  public function ‪getTotpAuthUrl(string $issuer, string $account = '', array $additionalParameters = []): string
120  {
121  $parameters = [
122  'secret' => $this->secret,
123  'issuer' => htmlspecialchars($issuer),
124  ];
125 
126  // Common OTP applications expect the following parameters:
127  // - algo: sha1
128  // - period: 30 (in seconds)
129  // - digits 6
130  // - epoch: 0
131  // Only if we differ from these assumption, the exact values must be provided.
132  if ($this->algo !== 'sha1') {
133  $parameters['algorithm'] = $this->algo;
134  }
135  if ($this->step !== 30) {
136  $parameters['period'] = $this->step;
137  }
138  if ($this->‪length !== 6) {
139  $parameters['digits'] = $this->length;
140  }
141  if ($this->epoch !== 0) {
142  $parameters['epoch'] = $this->epoch;
143  }
144 
145  // Generate the otpauth URL by providing information like issuer and account
146  return sprintf(
147  'otpauth://totp/%s?%s',
148  rawurlencode($issuer . ($account !== '' ? ':' . $account : '')),
149  http_build_query(array_merge($parameters, $additionalParameters), '', '&', PHP_QUERY_RFC3986)
150  );
151  }
152 
160  protected function ‪compare(string $totp, int $counter): bool
161  {
162  return hash_equals($this->‪generateTotp($counter), $totp);
163  }
164 
168  protected function ‪getTimeCounter(int $timestamp): int
169  {
170  return (int)floor(($timestamp - $this->epoch) / $this->step);
171  }
172 
177  public static function ‪generateEncodedSecret(array $additionalAuthFactors = []): string
178  {
179  $secret = '';
180  $payload = implode($additionalAuthFactors);
181  // Prevent secrets with a trailing pad character since this will eventually break the QR-code feature
182  while ($secret === '' || str_contains($secret, '=')) {
183  // RFC 4226 (https://tools.ietf.org/html/rfc4226#section-4) suggests 160 bit TOTP secret keys
184  // HMAC-SHA1 based on static factors and a 160 bit HMAC-key lead again to 160 bits (20 bytes)
185  // base64-encoding (factor 1.6) 20 bytes lead to 32 uppercase characters
186  $secret = Base32::encode(hash_hmac('sha1', $payload, random_bytes(20), true));
187  }
188  return $secret;
189  }
190 
191  protected function ‪getDecodedSecret(): string
192  {
193  return Base32::decode($this->secret);
194  }
195 }
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\length
‪@ length
Definition: HashType.php:33
‪TYPO3\CMS\Core\Authentication\Mfa\Provider
Definition: RecoveryCodes.php:18
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\getTotpAuthUrl
‪getTotpAuthUrl(string $issuer, string $account='', array $additionalParameters=[])
Definition: Totp.php:119
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\getDecodedSecret
‪getDecodedSecret()
Definition: Totp.php:191
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\compare
‪compare(string $totp, int $counter)
Definition: Totp.php:160
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp
Definition: Totp.php:30
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\verifyTotp
‪verifyTotp(string $totp, int $gracePeriod=null)
Definition: Totp.php:93
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\MAX_LENGTH
‪const MAX_LENGTH
Definition: Totp.php:33
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\generateEncodedSecret
‪static generateEncodedSecret(array $additionalAuthFactors=[])
Definition: Totp.php:177
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\getTimeCounter
‪getTimeCounter(int $timestamp)
Definition: Totp.php:168
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\__construct
‪__construct(protected readonly string $secret, protected readonly string $algo='sha1', protected readonly int $length=6, protected readonly int $step=30, protected readonly int $epoch=0)
Definition: Totp.php:35
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\ALLOWED_ALGOS
‪const ALLOWED_ALGOS
Definition: Totp.php:31
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\generateTotp
‪string generateTotp(int $counter)
Definition: Totp.php:63
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\MIN_LENGTH
‪const MIN_LENGTH
Definition: Totp.php:32