# ترحيل intaleqDBV2 → v3 (CBC القديم → GCM + الفهارس العمياء) دراسة مبنيّة على مقارنة فعلية بين `intaleqDBV2.sql` و `backend/schema_primary.sql` والكود الحيّ في `backend/auth/**`, `backend/serviceapp/**`, `backend/Admin/**`. --- ## 1. نطاق الترحيل — الجداول التي فيها بيانات فعلاً من 91 جدولاً في `intaleqDBV2.sql`، **12 فقط** تحوي `INSERT`: | الجدول | عدد عبارات INSERT | يُرحَّل تلقائياً؟ | |---|---|---| | `passengers` | 42 | ✅ | | `driver` | 24 | ✅ | | `tokens` | 20 | ⏸ opt-in | | `phone_verification` | 13 | ⏸ opt-in | | `driverToken` | 12 | ⏸ opt-in | | `CarRegistration` | 9 | ✅ | | `phone_verification_passenger` | 7 | ⏸ opt-in | | `token_verification_driver` | 2 | ⏸ opt-in | | `users` | 1 | ✅ | | `token_verification` | 1 | ⏸ opt-in | | `token_verification_admin` | 1 | ⏸ opt-in | | `employee` | 1 | ✅ | **لماذا opt-in؟** `tokens`/`driverToken` جلسات JWT، وجداول `*_verification` رموز OTP صلاحيتها 5 دقائق وكلها منتهية منذ شهور. ترحيلها بلا فائدة، وفيه خطر لأن صيغة المفتاح تغيّرت (انظر §4). الأسلم: إهمالها وإجبار إعادة تسجيل دخول واحدة. من أرادها: `--table=tokens`. --- ## 2. التشفير القديم — ما هو موجود فعلاً في v2 فحص العيّنات يثبت **AES-256-CBC حتمي بـ IV ثابت**: نفس النص يعطي نفس الـ ciphertext عبر كل الصفوف (مثلاً `1CBSKV8j4EKo8w0zjY4zcf8P4nbtMK+zhKcLtuN13no=` في `driver.gender` لكل السائقين، و `YJdUvEUnHWCdG/NAVZ3r/GyWXSYQr49nMGabOu0KAyc=` في ستة أعمدة من `passengers`). لو كان الـ IV عشوائياً لاختلفت كل قيمة. لكن في الكود **صيغتان** كانتا تكتبان في نفس الأعمدة: | المصدر | الصيغة | |---|---| | `core/Security/EncryptionHelper::encryptDataCBC` | `base64(cipher)` — IV ثابت، بلا بادئة | | `encrypt_decrypt.php::encryptData` (الجذر) | `base64(iv ‖ cipher)` — IV عشوائي مُلحق | ⚠️ **`EncryptionHelper::decryptData` الجديد لا يعرف الصيغة الثانية إطلاقاً** — يجرّب الـ IV الثابت فقط ويرجع `false`. أي ترحيل يعتمد عليه وحده سيُخرج صفوفاً فارغة بصمت. لذلك السكربت يحمل فكّاكاً خاصاً (`LegacyDecryptor`) يجرّب الصيغتين. ⚠️ **الصيغتان غير قابلتين للتمييز بشكل قاطع**: في CBC يؤثّر الـ IV على البلوك الأول فقط، فأي قيمة من بلوكين فأكثر تمرّ في المسارين بحشو صحيح. المصفاة الوحيدة هي صلاحية UTF-8. لذلك السكربت **لا يخمّن لكل قيمة**، بل يعتمد صيغة مفضّلة (`--legacy-format=fixed` افتراضاً، مبنيّة على الدليل أعلاه) ويعدّ الحالات الملتبسة ويعرضها في التقرير. ### نتيجة الفحص الفعلي على كامل `intaleqDBV2.sql` شُغّل الفكّاك على كل قيمة مشفّرة في الجداول الخمسة المستهدفة بالمفتاح والـ IV الفعليين (32 و16 بايت): ``` driver 1447 صفاً كل الأعمدة cbc_fixed passengers 2891 صفاً كل الأعمدة cbc_fixed users 1 صف cbc_fixed CarRegistration 1500 صفاً cbc_fixed ───────────────────────────────────────────── cbc_fixed=50364 cbc_random=0 FAILED=0 ``` **الخلاصة: الصيغة `fixed` هي المستعملة حصراً — لا وجود لصيغة الـ IV العشوائي في البيانات.** الالتباس النظري المذكور أعلاه لم يقع عملياً (المسار العشوائي لم ينجح في أي قيمة)، فالافتراضي `--legacy-format=fixed` صحيح ولا حاجة لتغييره. **شذوذات مكتشفة (كلها يعالجها السكربت):** | الحالة | العدد | المعالجة | |---|---|---| | `driver.phone` مخزَّن نصّاً عادياً | 1 | يُشفَّر ويُفهرَس كغيره | | `CarRegistration.car_plate` نصّ عادي | 6 | تُشفَّر | | `CarRegistration.vin` = `sdf` / `unknown` | 237 | `unknown` يمرّ كما هو، `sdf` يُشفَّر | | `driver.site` = `demascus` نصّ عادي | 56 | يُشفَّر | | `driver.fullNameMaritial` = `yet`/NULL | 1447 | يمرّ كما هو | القيمة المتكرّرة في ستة أعمدة من `passengers` فُكّت إلى **`unknown`** — أي أن الكود القديم كان يكتب قيمة موحّدة، وهو ما يفعله `$unknown_encrypted` في `auth/passenger/register.php` اليوم. سلوك مقصود لا خلل. ### الأعمدة المشفّرة (مؤكَّدة بالعيّنات) | الجدول | مشفّر | نصّ عادي | |---|---|---| | `driver` | phone, email, gender, national_number, name_arabic, address, birthdate, site, first_name, last_name, fullNameMaritial | password (bcrypt), id (hex), التواريخ, license_type, status | | `passengers` | phone, email, gender, birthdate, site, first_name, last_name, sosPhone, education, employmentType, maritalStatus | password, id, status | | `users` | fingerprint, phone, email, first_name, last_name | gender, birthdate, site, password, status, user_type | | `CarRegistration` | vin, car_plate, owner | driverID, make, model, color, fuel, … | | `employee` | — (كله نصّ عادي) | الكل | | جداول OTP | phone_number, token/token_code, email | التواريخ, verified | **تنبيه:** قيَم مثل `yet` / `none` / `sos` مخزَّنة نصّاً عادياً ويقارنها الكود حرفياً (`WHERE email = 'yet'`). تشفيرها يكسر تلك المقارنات — السكربت يمرّرها كما هي (`SENTINELS`). --- ## 3. التشفير الجديد — ما الذي يكتبه النظام اليوم - **التخزين:** `AES-256-GCM` ببادئة `GCM:` و IV عشوائي 12 بايت + tag 16 بايت. مفعّل بـ `ENCRYPTION_MODE=gcm`؛ القراءة تدعم الصيغتين تلقائياً. - **البحث:** `BlindIndex` = `HMAC-SHA256(scope:normalized_value, BLIND_INDEX_PEPPER)`. الـ scope يشمل الجدول والحقل عمداً حتى لا يُربط نفس الرقم بين `driver` و`passengers`. التطبيع يوحّد `07…` و `+9627…`، ويوحّد أشكال الألف/الياء/التاء المربوطة للأسماء. - **ربط جداول التحقق:** `otpPhoneKey()` = `'K:' . index('otp.phone', $phone)`. - **بصمة الجهاز:** `fingerprint` مشفّر + `fingerprint_hash = sha256(البصمة الخام)`. السكربت يفرض GCM على الكتابة بغضّ النظر عن `ENCRYPTION_MODE` — هذا غرضه. --- ## 4. فجوات المخطط — يجب سدّها قبل الترحيل هذه ليست ملاحظات تجميلية؛ اثنتان منها **تكسران الإنتاج اليوم** بمعزل عن الترحيل. 1. **`driver` ينقصه `name_bidx` و `phone_key`** — `auth/driver/register.php:444` يُدخلهما، فالتسجيل يفشل. نفس الشيء لـ `passengers` (`auth/passenger/register.php:133`). 2. **`token_verification_admin`: `phone_number VARCHAR(20)`, `token VARCHAR(10)`** — بينما `auth/otp/request.php:131` يكتب مفتاح `K:` بطول 66 حرفاً و OTP بـ GCM بطول ~90. النتيجة قطع صامت ⇒ **لا يمكن التحقق من أي OTP للأدمن**. 3. **`users` في `schema_primary` قديم جداً** — ينقصه `fingerprint`, `fingerprint_hash`, `status`, `country`, `phone_bidx`, `email_bidx` (يستخدمها `serviceapp/register.php:76`, `serviceapp/login.php:24`, `Admin/Staff/add.php:71`). كما أن `phone VARCHAR(15)` لا يتّسع لنصّ مشفّر. 4. **فهارس UNIQUE على أعمدة مشفّرة تفقد معناها تحت GCM**: `driver.national_number`, `passengers (phone, email)`, `users.email/phone`. التشفير العشوائي يجعل نفس الرقم ينتج نصاً مختلفاً كل مرة، فالـ UNIQUE يمنع تكرار *النص المشفّر* لا تكرار *الرقم* — أي يسمح بحسابين بنفس الهاتف. البديل: UNIQUE على `*_bidx`. كلها في `migrations/2026_07_29_v2_migration_schema_fixes.sql`. **تعارض جانبي:** `2026_07_25_blind_index.sql` يضيف `phone_bidx`/`email_bidx` لـ `driver`/`passengers`/`adminUser`، وهي موجودة أصلاً في `schema_primary.sql`. على قاعدة جديدة من `schema_primary` تخطَّ تلك المايغريشن (عدا جزء `adminUser.status` المعلَّق في آخر ملف الإصلاحات). --- ## 5. البيانات المفقودة عمداً - `driver.api_key` / `api_secret` و `passengers.*` و `users.*` — أُسقطت من المخطط الجديد لصالح جدول `api_keys` المستقل. لا تُنقل. - `users.status` موجود في v2 (`approved`) ويُعاد إدخاله بعد إضافة العمود. --- ## 6. خطوات التنفيذ ```bash # 0) نسخة احتياطية من الهدف mysqldump "$DB_PRIMARY_NAME_V2" > backup_before_migration.sql ``` ```bash # 1) سدّ فجوات المخطط mysql "$DB_PRIMARY_NAME_V2" < backend/migrations/2026_07_29_v2_migration_schema_fixes.sql ``` ```bash # 2) تأكيد أن المفتاح والـ IV صحيحان — لا تتقدّم إن لم يكن FAILED=0 php backend/scripts/migrate_v2_reencrypt.php --probe ``` ```bash # 3) تشغيل جاف php backend/scripts/migrate_v2_reencrypt.php --dry-run ``` ```bash # 4) التنفيذ الفعلي php backend/scripts/migrate_v2_reencrypt.php --truncate ``` ```bash # 5) التدقيق — يفحص GCM والانحراف في الفهارس php backend/scripts/migrate_v2_reencrypt.php --verify ``` ```bash # 6) تفعيل الكتابة بـ GCM في .env ثم إعادة تحميل PHP-FPM # ENCRYPTION_MODE=gcm ``` السكربت قابل لإعادة التشغيل: يتخطّى الصفوف الموجودة بالمفتاح الأساسي ما لم تُمرَّر `--force`، ويعمل على دفعات داخل معاملات مع فاصل قصير. --- ## 7. ما يبقى للمراجعة البشرية - **`BLIND_INDEX_PEPPER` لا يُغيَّر بعد الترحيل أبداً.** كل قيَم `*_bidx` و`phone_key` مشتقّة منه؛ تغييره لاحقاً يُبطل كل عمليات البحث وتسجيل الدخول دفعةً واحدة، ويتطلّب إعادة بناء الفهارس كاملة (`backfill_blind_index.php --force`). - **`driver.site` يحمل نفس ciphertext الخاص بـ `address`** في بعض الصفوف — يستحق نظرة قبل الاعتماد على `site` في التوجيه الجغرافي. ليس من شأن الترحيل إصلاحه. - **مفاتيح التشفير لا تُخزَّن في المستودع.** السكربت يقرأها من `ENCRYPTION_KEY_PATH`/`ENC_KEY` و`initializationVector` وقت التشغيل فقط.