Sindbad~EG File Manager

Current Path : /home/copmadinaarea/thecopmadinaarea.org/portal/classes/
Upload File :
Current File : /home/copmadinaarea/thecopmadinaarea.org/portal/classes/TwoFactorAuth.php

<?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