78 lines
2.4 KiB
PHP
78 lines
2.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Budget;
|
|
|
|
final class Totp
|
|
{
|
|
private const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
|
|
public static function generateSecret(int $length = 24): string
|
|
{
|
|
$secret = '';
|
|
for ($index = 0; $index < $length; $index++) {
|
|
$secret .= self::ALPHABET[random_int(0, 31)];
|
|
}
|
|
return $secret;
|
|
}
|
|
|
|
public static function verify(string $secret, string $code, int $window = 1): bool
|
|
{
|
|
$code = preg_replace('/\D/', '', $code) ?? '';
|
|
if (strlen($code) !== 6) {
|
|
return false;
|
|
}
|
|
$counter = (int) floor(time() / 30);
|
|
for ($offset = -$window; $offset <= $window; $offset++) {
|
|
if (hash_equals(self::code($secret, $counter + $offset), $code)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static function uri(string $secret, string $email, string $issuer): string
|
|
{
|
|
$label = rawurlencode($issuer . ':' . $email);
|
|
return sprintf(
|
|
'otpauth://totp/%s?secret=%s&issuer=%s&digits=6&period=30',
|
|
$label,
|
|
rawurlencode($secret),
|
|
rawurlencode($issuer)
|
|
);
|
|
}
|
|
|
|
private static function code(string $secret, int $counter): string
|
|
{
|
|
$key = self::decodeBase32($secret);
|
|
$binaryCounter = pack('N2', 0, $counter);
|
|
$hash = hash_hmac('sha1', $binaryCounter, $key, true);
|
|
$offset = ord($hash[19]) & 0x0f;
|
|
$value = (
|
|
((ord($hash[$offset]) & 0x7f) << 24) |
|
|
((ord($hash[$offset + 1]) & 0xff) << 16) |
|
|
((ord($hash[$offset + 2]) & 0xff) << 8) |
|
|
(ord($hash[$offset + 3]) & 0xff)
|
|
) % 1000000;
|
|
return str_pad((string) $value, 6, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
private static function decodeBase32(string $secret): string
|
|
{
|
|
$secret = strtoupper(preg_replace('/[^A-Z2-7]/i', '', $secret) ?? '');
|
|
$bits = '';
|
|
foreach (str_split($secret) as $character) {
|
|
$position = strpos(self::ALPHABET, $character);
|
|
$bits .= str_pad(decbin($position === false ? 0 : $position), 5, '0', STR_PAD_LEFT);
|
|
}
|
|
$output = '';
|
|
foreach (str_split($bits, 8) as $byte) {
|
|
if (strlen($byte) === 8) {
|
|
$output .= chr(bindec($byte));
|
|
}
|
|
}
|
|
return $output;
|
|
}
|
|
}
|