Sindbad~EG File Manager
<?php
/**
* Two-Factor Authentication Service
* Handles TOTP (Google Authenticator), Email OTP, and SMS OTP
*/
class TwoFactorAuth {
private $db;
private $userType; // 'admin' or 'member'
public function __construct($userType = 'admin') {
$this->db = Database::getInstance()->getConnection();
$this->userType = $userType;
}
/**
* Generate TOTP secret key
*/
public function generateSecret($length = 16) {
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$secret = '';
for ($i = 0; $i < $length; $i++) {
$secret .= $chars[random_int(0, strlen($chars) - 1)];
}
return $secret;
}
/**
* Get QR code URL for TOTP setup
*/
public function getQRCodeUrl($secret, $username, $issuer = 'Church Portal') {
$label = urlencode($issuer) . ':' . urlencode($username);
$params = [
'secret' => $secret,
'issuer' => $issuer
];
$query = http_build_query($params);
return "otpauth://totp/{$label}?{$query}";
}
/**
* Get QR code image URL using Google Charts API
*/
public function getQRCodeImageUrl($secret, $username, $issuer = 'Church Portal') {
$otpauthUrl = $this->getQRCodeUrl($secret, $username, $issuer);
return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . urlencode($otpauthUrl);
}
/**
* Verify TOTP code
*/
public function verifyTOTP($secret, $code, $discrepancy = 1) {
$time = floor(time() / 30);
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
if ($this->getTOTPCode($secret, $time + $i) === $code) {
return true;
}
}
return false;
}
/**
* Generate TOTP code
*/
private function getTOTPCode($secret, $time = null) {
if ($time === null) {
$time = floor(time() / 30);
}
$secret = $this->base32Decode($secret);
$time = pack('N*', 0) . pack('N*', $time);
$hash = hash_hmac('sha1', $time, $secret, true);
$offset = ord($hash[19]) & 0xf;
$code = (
((ord($hash[$offset + 0]) & 0x7f) << 24) |
((ord($hash[$offset + 1]) & 0xff) << 16) |
((ord($hash[$offset + 2]) & 0xff) << 8) |
(ord($hash[$offset + 3]) & 0xff)
) % 1000000;
return str_pad($code, 6, '0', STR_PAD_LEFT);
}
/**
* Base32 decode
*/
private function base32Decode($secret) {
$secret = strtoupper($secret);
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$output = '';
$v = 0;
$vbits = 0;
for ($i = 0, $j = strlen($secret); $i < $j; $i++) {
$v <<= 5;
$v += stripos($alphabet, $secret[$i]);
$vbits += 5;
while ($vbits >= 8) {
$vbits -= 8;
$output .= chr($v >> $vbits);
$v &= ((1 << $vbits) - 1);
}
}
return $output;
}
/**
* Generate backup codes
*/
public function generateBackupCodes($count = 10) {
$codes = [];
for ($i = 0; $i < $count; $i++) {
$codes[] = strtoupper(bin2hex(random_bytes(4))); // 8-character codes
}
return $codes;
}
/**
* Hash backup codes for storage
*/
public function hashBackupCodes($codes) {
return array_map(function($code) {
return password_hash($code, PASSWORD_DEFAULT);
}, $codes);
}
/**
* Verify backup code
*/
public function verifyBackupCode($userId, $code) {
$table = $this->userType === 'admin' ? 'user_2fa_settings' : 'member_2fa_settings';
$column = $this->userType === 'admin' ? 'user_id' : 'member_id';
$stmt = $this->db->prepare("SELECT backup_codes FROM {$table} WHERE {$column} = ?");
$stmt->execute([$userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row || !$row['backup_codes']) {
return false;
}
$hashedCodes = json_decode($row['backup_codes'], true);
foreach ($hashedCodes as $index => $hashedCode) {
if (password_verify($code, $hashedCode)) {
// Remove used backup code
unset($hashedCodes[$index]);
$hashedCodes = array_values($hashedCodes); // Re-index
$stmt = $this->db->prepare("UPDATE {$table} SET backup_codes = ? WHERE {$column} = ?");
$stmt->execute([json_encode($hashedCodes), $userId]);
return true;
}
}
return false;
}
/**
* Generate and send OTP via email or SMS
*/
public function generateOTP($userId, $method = 'email') {
// Get OTP length from settings
$stmt = $this->db->prepare("SELECT setting_value FROM system_settings WHERE setting_key = 'otp_length'");
$stmt->execute();
$length = (int)($stmt->fetchColumn() ?: 6);
// Generate random OTP
$code = str_pad(random_int(0, pow(10, $length) - 1), $length, '0', STR_PAD_LEFT);
// Get expiry time
$stmt = $this->db->prepare("SELECT setting_value FROM system_settings WHERE setting_key = 'otp_expiry_minutes'");
$stmt->execute();
$expiryMinutes = (int)($stmt->fetchColumn() ?: 10);
$expiresAt = date('Y-m-d H:i:s', time() + ($expiryMinutes * 60));
// Store OTP in database
$stmt = $this->db->prepare("
INSERT INTO otp_codes (user_type, user_id, code, method, expires_at)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([$this->userType, $userId, $code, $method, $expiresAt]);
// Send OTP
if ($method === 'email') {
$this->sendEmailOTP($userId, $code, $expiryMinutes);
} elseif ($method === 'sms') {
$this->sendSMSOTP($userId, $code, $expiryMinutes);
}
return true;
}
/**
* Verify OTP code
*/
public function verifyOTP($userId, $code) {
$stmt = $this->db->prepare("
SELECT id FROM otp_codes
WHERE user_type = ?
AND user_id = ?
AND code = ?
AND is_used = 0
AND expires_at > NOW()
ORDER BY created_at DESC
LIMIT 1
");
$stmt->execute([$this->userType, $userId, $code]);
$otp = $stmt->fetch(PDO::FETCH_ASSOC);
if ($otp) {
// Mark as used
$stmt = $this->db->prepare("UPDATE otp_codes SET is_used = 1 WHERE id = ?");
$stmt->execute([$otp['id']]);
return true;
}
return false;
}
/**
* Send OTP via email
*/
private function sendEmailOTP($userId, $code, $expiryMinutes) {
$table = $this->userType === 'admin' ? 'user_2fa_settings' : 'member_2fa_settings';
$column = $this->userType === 'admin' ? 'user_id' : 'member_id';
$stmt = $this->db->prepare("SELECT email FROM {$table} WHERE {$column} = ?");
$stmt->execute([$userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$email = $row['email'] ?? null;
// Get user name for email
$userName = 'User';
if (!$email) {
// Fallback to user table email and get name
if ($this->userType === 'admin') {
$stmt = $this->db->prepare("SELECT email, full_name FROM users WHERE id = ?");
} else {
$stmt = $this->db->prepare("SELECT email, full_name FROM member_accounts WHERE member_id = ?");
}
$stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
$email = $user['email'] ?? null;
$userName = $user['full_name'] ?? 'User';
} else {
// Get name from database
if ($this->userType === 'admin') {
$stmt = $this->db->prepare("SELECT full_name FROM users WHERE id = ?");
} else {
$stmt = $this->db->prepare("SELECT full_name FROM member_accounts WHERE member_id = ?");
}
$stmt->execute([$userId]);
$userName = $stmt->fetchColumn() ?: 'User';
}
if ($email) {
require_once __DIR__ . '/EmailService.php';
$emailService = new EmailService();
$subject = "Your Verification Code";
$message = "
<h2>Two-Factor Authentication</h2>
<p>Dear {$userName},</p>
<p>Your verification code is:</p>
<h1 style='font-size: 32px; letter-spacing: 5px; color: #1E40AF;'>{$code}</h1>
<p>This code will expire in {$expiryMinutes} minutes.</p>
<p>If you didn't request this code, please ignore this email.</p>
";
// Send instantly - 2FA codes are time-sensitive!
$emailService->sendInstantEmail($email, $userName, $subject, $message);
}
}
/**
* Send OTP via SMS
*/
private function sendSMSOTP($userId, $code, $expiryMinutes) {
$table = $this->userType === 'admin' ? 'user_2fa_settings' : 'member_2fa_settings';
$column = $this->userType === 'admin' ? 'user_id' : 'member_id';
$stmt = $this->db->prepare("SELECT phone_number FROM {$table} WHERE {$column} = ?");
$stmt->execute([$userId]);
$phone = $stmt->fetchColumn();
if ($phone) {
// TODO: Integrate with SMS provider (Twilio, etc.)
// For now, log the SMS
error_log("SMS OTP to {$phone}: {$code} (expires in {$expiryMinutes} min)");
}
}
/**
* Enable a specific 2FA method
*/
public function enableMethod($userId, $method, $secret = null, $phone = null, $email = null, $setPrimary = true) {
$table = $this->userType === 'admin' ? 'user_2fa_settings' : 'member_2fa_settings';
$column = $this->userType === 'admin' ? 'user_id' : 'member_id';
// Generate backup codes if this is the first method being enabled
$existingSettings = $this->get2FASettings($userId);
$backupCodes = [];
if (!$existingSettings || empty($existingSettings['backup_codes'])) {
$backupCodes = $this->generateBackupCodes();
$hashedCodes = $this->hashBackupCodes($backupCodes);
$backupCodesJson = json_encode($hashedCodes);
} else {
$backupCodesJson = $existingSettings['backup_codes'];
}
// Determine which column to update
$methodColumn = $method . '_enabled';
$stmt = $this->db->prepare("
INSERT INTO {$table} (
{$column}, is_enabled, primary_method,
{$methodColumn}, totp_secret, phone_number, email, backup_codes
)
VALUES (?, 1, ?, 1, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
is_enabled = 1,
{$methodColumn} = 1,
primary_method = IF(? = 1, VALUES(primary_method), primary_method),
totp_secret = IF(? = 'totp', VALUES(totp_secret), totp_secret),
phone_number = IF(? = 'sms', VALUES(phone_number), phone_number),
email = IF(? = 'email', VALUES(email), email),
backup_codes = IF(backup_codes IS NULL OR backup_codes = '', VALUES(backup_codes), backup_codes)
");
$stmt->execute([
$userId,
$method, // primary_method for INSERT
$secret,
$phone,
$email,
$backupCodesJson,
$setPrimary ? 1 : 0, // Should update primary_method?
$method, // For totp_secret condition
$method, // For phone_number condition
$method // For email condition
]);
return $backupCodes; // Return plain backup codes for user to save (empty if not generated)
}
/**
* Disable a specific 2FA method
*/
public function disableMethod($userId, $method) {
$table = $this->userType === 'admin' ? 'user_2fa_settings' : 'member_2fa_settings';
$column = $this->userType === 'admin' ? 'user_id' : 'member_id';
$methodColumn = $method . '_enabled';
$stmt = $this->db->prepare("
UPDATE {$table}
SET {$methodColumn} = 0,
is_enabled = IF(totp_enabled + email_enabled + sms_enabled - 1 > 0, 1, 0)
WHERE {$column} = ?
");
return $stmt->execute([$userId]);
}
/**
* Get list of enabled methods for a user
*/
public function getEnabledMethods($userId) {
$settings = $this->get2FASettings($userId);
if (!$settings || !$settings['is_enabled']) {
return [];
}
$methods = [];
if (!empty($settings['totp_enabled'])) {
$methods[] = 'totp';
}
if (!empty($settings['email_enabled'])) {
$methods[] = 'email';
}
if (!empty($settings['sms_enabled'])) {
$methods[] = 'sms';
}
return $methods;
}
/**
* Enable 2FA for user (legacy method - kept for backward compatibility)
*/
public function enable2FA($userId, $method, $secret = null, $phone = null, $email = null) {
return $this->enableMethod($userId, $method, $secret, $phone, $email, true);
}
/**
* Disable 2FA for user
*/
public function disable2FA($userId) {
$table = $this->userType === 'admin' ? 'user_2fa_settings' : 'member_2fa_settings';
$column = $this->userType === 'admin' ? 'user_id' : 'member_id';
$stmt = $this->db->prepare("UPDATE {$table} SET is_enabled = 0 WHERE {$column} = ?");
return $stmt->execute([$userId]);
}
/**
* Check if 2FA is enabled for user
*/
public function is2FAEnabled($userId) {
$table = $this->userType === 'admin' ? 'user_2fa_settings' : 'member_2fa_settings';
$column = $this->userType === 'admin' ? 'user_id' : 'member_id';
$stmt = $this->db->prepare("SELECT is_enabled, method FROM {$table} WHERE {$column} = ?");
$stmt->execute([$userId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
/**
* Get 2FA settings for user
*/
public function get2FASettings($userId) {
$table = $this->userType === 'admin' ? 'user_2fa_settings' : 'member_2fa_settings';
$column = $this->userType === 'admin' ? 'user_id' : 'member_id';
$stmt = $this->db->prepare("SELECT * FROM {$table} WHERE {$column} = ?");
$stmt->execute([$userId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
/**
* Log 2FA attempt
*/
public function logAttempt($userId, $method, $success) {
$stmt = $this->db->prepare("
INSERT INTO two_factor_attempts (user_type, user_id, method_used, success, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?, ?)
");
return $stmt->execute([
$this->userType,
$userId,
$method,
$success ? 1 : 0,
$_SERVER['REMOTE_ADDR'] ?? null,
$_SERVER['HTTP_USER_AGENT'] ?? null
]);
}
/**
* Clean up expired OTP codes
*/
public function cleanupExpiredOTPs() {
$stmt = $this->db->prepare("DELETE FROM otp_codes WHERE expires_at < NOW()");
return $stmt->execute();
}
}
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists