This commit is contained in:
Ty Clifford
2026-06-14 15:25:31 -04:00
parent 2f67ae718f
commit b7eaa81501
34 changed files with 3814 additions and 2 deletions
+77
View File
@@ -0,0 +1,77 @@
<?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;
}
}