asd
This commit is contained in:
commit
3e4a056b14
67 files changed
+13926
No files matched your search
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Two-factor authentication for admins: TOTP (RFC 6238, SHA-1, 6 digits, 30 s),
|
||||
* compatible with Google/Microsoft Authenticator. The shared secret is stored
|
||||
* encrypted (AES-256-GCM) with security.app_key, which lives outside the
|
||||
* document root. Recovery codes are stored as HMACs keyed with the same key.
|
||||
*/
|
||||
|
||||
const TOTP_PERIOD = 30;
|
||||
const TOTP_DIGITS = 6;
|
||||
const TOTP_WINDOW = 1; // accept one step before/after for clock drift
|
||||
const OTP_MAX_FAILS = 5; // wrong codes within LOGIN_WINDOW_MIN before the login is dropped
|
||||
const RECOVERY_CODE_COUNT = 10;
|
||||
const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
|
||||
function app_key(): string
|
||||
{
|
||||
$key = base64_decode((string)(security_config()['app_key'] ?? ''), true);
|
||||
if ($key === false || strlen($key) !== 32) {
|
||||
throw new RuntimeException('security.app_key belum diisi (32 byte base64); dibutuhkan untuk OTP.');
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
|
||||
function base32_encode(string $bin): string
|
||||
{
|
||||
$bits = '';
|
||||
foreach (str_split($bin) as $ch) {
|
||||
$bits .= str_pad(decbin(ord($ch)), 8, '0', STR_PAD_LEFT);
|
||||
}
|
||||
$out = '';
|
||||
foreach (str_split($bits, 5) as $chunk) {
|
||||
$out .= BASE32_ALPHABET[bindec(str_pad($chunk, 5, '0'))];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
function base32_decode(string $b32): string
|
||||
{
|
||||
$bits = '';
|
||||
foreach (str_split(strtoupper(preg_replace('/[^A-Za-z2-7]/', '', $b32))) as $ch) {
|
||||
$bits .= str_pad(decbin(strpos(BASE32_ALPHABET, $ch)), 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
$out = '';
|
||||
foreach (str_split($bits, 8) as $byte) {
|
||||
if (strlen($byte) === 8) {
|
||||
$out .= chr(bindec($byte));
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
function totp_new_secret(): string
|
||||
{
|
||||
return base32_encode(random_bytes(20)); // 160-bit, as recommended by RFC 4226
|
||||
}
|
||||
|
||||
function totp_code(string $secret, int $step): string
|
||||
{
|
||||
$hash = hash_hmac('sha1', pack('J', $step), base32_decode($secret), true);
|
||||
$offset = ord($hash[19]) & 0x0f;
|
||||
$bin = ((ord($hash[$offset]) & 0x7f) << 24) | (ord($hash[$offset + 1]) << 16)
|
||||
| (ord($hash[$offset + 2]) << 8) | ord($hash[$offset + 3]);
|
||||
return str_pad((string)($bin % (10 ** TOTP_DIGITS)), TOTP_DIGITS, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/** The matching time step, or null. Steps at or before $lastStep are refused (no replay). */
|
||||
function totp_verify(string $secret, string $code, ?int $lastStep): ?int
|
||||
{
|
||||
$code = preg_replace('/\D/', '', $code);
|
||||
if (strlen($code) !== TOTP_DIGITS) {
|
||||
return null;
|
||||
}
|
||||
$now = intdiv(time(), TOTP_PERIOD);
|
||||
for ($s = $now - TOTP_WINDOW; $s <= $now + TOTP_WINDOW; $s++) {
|
||||
if (($lastStep === null || $s > $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)];
|
||||
}
|
||||
Reference in new issue
Block a user