145 lines
4.6 KiB
PHP
145 lines
4.6 KiB
PHP
<?php
|
|
/**
|
|
* Pure PHP TOTP (RFC 6238) & 2FA Implementatie
|
|
* Volledig compatibel met Google Authenticator, Microsoft Authenticator & 1Password
|
|
*/
|
|
|
|
class TOTP {
|
|
private static string $base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
|
|
/**
|
|
* Genereert een willekeurige 16-teken Base32 geheime sleutel
|
|
*/
|
|
public static function generateSecret(int $length = 16): string {
|
|
$secret = '';
|
|
$bytes = random_bytes($length);
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$secret .= self::$base32Chars[ord($bytes[$i]) % 32];
|
|
}
|
|
return $secret;
|
|
}
|
|
|
|
/**
|
|
* Decodeert een Base32 string naar binaire data
|
|
*/
|
|
private static function base32Decode(string $b32): string {
|
|
$b32 = strtoupper(trim($b32));
|
|
$buffer = 0;
|
|
$bufferBits = 0;
|
|
$binary = '';
|
|
|
|
for ($i = 0; $i < strlen($b32); $i++) {
|
|
$char = $b32[$i];
|
|
$val = strpos(self::$base32Chars, $char);
|
|
if ($val === false) continue; // Negeer spaties/ongeldige tekens
|
|
|
|
$buffer = ($buffer << 5) | $val;
|
|
$bufferBits += 5;
|
|
|
|
if ($bufferBits >= 8) {
|
|
$bufferBits -= 8;
|
|
$binary .= chr(($buffer >> $bufferBits) & 0xFF);
|
|
}
|
|
}
|
|
|
|
return $binary;
|
|
}
|
|
|
|
/**
|
|
* Berekent de 6-cijferige TOTP code voor een bepaald tijdsinterval
|
|
*/
|
|
public static function getCode(string $secret, ?int $timestamp = null): string {
|
|
if ($timestamp === null) {
|
|
$timestamp = time();
|
|
}
|
|
|
|
$timeCounter = floor($timestamp / 30);
|
|
$binaryKey = self::base32Decode($secret);
|
|
|
|
// Converteer tijdsinterval naar 8-byte big-endian binaire string
|
|
$binaryTime = pack('N*', 0) . pack('N*', $timeCounter);
|
|
|
|
$hash = hash_hmac('sha1', $binaryTime, $binaryKey, true);
|
|
$offset = ord($hash[strlen($hash) - 1]) & 0x0F;
|
|
|
|
$binaryCode = (
|
|
((ord($hash[$offset]) & 0x7F) << 24) |
|
|
((ord($hash[$offset + 1]) & 0xFF) << 16) |
|
|
((ord($hash[$offset + 2]) & 0xFF) << 8) |
|
|
(ord($hash[$offset + 3]) & 0xFF)
|
|
);
|
|
|
|
$otp = $binaryCode % 1000000;
|
|
return str_pad((string)$otp, 6, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
/**
|
|
* Verifieert een ingevoerde 6-cijferige 2FA code (met 1 tijdstap speling naar voor/achter)
|
|
*/
|
|
public static function verifyCode(string $secret, string $userInputCode, int $window = 1): bool {
|
|
$userInputCode = trim(str_replace(' ', '', $userInputCode));
|
|
if (strlen($userInputCode) !== 6 || !ctype_digit($userInputCode)) {
|
|
return false;
|
|
}
|
|
|
|
$currentTime = time();
|
|
for ($i = -$window; $i <= $window; $i++) {
|
|
$calcCode = self::getCode($secret, $currentTime + ($i * 30));
|
|
if (hash_equals($calcCode, $userInputCode)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Maakt de otpauth:// URL aan voor authenticator apps
|
|
*/
|
|
public static function getOtpAuthUrl(string $email, string $secret, string $issuer = 'Fociz'): string {
|
|
return sprintf(
|
|
'otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30',
|
|
rawurlencode($issuer),
|
|
rawurlencode($email),
|
|
$secret,
|
|
rawurlencode($issuer)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Genereert een veilige client-side QR code (geen externe API calls meer ter bescherming van TOTP secrets)
|
|
*/
|
|
public static function getQrCodeUrl(string $otpAuthUrl, int $size = 220): string {
|
|
// Externe API (quickchart.io) verwijderd om te voorkomen dat 2FA secrets uitlekken naar derden.
|
|
// QR rendering gebeurt nu 100% lokaal via /assets/js/qrcode.min.js in de browser.
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Genereert een lijst met eenmalige herstelcodes
|
|
*/
|
|
public static function generateBackupCodes(int $count = 8): array {
|
|
$codes = [];
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$bytes = bin2hex(random_bytes(4));
|
|
$codes[] = strtoupper(substr($bytes, 0, 4) . '-' . substr($bytes, 4, 4));
|
|
}
|
|
return $codes;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Globale helper functies voor 2FA TOTP verificatie
|
|
*/
|
|
if (!function_exists('verify_totp')) {
|
|
function verify_totp(string $secret, string $userInputCode, int $window = 1): bool {
|
|
return TOTP::verifyCode($secret, $userInputCode, $window);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('get_totp_code')) {
|
|
function get_totp_code(string $secret, ?int $timestamp = null): string {
|
|
return TOTP::getCode($secret, $timestamp);
|
|
}
|
|
}
|