$lastStep) && hash_equals(totp_code($secret, $s), $code)) { return $s; } } return null; } function totp_uri(string $secret, string $username): string { $label = rawurlencode('SERUBI Bank Papua:' . $username); return "otpauth://totp/$label?secret=$secret&issuer=" . rawurlencode('SERUBI Bank Papua') . '&period=' . TOTP_PERIOD . '&digits=' . TOTP_DIGITS; } function secret_encrypt(string $plain): string { $iv = random_bytes(12); $tag = ''; $ct = openssl_encrypt($plain, 'aes-256-gcm', app_key(), OPENSSL_RAW_DATA, $iv, $tag); return base64_encode($iv . $tag . $ct); } function secret_decrypt(string $stored): string { $raw = (string)base64_decode($stored, true); $plain = openssl_decrypt(substr($raw, 28), 'aes-256-gcm', app_key(), OPENSSL_RAW_DATA, substr($raw, 0, 12), substr($raw, 12, 16)); if ($plain === false) { throw new RuntimeException('OTP secret tidak dapat dibuka (app_key berubah?).'); } return $plain; } function recovery_hash(string $code): string { return hash_hmac('sha256', strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $code)), app_key()); } /** Fresh one-time codes like "K7QD-M2XP": [plain codes to show once, hashes to store]. */ function recovery_codes_new(): array { $plain = []; for ($i = 0; $i < RECOVERY_CODE_COUNT; $i++) { $c = substr(base32_encode(random_bytes(5)), 0, 8); $plain[] = substr($c, 0, 4) . '-' . substr($c, 4); } return [$plain, array_map('recovery_hash', $plain)]; }