diff --git a/backend/Admin/driver/updateDriverFromAdmin.php b/backend/Admin/driver/updateDriverFromAdmin.php index 0ad5a21a..71cce4af 100644 --- a/backend/Admin/driver/updateDriverFromAdmin.php +++ b/backend/Admin/driver/updateDriverFromAdmin.php @@ -24,6 +24,13 @@ if ($phone !== null && $phone !== '') { $encphone = $encryptionHelper->encryptData($phone); $updateFields[] = "`phone` = :phone"; $params[':phone'] = $encphone; + + // الفهرس يُحدَّث مع الرقم نفسه حتى لا يشير إلى القيمة القديمة + global $blindIndex; + if ($blindIndex) { + $updateFields[] = "`phone_bidx` = :phone_bidx"; + $params[':phone_bidx'] = $blindIndex->index('driver.phone', $phone); + } } if ($status !== null && $status !== '') { diff --git a/backend/auth/driver/register.php b/backend/auth/driver/register.php index 1e44eaa6..0af20a46 100644 --- a/backend/auth/driver/register.php +++ b/backend/auth/driver/register.php @@ -383,6 +383,16 @@ Therefore, do NOT assume a specific field is on the front or the back of a card. $pwdHashed = password_hash($rawSecret, PASSWORD_DEFAULT); /* ================== 4) Encrypt sensitive fields ================== */ + // فهارس البحث تُحسب من القيم الخام قبل التشفير — بعده تصبح القيمة الأصلية + // غير متاحة، وبعد الانتقال إلى GCM لا يمكن استنتاجها من النص المشفّر. + global $blindIndex; + $phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $data['phone'] ?? null) : null; + $emailBidx = $blindIndex ? $blindIndex->index('driver.email', $data['email'] ?? null) : null; + $nameBidx = $blindIndex ? $blindIndex->index( + 'driver.name', + trim(($data['first_name'] ?? '') . ' ' . ($data['last_name'] ?? '')) + ) : null; + $toEncryptDriver = [ "phone","email","first_name","last_name","name_arabic","gender", "national_number","address","site","fullNameMaritial","birthdate" @@ -402,8 +412,18 @@ Therefore, do NOT assume a specific field is on the front or the back of a card. $con->beginTransaction(); /* ================== 6) Check duplicate ================== */ - $dup = $con->prepare("SELECT id FROM driver WHERE phone = :p OR email = :e"); - $dup->execute([':p' => $data['phone'], ':e' => $data['email']]); + $dup = $con->prepare( + "SELECT id FROM driver + WHERE phone = :p OR email = :e + OR (:pb IS NOT NULL AND phone_bidx = :pb) + OR (:eb IS NOT NULL AND email_bidx = :eb)" + ); + $dup->execute([ + ':p' => $data['phone'], + ':e' => $data['email'], + ':pb' => $phoneBidx, + ':eb' => $emailBidx, + ]); if ($dup->rowCount() > 0) { $con->rollBack(); jsonError("Phone or email already registered."); @@ -418,14 +438,16 @@ Therefore, do NOT assume a specific field is on the front or the back of a card. address, licenseIssueDate, status, birthdate, site, first_name, last_name, accountBank, bankCode, employmentType, ai_data, user_input, maritalStatus, - fullNameMaritial, expirationDate, created_at, updated_at + fullNameMaritial, expirationDate, created_at, updated_at, + phone_bidx, email_bidx, name_bidx ) VALUES ( :id, :phone, :email, :pwd, :gender, :license_type, :national_number, :name_arabic, :issue_date, :expiry_date, :license_categories, :address, :licenseIssueDate, :status, :birthdate, :site, :first_name, :last_name, :accountBank, :bankCode, :employmentType, :ai_data, :user_input, :maritalStatus, - :fullNameMaritial, :expirationDate, NOW(), NOW() + :fullNameMaritial, :expirationDate, NOW(), NOW(), + :phone_bidx, :email_bidx, :name_bidx ) "; $insD = $con->prepare($sqlDriver); @@ -456,6 +478,9 @@ Therefore, do NOT assume a specific field is on the front or the back of a card. ':maritalStatus' => !empty($data['maritalStatus']) ? $data['maritalStatus'] : 'yet', ':fullNameMaritial' => !empty($data['fullNameMaritial']) ? $data['fullNameMaritial'] : 'yet', ':expirationDate' => !empty($data['expirationDate']) ? $data['expirationDate'] : 'yet', + ':phone_bidx' => $phoneBidx, + ':email_bidx' => $emailBidx, + ':name_bidx' => $nameBidx, ]); if (!$okD) { $con->rollBack(); diff --git a/backend/auth/login.php b/backend/auth/login.php index 82a43005..636f8253 100644 --- a/backend/auth/login.php +++ b/backend/auth/login.php @@ -11,16 +11,39 @@ if (empty($phone) && empty($email)) { exit; } -// Build WHERE dynamically: support phone-only, email-only, or both +/** + * البحث عن الحساب. + * + * سابقاً كان يقارن القيمة الخام بالعمود المشفّر مباشرةً، وهو ما ينجح فقط لأن + * التشفير الحالي حتمي (CBC بـ IV ثابت). الفهرس الأعمى يجعل هذا الاستعلام + * مستقلاً عن أسلوب التشفير، فلا ينكسر تسجيل الدخول عند الانتقال إلى AES-GCM. + * + * تُبقى المقارنتان القديمتان في نفس الشرط كاحتياط للحسابات التي لم تُفهرس بعد. + */ +global $blindIndex; + $conditions = []; $params = [':password' => $password]; + if (!empty($phone)) { $conditions[] = "passengers.phone = :phone"; $params[':phone'] = $phone; + + $phoneBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone) : null; + if ($phoneBidx) { + $conditions[] = "passengers.phone_bidx = :phone_bidx"; + $params[':phone_bidx'] = $phoneBidx; + } } if (!empty($email)) { $conditions[] = "passengers.email = :email"; $params[':email'] = $email; + + $emailBidx = $blindIndex ? $blindIndex->index('passengers.email', $email) : null; + if ($emailBidx) { + $conditions[] = "passengers.email_bidx = :email_bidx"; + $params[':email_bidx'] = $emailBidx; + } } $where = implode(' OR ', $conditions); diff --git a/backend/auth/passenger/register.php b/backend/auth/passenger/register.php index 6c423c8b..ed1290a7 100644 --- a/backend/auth/passenger/register.php +++ b/backend/auth/passenger/register.php @@ -103,8 +103,17 @@ try { // Step 6: التحقق من وجود المستخدم (Database Check) // ====================================================== $step = 6; - $checkStmt = $con->prepare("SELECT id FROM passengers WHERE phone = ?"); - $checkStmt->execute([$phoneNumber_encrypted]); + // كشف التكرار عبر الفهرس الأعمى + المقارنة القديمة: بدون الفهرس يفشل + // الكشف بعد الانتقال إلى GCM فيُسمح بتسجيل نفس الرقم مرتين. + global $blindIndex; + $phoneBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phoneNumber) : null; + $emailBidx = $blindIndex ? $blindIndex->index('passengers.email', $email) : null; + $nameBidx = $blindIndex ? $blindIndex->index('passengers.name', trim("$firstName $lastName")) : null; + + $checkStmt = $con->prepare( + "SELECT id FROM passengers WHERE phone = ? OR (? IS NOT NULL AND phone_bidx = ?)" + ); + $checkStmt->execute([$phoneNumber_encrypted, $phoneBidx, $phoneBidx]); if ($checkStmt->rowCount() > 0) { error_log("$logTag Step 6 Error: User already exists."); @@ -119,8 +128,8 @@ try { error_log("$logTag Step 7: Inserting into passengers table..."); $insertStmt = $con->prepare(" - INSERT INTO passengers (id, first_name, last_name, email, phone, password, gender, birthdate, site, sosPhone, education, employmentType, maritalStatus, status, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', NOW(), NOW()) + INSERT INTO passengers (id, first_name, last_name, email, phone, password, gender, birthdate, site, sosPhone, education, employmentType, maritalStatus, status, created_at, updated_at, phone_bidx, email_bidx, name_bidx) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', NOW(), NOW(), ?, ?, ?) "); $success = $insertStmt->execute([ $uniqueId, @@ -135,7 +144,11 @@ try { $unknown_encrypted, $unknown_encrypted, $unknown_encrypted, - $unknown_encrypted + $unknown_encrypted, + // فهارس البحث: تُكتب مع السجل حتى يكون قابلاً للبحث فوراً + $phoneBidx, + $emailBidx, + $nameBidx ]); if (!$success) { diff --git a/backend/ride/profile/update.php b/backend/ride/profile/update.php index 2cab13fa..d23d4077 100644 --- a/backend/ride/profile/update.php +++ b/backend/ride/profile/update.php @@ -11,15 +11,40 @@ $encryptedFields = [ "first_name", "last_name", "education", "employmentType", "maritalStatus" ]; +// أي تعديل على حقل مفهرس يجب أن يُحدّث فهرسه في نفس العبارة، وإلا بقي +// الفهرس يشير إلى القيمة القديمة وأصبح البحث يعطي نتيجة خاطئة. +global $blindIndex; +$plain = []; + foreach ($encryptedFields as $field) { if (isset($_POST[$field]) && !empty($_POST[$field])) { $value = filterRequest($field); + $plain[$field] = $value; $encryptedValue = $encryptionHelper->encryptData($value); $fields[] = "`$field` = :$field"; $params[":$field"] = $encryptedValue; } } +if ($blindIndex) { + if (isset($plain['phone'])) { + $fields[] = "`phone_bidx` = :phone_bidx"; + $params[':phone_bidx'] = $blindIndex->index('passengers.phone', $plain['phone']); + } + if (isset($plain['first_name']) || isset($plain['last_name'])) { + // الاسم مركّب من عمودين: نقرأ الجزء غير المُعدَّل من السجل الحالي + $current = $con->prepare("SELECT first_name, last_name FROM passengers WHERE id = :id"); + $current->execute([':id' => $id]); + $row = $current->fetch(PDO::FETCH_ASSOC) ?: []; + + $first = $plain['first_name'] ?? $encryptionHelper->decryptData($row['first_name'] ?? null) ?: ''; + $last = $plain['last_name'] ?? $encryptionHelper->decryptData($row['last_name'] ?? null) ?: ''; + + $fields[] = "`name_bidx` = :name_bidx"; + $params[':name_bidx'] = $blindIndex->index('passengers.name', trim("$first $last")); + } +} + if (!empty($fields)) { $setClause = implode(", ", $fields); $sql = "UPDATE `passengers` SET $setClause WHERE `id` = :id"; diff --git a/backend/scripts/backfill_blind_index.php b/backend/scripts/backfill_blind_index.php index b87bfb9a..8e7d8cd8 100644 --- a/backend/scripts/backfill_blind_index.php +++ b/backend/scripts/backfill_blind_index.php @@ -25,7 +25,7 @@ if (PHP_SAPI !== 'cli') { require_once __DIR__ . '/../core/bootstrap.php'; require_once __DIR__ . '/../core/Security/BlindIndex.php'; -$options = getopt('', ['dry-run', 'force', 'verify::', 'table::', 'batch::']); +$options = getopt('', ['dry-run', 'force', 'audit', 'verify::', 'table::', 'batch::']); $dryRun = isset($options['dry-run']); $force = isset($options['force']); $only = $options['table'] ?? null; @@ -79,6 +79,56 @@ if (isset($options['verify'])) { exit; } + +/** + * --audit: يقارن كل فهرس مخزَّن بالفهرس المحسوب من القيمة المشفّرة. + * + * الهدف كشف "الانحراف": صف عُدِّل هاتفه أو بريده عبر مسار كتابة لا يُحدّث + * الفهرس، فيصبح البحث يجد السجل بقيمته القديمة أو لا يجده إطلاقاً. الانحراف + * صامت بطبيعته، ولن يظهر إلا حين يفشل بحث حقيقي. + */ +if (isset($options['audit'])) { + $con = Database::get('main'); + $checks = [ + ['driver', 'driver.phone', 'phone', 'phone_bidx'], + ['driver', 'driver.email', 'email', 'email_bidx'], + ['passengers', 'passengers.phone', 'phone', 'phone_bidx'], + ['passengers', 'passengers.email', 'email', 'email_bidx'], + ['adminUser', 'adminUser.phone', 'phone', 'phone_bidx'], + ]; + + $problems = 0; + foreach ($checks as [$table, $scope, $source, $column]) { + try { + $rows = $con->query("SELECT id, `$source`, `$column` FROM `$table`")->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + echo " ! $table skipped: " . $e->getMessage() . "\n"; + continue; + } + + $stale = $missing = 0; + foreach ($rows as $row) { + $plain = $encryptionHelper->decryptData($row[$source] ?? null); + if ($plain === false || $plain === '') continue; + + $expected = $blind->index($scope, $plain); + if ($expected === null) continue; + + if (empty($row[$column])) $missing++; + elseif ($row[$column] !== $expected) $stale++; + } + + $problems += $stale + $missing; + $verdict = ($stale + $missing) === 0 ? 'ok' : "MISSING=$missing STALE=$stale"; + echo sprintf(" %-24s %s\n", "$table.$column", $verdict); + } + + echo $problems === 0 + ? "\n✔ every index matches its encrypted value.\n" + : "\n✘ $problems row(s) out of sync — re-run with --force to rebuild.\n"; + exit($problems === 0 ? 0 : 1); +} + $targets = [ 'driver' => [ 'phone_bidx' => ['scope' => 'driver.phone', 'columns' => ['phone']],