v1.0.0.1
StocketBase
- <?php
-
- declare(strict_types=1);
-
- final class Totp
- {
- private const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
- private const PERIOD = 30;
-
- public static function generateSecret(): string
- {
- return self::base32Encode(random_bytes(20));
- }
-
- public static function verify(string $secret, string $input, ?int $lastStep): ?int
- {
- $input = (string) preg_replace('/\s+/', '', $input);
- if (preg_match('/^\d{6}$/', $input) !== 1) {
- return null;
- }
-
- $current = intdiv(time(), self::PERIOD);
-
- for ($offset = -1; $offset <= 1; $offset++) {
- $step = $current + $offset;
- if ($lastStep !== null && $step <= $lastStep) {
- continue;
- }
-
- if (hash_equals(self::code($secret, $step), $input)) {
- return $step;
- }
- }
-
- return null;
- }
-
- public static function uri(string $secret, string $account, string $issuer): string
- {
- return 'otpauth://totp/' . rawurlencode($issuer . ':' . $account)
- . '?secret=' . $secret
- . '&issuer=' . rawurlencode($issuer)
- . '&algorithm=SHA1&digits=6&period=' . self::PERIOD;
- }
-
- public static function formatSecret(string $secret): string
- {
- return trim(chunk_split($secret, 4, ' '));
- }
-
- private static function code(string $secret, int $step): string
- {
- $key = self::base32Decode($secret);
- $hash = hash_hmac('sha1', pack('J', $step), $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);
-
- return str_pad((string) ($value % 1000000), 6, '0', STR_PAD_LEFT);
- }
-
- private static function base32Encode(string $data): string
- {
- $bits = '';
- foreach (str_split($data) as $char) {
- $bits .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
- }
-
- $encoded = '';
- foreach (str_split($bits, 5) as $chunk) {
- $encoded .= self::ALPHABET[bindec(str_pad($chunk, 5, '0', STR_PAD_RIGHT))];
- }
-
- return $encoded;
- }
-
- private static function base32Decode(string $secret): string
- {
- $bits = '';
- foreach (str_split(strtoupper((string) preg_replace('/[^A-Za-z2-7]/', '', $secret))) as $char) {
- $position = strpos(self::ALPHABET, $char);
- if ($position !== false) {
- $bits .= str_pad(decbin($position), 5, '0', STR_PAD_LEFT);
- }
- }
-
- $decoded = '';
- foreach (str_split($bits, 8) as $byte) {
- if (strlen($byte) === 8) {
- $decoded .= chr((int) bindec($byte));
- }
- }
-
- return $decoded;
- }
- }
-