enforce(RateLimiter::identifier(), 'login'); $passengerId = filterRequest('id'); $fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint'); $audience = filterRequest('aud'); if (empty($passengerId) || empty($fingerprint) || empty($audience)) { jsonError('Missing required parameters', 400); } // ── التحقق من الـ audience (نفس منطق loginFirstTime.php) ──────── // يمنع استخراج توكن بـ audience المحفظة من مسار الراكب العادي. $allowed1 = getenv('allowed1'); $allowed2 = getenv('allowed2'); $allowedAudiences = array_values(array_filter([$allowed1, $allowed2])); if (!in_array($audience, $allowedAudiences, true)) { securityLog("Login rejected: invalid audience", [ 'passengerId' => $passengerId, 'audience' => $audience, ]); jsonError('Invalid audience', 400); } $con = Database::get('main'); // التحقق من الجهاز من خلال البصمة $stmt = $con->prepare(' SELECT passengerID, fingerPrint FROM tokens WHERE passengerID = :pid LIMIT 1 '); $stmt->execute([':pid' => $passengerId]); $row = $stmt->fetch(); $fpVerified = false; if ($row) { $fpPepper = getenv('FP_PEPPER') ?: ''; $storedFp = $row['fingerPrint'] ?? $row['fingerprint'] ?? ''; // دعم الطريقة الجديدة (hash) والقديمة (مباشر) if ($fpPepper) { $expectedHash = hash('sha256', $fingerprint . $fpPepper); $fpVerified = hash_equals($storedFp, $expectedHash); if (!$fpVerified) { $fpVerified = hash_equals($storedFp, $fingerprint); } } else { $fpVerified = hash_equals($storedFp, $fingerprint); } // ── ترحيل لمرة واحدة: بصمات AES-GCM القديمة ──────────────── // النسخ القديمة من تطبيق الراكب كانت تشفّر البصمة بـ AES-GCM // بـ IV عشوائي، فالناتج يختلف في كل مرة ولا يمكن مطابقته إطلاقاً. // النسخة الجديدة تستخدم AES-CBC بـ IV ثابت (ناتج حتمي). // لذلك نقبل استبدال البصمة مرة واحدة فقط عندما تكون المخزّنة // بصيغة GCM القديمة؛ وأي بصمة بالصيغة الجديدة تُقارن بصرامة // ويتكفّل مسار الـ OTP بتغيير الجهاز. // ⚠️ يُحذف هذا الفرع بعد اكتمال ترحيل المستخدمين. if (!$fpVerified && !empty($fingerprint) && isLegacyGcmFingerprint($storedFp)) { // نخزّن القيمة الخام (كما يفعل addToken.php و verify_otp.php) // كي تبقى الصيغة قابلة للتمييز في الطلبات القادمة. $updateStmt = $con->prepare('UPDATE tokens SET fingerPrint = :fp WHERE passengerID = :pid'); $updateStmt->execute([':fp' => $fingerprint, ':pid' => $passengerId]); $fpVerified = true; securityLog("Legacy GCM fingerprint migrated to CBC", [ 'passengerId' => $passengerId, ]); } } // وقت رد ثابت لمنع Timing Attack $elapsed = microtime(true) - $startTime; if ($elapsed < 0.1) usleep((int)((0.1 - $elapsed) * 1000000)); if (!$fpVerified) { securityLog("Invalid login fingerprint", ['passengerId' => $passengerId]); jsonError('Invalid credentials', 401); } $limiter->reset(RateLimiter::identifier(), 'login'); $jwtService = new JwtService($redis); $jwt = $jwtService->generateAccessToken($passengerId, 'passenger', $audience, $fingerprint); // $refresh = $jwtService->generateRefreshToken($passengerId); jsonSuccess([ 'jwt' => $jwt, // 'refresh_token' => $refresh['token'], 'expires_in' => 3600 ]); } catch (PDOException $e) { securityLog("Login PDO Error", ['msg' => $e->getMessage()]); jsonError('Database error', 500); } catch (Exception $e) { securityLog("Login Error", ['msg' => $e->getMessage()]); jsonError('Server error', 500); } catch (\Throwable $e) { securityLog("Login Fatal Error", ['msg' => $e->getMessage()]); jsonError('Server error', 500); }