| Current Path : /var/www/surf/TYPO3/vendor/typo3/cms-core/Classes/Authentication/Mfa/Provider/ |
| Current File : //var/www/surf/TYPO3/vendor/typo3/cms-core/Classes/Authentication/Mfa/Provider/Totp.php |
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Authentication\Mfa\Provider;
use Base32\Base32;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Time-based one-time password (TOTP) implementation according to rfc6238
*
* @internal should only be used by the TYPO3 Core
*/
class Totp
{
private const ALLOWED_ALGOS = ['sha1', 'sha256', 'sha512'];
private const MIN_LENGTH = 6;
private const MAX_LENGTH = 8;
protected string $secret;
protected string $algo;
protected int $length;
protected int $step;
protected int $epoch;
public function __construct(
string $secret,
string $algo = 'sha1',
int $length = 6,
int $step = 30,
int $epoch = 0
) {
$this->secret = $secret;
$this->step = $step;
$this->epoch = $epoch;
if (!in_array($algo, self::ALLOWED_ALGOS, true)) {
throw new \InvalidArgumentException(
$algo . ' is not allowed. Allowed algos are: ' . implode(',', self::ALLOWED_ALGOS),
1611748791
);
}
$this->algo = $algo;
if ($length < self::MIN_LENGTH || $length > self::MAX_LENGTH) {
throw new \InvalidArgumentException(
$length . ' is not allowed as TOTP length. Must be between ' . self::MIN_LENGTH . ' and ' . self::MAX_LENGTH,
1611748792
);
}
$this->length = $length;
}
/**
* Generate a time-based one-time password for the given counter according to rfc4226
*
* @param int $counter A timestamp (counter) according to rfc6238
* @return string The generated TOTP
*/
public function generateTotp(int $counter): string
{
// Generate a 8-byte counter value (C) from the given counter input
$binary = [];
while ($counter !== 0) {
$binary[] = pack('C*', $counter);
$counter >>= 8;
}
// Implode and fill with NULL values
$binary = str_pad(implode(array_reverse($binary)), 8, "\000", STR_PAD_LEFT);
// Create a 20-byte hash string (HS) with given algo and decoded shared secret (K)
$hash = hash_hmac($this->algo, $binary, $this->getDecodedSecret());
// Convert hash into hex and generate an array with the decimal values of the hash
$hmac = [];
foreach (str_split($hash, 2) as $hex) {
$hmac[] = hexdec($hex);
}
// Generate a 4-byte string with dynamic truncation (DT)
$offset = $hmac[\count($hmac) - 1] & 0xf;
$bits = ((($hmac[$offset + 0] & 0x7f) << 24) | (($hmac[$offset + 1] & 0xff) << 16) | (($hmac[$offset + 2] & 0xff) << 8) | ($hmac[$offset + 3] & 0xff));
// Compute the TOTP value by reducing the bits modulo 10^Digits and filling it with zeros '0'
return str_pad((string)($bits % (10 ** $this->length)), $this->length, '0', STR_PAD_LEFT);
}
/**
* Verify the given time-based one-time password
*
* @param string $totp The time-based one-time password to be verified
* @param int|null $gracePeriod The grace period for the TOTP +- (mainly to circumvent transmission delays)
*/
public function verifyTotp(string $totp, int $gracePeriod = null): bool
{
$counter = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp');
// If no grace period is given, only check once
if ($gracePeriod === null) {
return $this->compare($totp, $this->getTimeCounter($counter));
}
// Check the token within the given grace period till it can be verified or the grace period is exhausted
for ($i = 0; $i < $gracePeriod; ++$i) {
$next = $i * $this->step + $counter;
$prev = $counter - $i * $this->step;
if ($this->compare($totp, $this->getTimeCounter($next))
|| $this->compare($totp, $this->getTimeCounter($prev))
) {
return true;
}
}
return false;
}
/**
* Generate and return the otpauth URL for TOTP
*/
public function getTotpAuthUrl(string $issuer, string $account = '', array $additionalParameters = []): string
{
$parameters = [
'secret' => $this->secret,
'issuer' => htmlspecialchars($issuer),
];
// Common OTP applications expect the following parameters:
// - algo: sha1
// - period: 30 (in seconds)
// - digits 6
// - epoch: 0
// Only if we differ from these assumption, the exact values must be provided.
if ($this->algo !== 'sha1') {
$parameters['algorithm'] = $this->algo;
}
if ($this->step !== 30) {
$parameters['period'] = $this->step;
}
if ($this->length !== 6) {
$parameters['digits'] = $this->length;
}
if ($this->epoch !== 0) {
$parameters['epoch'] = $this->epoch;
}
// Generate the otpauth URL by providing information like issuer and account
return sprintf(
'otpauth://totp/%s?%s',
rawurlencode($issuer . ($account !== '' ? ':' . $account : '')),
http_build_query(array_merge($parameters, $additionalParameters), '', '&', PHP_QUERY_RFC3986)
);
}
/**
* Compare given time-based one-time password with a time-based one-time
* password generated from the known $counter (the moving factor).
*
* @param string $totp The time-based one-time password to verify
* @param int $counter The counter value, the moving factor
*/
protected function compare(string $totp, int $counter): bool
{
return hash_equals($this->generateTotp($counter), $totp);
}
/**
* Generate the counter value (moving factor) from the given timestamp
*/
protected function getTimeCounter(int $timestamp): int
{
return (int)floor(($timestamp - $this->epoch) / $this->step);
}
/**
* Generate the shared secret (K) by using a random and applying
* additional authentication factors like username or email address.
*/
public static function generateEncodedSecret(array $additionalAuthFactors = []): string
{
$secret = '';
$payload = implode($additionalAuthFactors);
// Prevent secrets with a trailing pad character since this will eventually break the QR-code feature
while ($secret === '' || str_contains($secret, '=')) {
// RFC 4226 (https://tools.ietf.org/html/rfc4226#section-4) suggests 160 bit TOTP secret keys
// HMAC-SHA1 based on static factors and a 160 bit HMAC-key lead again to 160 bits (20 bytes)
// base64-encoding (factor 1.6) 20 bytes lead to 32 uppercase characters
$secret = Base32::encode(hash_hmac('sha1', $payload, random_bytes(20), true));
}
return $secret;
}
protected function getDecodedSecret(): string
{
return Base32::decode($this->secret);
}
}