<?php
// =============================================================================
// includes/totp.php — RFC 6238 / RFC 4226 implementation (HOTP/TOTP, SHA-1).
// Compatible with Google Authenticator, Authy, 1Password, Microsoft Authenticator.
// =============================================================================

function totp_random_secret($bytes = 20) {
    return totp_base32_encode(random_bytes($bytes));
}

function totp_base32_encode($raw) {
    $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    $out = '';
    $bits = 0;
    $val = 0;
    for ($i = 0, $n = strlen($raw); $i < $n; $i++) {
        $val = ($val << 8) | ord($raw[$i]);
        $bits += 8;
        while ($bits >= 5) {
            $out .= $alphabet[($val >> ($bits - 5)) & 0x1f];
            $bits -= 5;
        }
    }
    if ($bits > 0) {
        $out .= $alphabet[($val << (5 - $bits)) & 0x1f];
    }
    return $out;
}

function totp_base32_decode($s) {
    $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    $s = strtoupper(preg_replace('/[^A-Z2-7]/', '', $s));
    $out = '';
    $bits = 0;
    $val = 0;
    for ($i = 0, $n = strlen($s); $i < $n; $i++) {
        $idx = strpos($alphabet, $s[$i]);
        if ($idx === false) { continue; }
        $val = ($val << 5) | $idx;
        $bits += 5;
        if ($bits >= 8) {
            $bits -= 8;
            $out .= chr(($val >> $bits) & 0xff);
        }
    }
    return $out;
}

function totp_code($secret_b32, $time = null, $period = 30, $digits = 6) {
    if ($time === null) { $time = time(); }
    $counter = intdiv($time, $period);
    $key = totp_base32_decode($secret_b32);
    $bin = pack('N*', 0, $counter);
    $hash = hash_hmac('sha1', $bin, $key, true);
    $offset = ord(substr($hash, -1)) & 0x0f;
    $part = substr($hash, $offset, 4);
    $val = unpack('N', $part)[1] & 0x7fffffff;
    $code = $val % pow(10, $digits);
    return str_pad((string)$code, $digits, '0', STR_PAD_LEFT);
}

function totp_verify($secret_b32, $code, $window = 1, $period = 30, $digits = 6) {
    if (!preg_match('/^\d{' . $digits . '}$/', (string)$code)) { return false; }
    $now = time();
    for ($w = -$window; $w <= $window; $w++) {
        if (hash_equals(totp_code($secret_b32, $now + ($w * $period), $period, $digits), (string)$code)) {
            return true;
        }
    }
    return false;
}

function totp_otpauth_url($secret_b32, $account, $issuer) {
    $label = rawurlencode($issuer . ':' . $account);
    $qs = http_build_query([
        'secret' => $secret_b32,
        'issuer' => $issuer,
        'algorithm' => 'SHA1',
        'digits' => 6,
        'period' => 30,
    ]);
    return 'otpauth://totp/' . $label . '?' . $qs;
}
