From 8d7e3118b55e5bb5582bc6b14144b3385a781eaa Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 25 Jul 2026 16:36:32 +0300 Subject: [PATCH] Migrate remaining encrypted-column lookups to the blind index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the set of queries that matched a freshly encrypted value against a stored one, which only works while encryption is deterministic. Each keeps its original comparison and adds an index comparison in the same WHERE, so nothing changes today. - passenger sign-in by email, service-staff sign-in, Firebase token lookup - driver lookup by phone and by national number - admin ride lookup and ride monitor (both tables) - nabeh: driver status, user resolution, ride history, complaint submission transit_org_admins lives in the transit database and has no index column, so login there falls back to decrypting the small set of active admins and comparing normalised numbers. Schema: adds users.email_bidx/phone_bidx and driver.national_bidx with their indexes. Verified that every :*_bidx placeholder introduced is actually bound — an unbound one is a fatal error at request time, not a silent miss. Co-Authored-By: Claude Opus 5 --- .../Admin/rides/admin_get_rides_by_phone.php | 8 +++---- backend/Admin/rides/monitorRide.php | 13 ++++++++---- .../loginUsingCredentialsWithoutGoogle.php | 5 ++++- backend/nabeh/driver_status.php | 5 ++++- backend/nabeh/get_user_rides.php | 11 ++++++---- backend/nabeh/resolve_user.php | 11 ++++++---- backend/nabeh/submit_complaint.php | 11 ++++++---- backend/ride/firebase/getTokenParent.php | 5 ++++- backend/scripts/migrate.php | 8 +++++++ backend/serviceapp/getDriverByNational.php | 7 +++++-- backend/serviceapp/getDriverByPhone.php | 5 ++++- backend/serviceapp/login.php | 6 ++++-- backend/transit/admin/login_request.php | 21 +++++++++++++++++++ backend/transit/admin/login_verify.php | 21 +++++++++++++++++++ 14 files changed, 109 insertions(+), 28 deletions(-) diff --git a/backend/Admin/rides/admin_get_rides_by_phone.php b/backend/Admin/rides/admin_get_rides_by_phone.php index 3b106dc3..410318fe 100644 --- a/backend/Admin/rides/admin_get_rides_by_phone.php +++ b/backend/Admin/rides/admin_get_rides_by_phone.php @@ -29,20 +29,20 @@ try { $selP = $con->prepare(" SELECT id, first_name, last_name, phone FROM passengers - WHERE phone = :enc_raw + WHERE phone = :enc_raw OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1 "); - $selP->execute(['enc_raw' => $enc_raw]); + $selP->execute(['enc_raw' => $enc_raw, 'bidx' => $pBidx]); $passenger = $selP->fetch(PDO::FETCH_ASSOC); // 2) ابحث عن السائق بالهاتف المشفّر $selD = $con->prepare(" SELECT id AS driverID, first_name, last_name, phone FROM driver - WHERE phone = :enc_raw + WHERE phone = :enc_raw OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1 "); - $selD->execute(['enc_raw' => $enc_raw]); + $selD->execute(['enc_raw' => $enc_raw, 'bidx' => $dBidx]); $driver = $selD->fetch(PDO::FETCH_ASSOC); $userId = null; diff --git a/backend/Admin/rides/monitorRide.php b/backend/Admin/rides/monitorRide.php index f2f599a4..b4330c4a 100644 --- a/backend/Admin/rides/monitorRide.php +++ b/backend/Admin/rides/monitorRide.php @@ -23,16 +23,21 @@ error_log("[MONITOR_RIDE] 1.5 Normalized Phone: " . $phone); //------------------------------------------------------------------------ $encPhone = $encryptionHelper->encryptData($phone); + +// فهرس البحث لكل جدول على حدة (النطاقات معزولة عمداً) +global $blindIndex; +$dBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null; +$pBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone) : null; error_log("[MONITOR_RIDE] 2. Encrypted Phone: " . $encPhone); // Check Driver Table -$driverQuery = $con->prepare("SELECT id AS driverID FROM driver WHERE phone = :phone LIMIT 1"); -$driverQuery->execute([':phone' => $encPhone]); +$driverQuery = $con->prepare("SELECT id AS driverID FROM driver WHERE phone = :phone OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1"); +$driverQuery->execute([':phone' => $encPhone, ':bidx' => $dBidx]); $driver = $driverQuery->fetch(PDO::FETCH_ASSOC); // Check Passenger Table -$customerQuery = $con->prepare("SELECT id AS customerID FROM passengers WHERE phone = :phone LIMIT 1"); -$customerQuery->execute([':phone' => $encPhone]); +$customerQuery = $con->prepare("SELECT id AS customerID FROM passengers WHERE phone = :phone OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1"); +$customerQuery->execute([':phone' => $encPhone, ':bidx' => $pBidx]); $customer = $customerQuery->fetch(PDO::FETCH_ASSOC); // حدد نوع المستخدم diff --git a/backend/auth/passenger/loginUsingCredentialsWithoutGoogle.php b/backend/auth/passenger/loginUsingCredentialsWithoutGoogle.php index a9dde3b1..4c07f934 100644 --- a/backend/auth/passenger/loginUsingCredentialsWithoutGoogle.php +++ b/backend/auth/passenger/loginUsingCredentialsWithoutGoogle.php @@ -40,6 +40,8 @@ try { // تشفير الإيميل للبحث في قاعدة البيانات $encryptedEmail = $encryptionHelper->encryptData($email); + global $blindIndex; + $emailBidx = $blindIndex ? $blindIndex->index('passengers.email', $email) : null; // Auto-seed/create tester passenger logic removed for security @@ -54,11 +56,12 @@ try { ON phone_verification_passenger.phone_number = p.phone LEFT JOIN invitesToPassengers ON invitesToPassengers.inviterPassengerPhone = p.phone - WHERE p.email = :email + WHERE p.email = :email OR (:email_bidx IS NOT NULL AND p.email_bidx = :email_bidx) LIMIT 1"; $stmt = $con->prepare($sql); $stmt->bindParam(':email', $encryptedEmail); + $stmt->bindParam(':email_bidx', $emailBidx); $stmt->execute(); $data = $stmt->fetch(PDO::FETCH_ASSOC); diff --git a/backend/nabeh/driver_status.php b/backend/nabeh/driver_status.php index d5a7729f..0b90ddf9 100644 --- a/backend/nabeh/driver_status.php +++ b/backend/nabeh/driver_status.php @@ -33,17 +33,20 @@ try { global $encryptionHelper; $encryptedPhone = $encryptionHelper->encryptData($phone); + global $blindIndex; + $phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null; $stmt = $db->prepare(" SELECT d.id, d.phone, d.first_name, d.last_name, d.status, d.created_at, cr.id as car_id, cr.make, cr.model, cr.year, cr.car_plate, cr.status as car_status FROM driver d LEFT JOIN CarRegistration cr ON cr.driverID = d.id - WHERE d.phone = :phone + WHERE (d.phone = :phone OR (:phone_bidx IS NOT NULL AND d.phone_bidx = :phone_bidx)) LIMIT 1 "); $stmt->execute([ ':phone' => $encryptedPhone, + ':phone_bidx' => $phoneBidx, ]); $result = $stmt->fetch(PDO::FETCH_ASSOC); diff --git a/backend/nabeh/get_user_rides.php b/backend/nabeh/get_user_rides.php index 3b0f03c0..8f671de1 100644 --- a/backend/nabeh/get_user_rides.php +++ b/backend/nabeh/get_user_rides.php @@ -52,13 +52,16 @@ global $encryptionHelper; // Resolve user $encryptedPhone = $encryptionHelper->encryptData($phone); -$driver = $mainDb->prepare("SELECT id, 'driver' AS type FROM driver WHERE phone = :p LIMIT 1"); -$driver->execute([':p' => $encryptedPhone]); +global $blindIndex; +$dBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null; +$pBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone) : null; +$driver = $mainDb->prepare("SELECT id, 'driver' AS type FROM driver WHERE phone = :p OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1"); +$driver->execute([':p' => $encryptedPhone, ':bidx' => $dBidx]); $user = $driver->fetch(PDO::FETCH_ASSOC); if (!$user) { - $passenger = $mainDb->prepare("SELECT id, 'passenger' AS type FROM passengers WHERE phone = :p LIMIT 1"); - $passenger->execute([':p' => $encryptedPhone]); + $passenger = $mainDb->prepare("SELECT id, 'passenger' AS type FROM passengers WHERE phone = :p OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1"); + $passenger->execute([':p' => $encryptedPhone, ':bidx' => $pBidx]); $user = $passenger->fetch(PDO::FETCH_ASSOC); } diff --git a/backend/nabeh/resolve_user.php b/backend/nabeh/resolve_user.php index 3f38c7a5..d796f92b 100644 --- a/backend/nabeh/resolve_user.php +++ b/backend/nabeh/resolve_user.php @@ -59,12 +59,15 @@ try { global $encryptionHelper; $encryptedPhone = $encryptionHelper->encryptData($phone); + global $blindIndex; + $dBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null; + $pBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone) : null; // Look for driver first $stmt = $db->prepare( - "SELECT id, phone, first_name, last_name FROM driver WHERE phone = :phone LIMIT 1" + "SELECT id, phone, first_name, last_name FROM driver WHERE phone = :phone OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1" ); - $stmt->execute([':phone' => $encryptedPhone]); + $stmt->execute([':phone' => $encryptedPhone, ':bidx' => $dBidx]); $driver = $stmt->fetch(PDO::FETCH_ASSOC); if ($driver) { @@ -86,9 +89,9 @@ try { // Fallback: look for passenger $stmt = $db->prepare( - "SELECT id, phone, first_name, last_name FROM passengers WHERE phone = :phone LIMIT 1" + "SELECT id, phone, first_name, last_name FROM passengers WHERE phone = :phone OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1" ); - $stmt->execute([':phone' => $encryptedPhone]); + $stmt->execute([':phone' => $encryptedPhone, ':bidx' => $pBidx]); $passenger = $stmt->fetch(PDO::FETCH_ASSOC); if ($passenger) { diff --git a/backend/nabeh/submit_complaint.php b/backend/nabeh/submit_complaint.php index 6507d4f5..7ee5fe4e 100644 --- a/backend/nabeh/submit_complaint.php +++ b/backend/nabeh/submit_complaint.php @@ -64,14 +64,17 @@ global $encryptionHelper; // ── Resolve user by phone ──────────────────────────────────── $encryptedPhone = $encryptionHelper->encryptData($phone); -$driverRow = $mainDb->prepare("SELECT id, first_name, last_name FROM driver WHERE phone = :p LIMIT 1"); -$driverRow->execute([':p' => $encryptedPhone]); +global $blindIndex; +$dBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null; +$pBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone) : null; +$driverRow = $mainDb->prepare("SELECT id, first_name, last_name FROM driver WHERE phone = :p OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1"); +$driverRow->execute([':p' => $encryptedPhone, ':bidx' => $dBidx]); $driver = $driverRow->fetch(PDO::FETCH_ASSOC); $passengerRow = null; if (!$driver) { - $passengerRow = $mainDb->prepare("SELECT id, first_name, last_name FROM passengers WHERE phone = :p LIMIT 1"); - $passengerRow->execute([':p' => $encryptedPhone]); + $passengerRow = $mainDb->prepare("SELECT id, first_name, last_name FROM passengers WHERE phone = :p OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1"); + $passengerRow->execute([':p' => $encryptedPhone, ':bidx' => $pBidx]); $passenger = $passengerRow->fetch(PDO::FETCH_ASSOC); } diff --git a/backend/ride/firebase/getTokenParent.php b/backend/ride/firebase/getTokenParent.php index 03d7a868..4ce6eefa 100644 --- a/backend/ride/firebase/getTokenParent.php +++ b/backend/ride/firebase/getTokenParent.php @@ -5,11 +5,14 @@ $phone = filterRequest("phone"); // 🔐 تشفير رقم الهاتف قبل البحث (لأنه مشفّر في قاعدة البيانات) $phoneEncrypted = $encryptionHelper->encryptData($phone); +global $blindIndex; +$phoneBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone) : null; // 1️⃣ جلب passengerID بناءً على رقم الهاتف -$sql = "SELECT `id` FROM `passengers` WHERE `phone` = :phone"; +$sql = "SELECT `id` FROM `passengers` WHERE `phone` = :phone OR (:phone_bidx IS NOT NULL AND `phone_bidx` = :phone_bidx)"; $stmt = $con->prepare($sql); $stmt->bindParam(':phone', $phoneEncrypted); +$stmt->bindParam(':phone_bidx', $phoneBidx); $stmt->execute(); $data = $stmt->fetch(PDO::FETCH_ASSOC); diff --git a/backend/scripts/migrate.php b/backend/scripts/migrate.php index acc9ba01..8f07915e 100644 --- a/backend/scripts/migrate.php +++ b/backend/scripts/migrate.php @@ -84,6 +84,11 @@ $columns = [ ['token_verification', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"], ['token_verification_driver', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"], + // بقية الجداول التي يُبحث فيها بحقل مشفّر + ['users', 'email_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC لبريد موظف الخدمة'"], + ['users', 'phone_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC لهاتف موظف الخدمة'"], + ['driver', 'national_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للرقم الوطني'"], + // أعمدة موافقات المشرفين المفقودة في هذا النشر ['adminUser', 'status', "VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active | pending | suspended | rejected'"], ['adminUser', 'approved_by', "VARCHAR(32) NULL DEFAULT NULL"], @@ -99,6 +104,9 @@ $indexes = [ ['passengers', 'idx_passengers_name_bidx', 'name_bidx'], ['adminUser', 'idx_adminuser_phone_bidx', 'phone_bidx'], ['adminUser', 'idx_adminuser_email_bidx', 'email_bidx'], + ['users', 'idx_users_email_bidx', 'email_bidx'], + ['users', 'idx_users_phone_bidx', 'phone_bidx'], + ['driver', 'idx_driver_national_bidx', 'national_bidx'], ]; $applied = 0; diff --git a/backend/serviceapp/getDriverByNational.php b/backend/serviceapp/getDriverByNational.php index 033511d8..4fae7e9b 100644 --- a/backend/serviceapp/getDriverByNational.php +++ b/backend/serviceapp/getDriverByNational.php @@ -5,7 +5,9 @@ require_once __DIR__ . '/../connect.php'; $national_number = filterRequest("national_number"); // 2. تشفير الرقم الوطني للمقارنة مع القيمة المشفرة في قاعدة البيانات -$encryptedNationalNumber = $encryptionHelper->encryptData($national_number); +$encryptedNationalNumber = $encryptionHelper->encryptData($national_number); +global $blindIndex; +$nationalBidx = $blindIndex ? $blindIndex->index('driver.national', $national_number) : null; $sql = "SELECT COALESCE( @@ -73,13 +75,14 @@ $sql = "SELECT d.* FROM driver d LEFT JOIN CarRegistration cr ON cr.driverID = d.id -WHERE d.national_number = :national_number; +WHERE d.national_number = :national_number OR (:national_bidx IS NOT NULL AND d.national_bidx = :national_bidx); "; // 3. تم تعديل الشرط أعلاه للبحث بالرقم الوطني $stmt = $con->prepare($sql); // 4. ربط الباراميتر الجديد $stmt->bindParam(':national_number', $encryptedNationalNumber); +$stmt->bindParam(':national_bidx', $nationalBidx); $stmt->execute(); if ($stmt->rowCount() > 0) { diff --git a/backend/serviceapp/getDriverByPhone.php b/backend/serviceapp/getDriverByPhone.php index fbc2b66a..e17776e5 100644 --- a/backend/serviceapp/getDriverByPhone.php +++ b/backend/serviceapp/getDriverByPhone.php @@ -3,6 +3,8 @@ require_once __DIR__ . '/../connect.php'; $phone = filterRequest("phone"); $encryptedPhone = $encryptionHelper->encryptData($phone); // تشفير الهاتف +global $blindIndex; +$phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null; $sql = "SELECT COALESCE( @@ -70,11 +72,12 @@ $sql = "SELECT d.* FROM driver d LEFT JOIN CarRegistration cr ON cr.driverID = d.id -WHERE d.phone = :phone; +WHERE d.phone = :phone OR (:phone_bidx IS NOT NULL AND d.phone_bidx = :phone_bidx); "; $stmt = $con->prepare($sql); $stmt->bindParam(':phone', $encryptedPhone); +$stmt->bindParam(':phone_bidx', $phoneBidx); $stmt->execute(); if ($stmt->rowCount() > 0) { diff --git a/backend/serviceapp/login.php b/backend/serviceapp/login.php index bdc4a055..737da59c 100644 --- a/backend/serviceapp/login.php +++ b/backend/serviceapp/login.php @@ -32,8 +32,10 @@ try { // إذا لم يتم العثور بالبصمة، وتم تمرير الإيميل (تسجيل دخول لأول مرة أو من جهاز جديد) if (!$user && !empty($email)) { $encEmailInput = $encryptionHelper->encryptData($email); - $stmtEmail = $con->prepare("SELECT * FROM `users` WHERE `email` = :email AND `user_type` = 'service' LIMIT 1"); - $stmtEmail->execute([':email' => $encEmailInput]); + global $blindIndex; + $emailBidx = $blindIndex ? $blindIndex->index('users.email', $email) : null; + $stmtEmail = $con->prepare("SELECT * FROM `users` WHERE (`email` = :email OR (:email_bidx IS NOT NULL AND `email_bidx` = :email_bidx)) AND `user_type` = 'service' LIMIT 1"); + $stmtEmail->execute([':email' => $encEmailInput, ':email_bidx' => $emailBidx]); $user = $stmtEmail->fetch(PDO::FETCH_ASSOC); // تأكيد كلمة المرور وتحديث بصمة الجهاز إذا تم إيجاد الحساب diff --git a/backend/transit/admin/login_request.php b/backend/transit/admin/login_request.php index 0eb48d79..c10650a4 100644 --- a/backend/transit/admin/login_request.php +++ b/backend/transit/admin/login_request.php @@ -22,6 +22,9 @@ if (transitIsOtpLocked($phone)) { } $phoneEnc = $encryptionHelper->encryptData($phone); +// transit_org_admins يعيش في قاعدة المواصلات ولا يملك عمود فهرس بعد، +// فتتم المطابقة على الرقم الأصلي بعد فك التشفير عند فشل المقارنة المباشرة. +$phoneNorm = normalizePhone($phone); $st = $transit_con->prepare( "SELECT a.id, o.contract_status FROM transit_org_admins a @@ -31,6 +34,24 @@ $st = $transit_con->prepare( $st->execute([$phoneEnc]); $admin = $st->fetch(); +if (!$admin) { + // تحت التشفير العشوائي لا تتطابق النصوص المشفّرة، فنقارن الأرقام الأصلية. + $all = $transit_con->query( + "SELECT a.id, a.phone, o.contract_status + FROM transit_org_admins a + JOIN transit_orgs o ON o.id = a.org_id + WHERE a.is_active = 1" + )->fetchAll(PDO::FETCH_ASSOC); + + foreach ($all as $row) { + $plain = $encryptionHelper->decryptData($row['phone'] ?? null); + if ($plain && normalizePhone($plain) === $phoneNorm) { + $admin = $row; + break; + } + } +} + // لا نكشف إن كان الهاتف موجوداً أم لا — نفس الرد دائماً if (!$admin || $admin['contract_status'] === 'terminated') { jsonSuccess(null, 'OTP sent successfully'); diff --git a/backend/transit/admin/login_verify.php b/backend/transit/admin/login_verify.php index e2ec609a..fa57bef1 100644 --- a/backend/transit/admin/login_verify.php +++ b/backend/transit/admin/login_verify.php @@ -19,6 +19,9 @@ $otp = preg_replace('/\D/', '', filterRequest('otp')); if (!transitVerifyAdminOtp($phone, $otp)) jsonError('Invalid or expired OTP', 401); $phoneEnc = $encryptionHelper->encryptData($phone); +// transit_org_admins يعيش في قاعدة المواصلات ولا يملك عمود فهرس بعد، +// فتتم المطابقة على الرقم الأصلي بعد فك التشفير عند فشل المقارنة المباشرة. +$phoneNorm = normalizePhone($phone); $st = $transit_con->prepare( "SELECT a.id, a.org_id, a.name, a.role, a.permissions, o.name_ar, o.name_en, o.type, o.contract_status, o.logo_url @@ -28,6 +31,24 @@ $st = $transit_con->prepare( ); $st->execute([$phoneEnc]); $admin = $st->fetch(); + +if (!$admin) { + // تحت التشفير العشوائي لا تتطابق النصوص المشفّرة، فنقارن الأرقام الأصلية. + $all = $transit_con->query( + "SELECT a.id, a.phone, o.contract_status + FROM transit_org_admins a + JOIN transit_orgs o ON o.id = a.org_id + WHERE a.is_active = 1" + )->fetchAll(PDO::FETCH_ASSOC); + + foreach ($all as $row) { + $plain = $encryptionHelper->decryptData($row['phone'] ?? null); + if ($plain && normalizePhone($plain) === $phoneNorm) { + $admin = $row; + break; + } + } +} if (!$admin) jsonError('Admin not found', 401); $token = transitCreateSession((int)$admin['id'], (int)$admin['org_id']);