Fixes 2fa classes

main
Yasen Pramatarov 2025-04-08 10:27:52 +03:00
parent ac1581e8de
commit 7b7e44faf2
2 changed files with 176 additions and 125 deletions

View File

@ -8,11 +8,12 @@
*/ */
class TwoFactorAuthentication { class TwoFactorAuthentication {
private $db; private $db;
private $secretLength = 32; private $secretLength = 20; // 160 bits for SHA1
private $period = 30; // Time step in seconds private $period = 30; // Time step in seconds (T0)
private $digits = 6; // Number of digits in TOTP code private $digits = 6; // Number of digits in TOTP code
private $algorithm = 'sha1'; private $algorithm = 'sha1'; // HMAC algorithm
private $issuer = 'Jilo'; private $issuer = 'TotalMeet';
private $window = 1; // Time window of 1 step before/after
/** /**
* Constructor * Constructor
@ -31,9 +32,11 @@ class TwoFactorAuthentication {
* Enable 2FA for a user * Enable 2FA for a user
* *
* @param int $userId User ID * @param int $userId User ID
* @return array Array containing success status and data (secret, QR code URL) * @param string $secret Secret key (base32 encoded)
* @param string $code Verification code
* @return bool True if enabled successfully
*/ */
public function enable($userId) { public function enable($userId, $secret = null, $code = null) {
try { try {
// Check if 2FA is already enabled // Check if 2FA is already enabled
$stmt = $this->db->prepare('SELECT enabled FROM user_2fa WHERE user_id = ?'); $stmt = $this->db->prepare('SELECT enabled FROM user_2fa WHERE user_id = ?');
@ -41,58 +44,81 @@ class TwoFactorAuthentication {
$existing = $stmt->fetch(PDO::FETCH_ASSOC); $existing = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existing && $existing['enabled']) { if ($existing && $existing['enabled']) {
return ['success' => false, 'message' => '2FA is already enabled']; return false;
} }
// Generate secret key // If no secret provided, generate one and return setup data
$secret = $this->generateSecret(); if ($secret === null) {
// Generate secret key
$secret = $this->generateSecret();
// Get user's username for the QR code // Get user's username for the QR code
$stmt = $this->db->prepare('SELECT username FROM users WHERE id = ?'); $stmt = $this->db->prepare('SELECT username FROM user WHERE id = ?');
$stmt->execute([$userId]); $stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC); $user = $stmt->fetch(PDO::FETCH_ASSOC);
// Generate backup codes // Generate backup codes
$backupCodes = $this->generateBackupCodes(); $backupCodes = $this->generateBackupCodes();
// Store in database // Store in database without enabling yet
$this->db->beginTransaction(); $this->db->beginTransaction();
$stmt = $this->db->prepare(' $stmt = $this->db->prepare('
INSERT INTO user_2fa (user_id, secret_key, backup_codes, enabled, created_at) INSERT INTO user_2fa (user_id, secret_key, backup_codes, enabled, created_at)
VALUES (?, ?, ?, 0, NOW()) VALUES (?, ?, ?, 0, NOW())
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
secret_key = VALUES(secret_key), secret_key = VALUES(secret_key),
backup_codes = VALUES(backup_codes), backup_codes = VALUES(backup_codes),
enabled = VALUES(enabled), enabled = VALUES(enabled),
created_at = VALUES(created_at) created_at = VALUES(created_at)
'); ');
$stmt->execute([ $stmt->execute([
$userId, $userId,
$secret, $secret,
json_encode($backupCodes) json_encode($backupCodes)
]); ]);
$this->db->commit(); $this->db->commit();
// Generate otpauth URL for QR code // Generate otpauth URL for QR code
$otpauthUrl = $this->generateOtpauthUrl($user['username'], $secret); $otpauthUrl = $this->generateOtpauthUrl($user['username'], $secret);
return [ return [
'success' => true, 'success' => true,
'data' => [ 'data' => [
'secret' => $secret, 'secret' => $secret,
'otpauthUrl' => $otpauthUrl, 'otpauthUrl' => $otpauthUrl,
'backupCodes' => $backupCodes 'backupCodes' => $backupCodes
] ]
]; ];
}
// If secret and code provided, verify the code and enable 2FA
if ($code !== null) {
// Verify the setup code
if (!$this->verify($userId, $code)) {
error_log("Code verification failed");
return false;
}
// Enable 2FA
$stmt = $this->db->prepare('
UPDATE user_2fa
SET enabled = 1
WHERE user_id = ? AND secret_key = ?
');
return $stmt->execute([$userId, $secret]);
}
return false;
} catch (Exception $e) { } catch (Exception $e) {
if ($this->db->inTransaction()) { if ($this->db->inTransaction()) {
$this->db->rollBack(); $this->db->rollBack();
} }
return ['success' => false, 'message' => $e->getMessage()]; error_log('2FA enable error: ' . $e->getMessage());
return false;
} }
} }
@ -106,37 +132,24 @@ class TwoFactorAuthentication {
public function verify($userId, $code) { public function verify($userId, $code) {
try { try {
// Get user's 2FA settings // Get user's 2FA settings
$stmt = $this->db->prepare(' $settings = $this->getUserSettings($userId);
SELECT secret_key, backup_codes, enabled if (!$settings) {
FROM user_2fa
WHERE user_id = ?
');
$stmt->execute([$userId]);
$tfa = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$tfa || !$tfa['enabled']) {
return false; return false;
} }
// Check if it's a backup code // Check if code matches a backup code
if ($this->verifyBackupCode($userId, $code)) { if ($this->verifyBackupCode($userId, $code)) {
return true; return true;
} }
// Verify TOTP code // Get current Unix timestamp
$currentTime = time(); $currentTime = time();
// Check current and adjacent time steps // Check time window
for ($timeStep = -1; $timeStep <= 1; $timeStep++) { for ($timeSlot = -$this->window; $timeSlot <= $this->window; $timeSlot++) {
$checkTime = $currentTime + ($timeStep * $this->period); $checkTime = $currentTime + ($timeSlot * $this->period);
if ($this->generateCode($tfa['secret_key'], $checkTime) === $code) { $generatedCode = $this->generateCode($settings['secret_key'], $checkTime);
// Update last used timestamp if (hash_equals($generatedCode, $code)) {
$stmt = $this->db->prepare('
UPDATE user_2fa
SET last_used = NOW()
WHERE user_id = ?
');
$stmt->execute([$userId]);
return true; return true;
} }
} }
@ -149,49 +162,13 @@ class TwoFactorAuthentication {
} }
} }
/**
* Generate a TOTP code for a given secret and time
*
* @param string $secret The secret key
* @param int $time Current Unix timestamp
* @return string Generated code
*/
private function generateCode($secret, $time) {
$timeStep = floor($time / $this->period);
$timeHex = str_pad(dechex($timeStep), 16, '0', STR_PAD_LEFT);
// Convert hex time to binary
$timeBin = '';
for ($i = 0; $i < strlen($timeHex); $i += 2) {
$timeBin .= chr(hexdec(substr($timeHex, $i, 2)));
}
// Get binary secret
$secretBin = $this->base32Decode($secret);
// Calculate HMAC
$hash = hash_hmac($this->algorithm, $timeBin, $secretBin, true);
// Get offset
$offset = ord($hash[strlen($hash) - 1]) & 0xF;
// Generate 4-byte code
$code = (
((ord($hash[$offset]) & 0x7F) << 24) |
((ord($hash[$offset + 1]) & 0xFF) << 16) |
((ord($hash[$offset + 2]) & 0xFF) << 8) |
(ord($hash[$offset + 3]) & 0xFF)
) % pow(10, $this->digits);
return str_pad($code, $this->digits, '0', STR_PAD_LEFT);
}
/** /**
* Generate a random secret key * Generate a random secret key
* *
* @return string Base32 encoded secret * @return string Base32 encoded secret
*/ */
private function generateSecret() { private function generateSecret() {
// Generate random bytes (160 bits for SHA1)
$random = random_bytes($this->secretLength); $random = random_bytes($this->secretLength);
return $this->base32Encode($random); return $this->base32Encode($random);
} }
@ -214,10 +191,19 @@ class TwoFactorAuthentication {
// Process 5 bits at a time // Process 5 bits at a time
for ($i = 0; $i < strlen($binary); $i += 5) { for ($i = 0; $i < strlen($binary); $i += 5) {
$chunk = substr($binary . '0000', $i, 5); $chunk = substr($binary, $i, 5);
if (strlen($chunk) < 5) {
$chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);
}
$encoded .= $alphabet[bindec($chunk)]; $encoded .= $alphabet[bindec($chunk)];
} }
// Add padding
$padding = strlen($encoded) % 8;
if ($padding > 0) {
$encoded .= str_repeat('=', 8 - $padding);
}
return $encoded; return $encoded;
} }
@ -229,16 +215,22 @@ class TwoFactorAuthentication {
*/ */
private function base32Decode($data) { private function base32Decode($data) {
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
// Remove padding and uppercase
$data = rtrim(strtoupper($data), '=');
$binary = ''; $binary = '';
$decoded = '';
// Convert to binary // Convert to binary
for ($i = 0; $i < strlen($data); $i++) { for ($i = 0; $i < strlen($data); $i++) {
$position = strpos($alphabet, $data[$i]); $position = strpos($alphabet, $data[$i]);
if ($position === false) continue; if ($position === false) {
continue;
}
$binary .= str_pad(decbin($position), 5, '0', STR_PAD_LEFT); $binary .= str_pad(decbin($position), 5, '0', STR_PAD_LEFT);
} }
$decoded = '';
// Process 8 bits at a time // Process 8 bits at a time
for ($i = 0; $i + 7 < strlen($binary); $i += 8) { for ($i = 0; $i + 7 < strlen($binary); $i += 8) {
$chunk = substr($binary, $i, 8); $chunk = substr($binary, $i, 8);
@ -248,23 +240,60 @@ class TwoFactorAuthentication {
return $decoded; return $decoded;
} }
/**
* Generate a TOTP code for a given secret and time
* RFC 6238 compliant implementation
*/
private function generateCode($secret, $time) {
// Calculate number of time steps since Unix epoch
$timeStep = (int)floor($time / $this->period);
// Pack time into 8 bytes (64-bit big-endian)
$timeBin = pack('J', $timeStep);
// Clean secret of any padding
$secret = rtrim($secret, '=');
// Get binary secret
$secretBin = $this->base32Decode($secret);
// Calculate HMAC
$hash = hash_hmac($this->algorithm, $timeBin, $secretBin, true);
// Get dynamic truncation offset
$offset = ord($hash[strlen($hash) - 1]) & 0xF;
// Generate 31-bit number
$code = (
((ord($hash[$offset]) & 0x7F) << 24) |
((ord($hash[$offset + 1]) & 0xFF) << 16) |
((ord($hash[$offset + 2]) & 0xFF) << 8) |
(ord($hash[$offset + 3]) & 0xFF)
) % pow(10, $this->digits);
$code = str_pad($code, $this->digits, '0', STR_PAD_LEFT);
return $code;
}
/** /**
* Generate otpauth URL for QR codes * Generate otpauth URL for QR codes
* * Format: otpauth://totp/ISSUER:ACCOUNT?secret=SECRET&issuer=ISSUER&algorithm=ALGORITHM&digits=DIGITS&period=PERIOD
* @param string $username Username
* @param string $secret Secret key
* @return string otpauth URL
*/ */
private function generateOtpauthUrl($username, $secret) { private function generateOtpauthUrl($username, $secret) {
$params = [
'secret' => $secret,
'issuer' => $this->issuer,
'algorithm' => strtoupper($this->algorithm),
'digits' => $this->digits,
'period' => $this->period
];
return sprintf( return sprintf(
'otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d', 'otpauth://totp/%s:%s?%s',
urlencode($this->issuer), rawurlencode($this->issuer),
urlencode($username), rawurlencode($username),
$secret, http_build_query($params)
urlencode($this->issuer),
strtoupper($this->algorithm),
$this->digits,
$this->period
); );
} }
@ -335,11 +364,15 @@ class TwoFactorAuthentication {
*/ */
public function disable($userId) { public function disable($userId) {
try { try {
// First check if user has 2FA settings
$settings = $this->getUserSettings($userId);
if (!$settings) {
return false;
}
// Delete the 2FA settings entirely instead of just disabling
$stmt = $this->db->prepare(' $stmt = $this->db->prepare('
UPDATE user_2fa DELETE FROM user_2fa
SET enabled = 0,
secret_key = NULL,
backup_codes = NULL
WHERE user_id = ? WHERE user_id = ?
'); ');
return $stmt->execute([$userId]); return $stmt->execute([$userId]);
@ -368,4 +401,20 @@ class TwoFactorAuthentication {
return false; return false;
} }
} }
private function getUserSettings($userId) {
try {
$stmt = $this->db->prepare('
SELECT secret_key, backup_codes, enabled
FROM user_2fa
WHERE user_id = ?
');
$stmt->execute([$userId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log('Failed to get user 2FA settings: ' . $e->getMessage());
return null;
}
}
} }

View File

@ -507,10 +507,12 @@ class User {
* Enable two-factor authentication for a user * Enable two-factor authentication for a user
* *
* @param int $userId User ID * @param int $userId User ID
* @return array Result of enabling 2FA * @param string $secret Secret key to use
* @param string $code Verification code to validate
* @return bool True if enabled successfully
*/ */
public function enableTwoFactor($userId) { public function enableTwoFactor($userId, $secret = null, $code = null) {
return $this->twoFactorAuth->enable($userId); return $this->twoFactorAuth->enable($userId, $secret, $code);
} }
/** /**