db = Database::getInstance(); } /** * Validate HMAC signature of incoming request * * @param string $api_key The API key from request header * @param string $signature The HMAC signature from request header * @param string $payload The raw request body * @param string $timestamp The request timestamp * @return array ['valid' => bool, 'user_id' => int|null, 'error' => string|null] */ public function validateHmacSignature($api_key, $signature, $payload, $timestamp) { // Validate timestamp to prevent replay attacks if (!$this->isValidTimestamp($timestamp)) { return [ 'valid' => false, 'user_id' => null, 'error' => 'Request timestamp is invalid or expired' ]; } // Get user by API key $user = $this->getUserByApiKey($api_key); if (!$user) { // Log suspicious activity $this->logSecurityEvent('INVALID_API_KEY', $api_key); return [ 'valid' => false, 'user_id' => null, 'error' => 'Invalid API key' ]; } // Generate expected signature $expectedSignature = $this->generateSignature( $payload, $user['api_secret'], $timestamp, $api_key ); // Compare signatures using timing-safe comparison if (!hash_equals($expectedSignature, $signature)) { // Log failed authentication attempt $this->logSecurityEvent('INVALID_SIGNATURE', $api_key, $user['id']); return [ 'valid' => false, 'user_id' => null, 'error' => 'Invalid signature' ]; } // Check if user is active if (!$user['is_active']) { return [ 'valid' => false, 'user_id' => null, 'error' => 'User account is inactive' ]; } return [ 'valid' => true, 'user_id' => $user['id'], 'error' => null ]; } /** * Generate HMAC signature * * Signature format: HMAC-SHA256(timestamp|payload, api_secret) */ private function generateSignature($payload, $api_secret, $timestamp, $api_key) { $data = $timestamp . '|' . $api_key . '|' . $payload; return hash_hmac(self::SIGNATURE_ALGORITHM, $data, $api_secret); } /** * Verify timestamp is within acceptable range */ private function isValidTimestamp($timestamp) { $current_time = time(); $request_time = (int)$timestamp; $time_diff = abs($current_time - $request_time); return $time_diff <= self::TIMESTAMP_TOLERANCE; } /** * Get user by API key */ private function getUserByApiKey($api_key) { $stmt = $this->db->prepare(' SELECT id, api_secret, is_active, uuid FROM users WHERE api_key = ? LIMIT 1 '); $stmt->bind_param('s', $api_key); $stmt->execute(); $result = $stmt->get_result(); if ($result->num_rows === 0) { return null; } return $result->fetch_assoc(); } /** * Log security events for audit trail */ private function logSecurityEvent($event_type, $api_key, $user_id = null) { $ip_address = $this->getClientIpAddress(); $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'; $stmt = $this->db->prepare(' INSERT INTO api_logs (user_id, endpoint, method, status_code, ip_address, user_agent, error_message, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, NOW()) '); $endpoint = $event_type; $method = $_SERVER['REQUEST_METHOD']; $status_code = 401; $error_msg = $event_type; $stmt->bind_param( 'issssss', $user_id, $endpoint, $method, $status_code, $ip_address, $user_agent, $error_msg ); $stmt->execute(); $stmt->close(); } /** * Get client IP address (handles proxies) */ private function getClientIpAddress() { if (!empty($_SERVER['HTTP_CLIENT_IP'])) { return $_SERVER['HTTP_CLIENT_IP']; } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); return trim($ips[0]); } else { return $_SERVER['REMOTE_ADDR'] ?? 'Unknown'; } } /** * Generate API key and secret for new user */ public static function generateApiCredentials() { return [ 'api_key' => bin2hex(random_bytes(32)), 'api_secret' => bin2hex(random_bytes(32)) ]; } /** * Hash password using bcrypt */ public static function hashPassword($password) { return password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); } /** * Verify password */ public static function verifyPassword($password, $hash) { return password_verify($password, $hash); } } ?>