Update: 2026-07-29 16:36:26
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 2026_07_29_v2_migration_schema_fixes.sql
|
||||||
|
--
|
||||||
|
-- فجوات بين schema_primary.sql والكود الحيّ، يجب سدّها *قبل* تشغيل
|
||||||
|
-- scripts/migrate_v2_reencrypt.php وإلا فشل الإدخال أو فشل تسجيل الدخول.
|
||||||
|
--
|
||||||
|
-- كل تعليمة هنا نتيجة مقارنة فعلية بين:
|
||||||
|
-- - CREATE TABLE في backend/schema_primary.sql
|
||||||
|
-- - أعمدة INSERT/SELECT في auth/driver/register.php, auth/passenger/register.php,
|
||||||
|
-- serviceapp/register.php, serviceapp/login.php, Admin/auth/*, auth/otp/*
|
||||||
|
-- - scripts/backfill_blind_index.php (خريطة الفهارس المعتمدة)
|
||||||
|
--
|
||||||
|
-- التنفيذ على قاعدة v3 الجديدة (الفارغة) قبل الترحيل.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- 1) driver — الكود يُدخل name_bidx و phone_key وهما غير موجودين
|
||||||
|
-- (auth/driver/register.php:444) → INSERT يفشل اليوم.
|
||||||
|
-- national_bidx مطلوب من backfill_blind_index.php.
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE `driver`
|
||||||
|
ADD COLUMN `name_bidx` VARCHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للاسم الكامل بعد التطبيع',
|
||||||
|
ADD COLUMN `national_bidx` VARCHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للرقم الوطني',
|
||||||
|
ADD COLUMN `phone_key` VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط جداول التحقق — نفس صيغة otpPhoneKey()',
|
||||||
|
ADD KEY `idx_driver_name_bidx` (`name_bidx`),
|
||||||
|
ADD KEY `idx_driver_national_bidx` (`national_bidx`),
|
||||||
|
ADD KEY `idx_driver_phone_key` (`phone_key`);
|
||||||
|
|
||||||
|
-- UNIQUE على national_number يصبح بلا معنى تحت GCM (نفس الرقم يُنتج نصاً
|
||||||
|
-- مختلفاً كل مرة) — بل يمنع فقط تكرار *النص المشفّر* لا تكرار الرقم.
|
||||||
|
-- البديل الصحيح: UNIQUE على الفهرس الأعمى.
|
||||||
|
ALTER TABLE `driver` DROP INDEX `national_number`;
|
||||||
|
ALTER TABLE `driver` ADD UNIQUE KEY `uniq_driver_national_bidx` (`national_bidx`);
|
||||||
|
ALTER TABLE `driver` ADD UNIQUE KEY `uniq_driver_phone_bidx` (`phone_bidx`);
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- 2) passengers — نفس المشكلة (auth/passenger/register.php:133)
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE `passengers`
|
||||||
|
ADD COLUMN `name_bidx` VARCHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للاسم الكامل بعد التطبيع',
|
||||||
|
ADD COLUMN `phone_key` VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط جداول التحقق — نفس صيغة otpPhoneKey()',
|
||||||
|
ADD KEY `idx_passengers_name_bidx` (`name_bidx`),
|
||||||
|
ADD KEY `idx_passengers_phone_key` (`phone_key`);
|
||||||
|
|
||||||
|
-- UNIQUE (phone, email) على نصّين مشفّرين عشوائياً لا يمنع تكرار الحساب.
|
||||||
|
ALTER TABLE `passengers` DROP INDEX `phone`;
|
||||||
|
ALTER TABLE `passengers` ADD UNIQUE KEY `uniq_passengers_phone_bidx` (`phone_bidx`);
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- 3) users — نسخة schema_primary قديمة جداً مقارنة بالكود:
|
||||||
|
-- ينقصها fingerprint / fingerprint_hash / status / country /
|
||||||
|
-- email_bidx / phone_bidx (serviceapp/register.php:76,
|
||||||
|
-- serviceapp/login.php:24, Admin/Staff/add.php:71).
|
||||||
|
-- كما أن phone VARCHAR(15) لا يتّسع لنص مشفّر (≈44 حرفاً بـ CBC،
|
||||||
|
-- ≈90 بـ GCM) — الإدخال سيُقطع أو يفشل.
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE `users`
|
||||||
|
MODIFY COLUMN `phone` VARCHAR(255) NULL DEFAULT NULL,
|
||||||
|
MODIFY COLUMN `email` VARCHAR(255) NULL DEFAULT NULL,
|
||||||
|
MODIFY COLUMN `password` VARCHAR(255) NULL DEFAULT NULL,
|
||||||
|
MODIFY COLUMN `first_name` VARCHAR(255) NULL DEFAULT NULL,
|
||||||
|
MODIFY COLUMN `last_name` VARCHAR(255) NULL DEFAULT NULL,
|
||||||
|
MODIFY COLUMN `birthdate` DATE NOT NULL DEFAULT '2002-01-01',
|
||||||
|
MODIFY COLUMN `site` VARCHAR(255) NOT NULL DEFAULT 'Syria',
|
||||||
|
MODIFY COLUMN `gender` VARCHAR(10) NOT NULL DEFAULT 'Female',
|
||||||
|
ADD COLUMN `fingerprint` TEXT NULL DEFAULT NULL COMMENT 'بصمة الجهاز مشفرة',
|
||||||
|
ADD COLUMN `fingerprint_hash` VARCHAR(64) NULL DEFAULT NULL COMMENT 'sha256(fingerprint) للبحث',
|
||||||
|
ADD COLUMN `status` VARCHAR(20) NULL DEFAULT 'pending',
|
||||||
|
ADD COLUMN `country` VARCHAR(100) NULL DEFAULT 'Jordan',
|
||||||
|
ADD COLUMN `phone_bidx` VARCHAR(64) NULL DEFAULT NULL,
|
||||||
|
ADD COLUMN `email_bidx` VARCHAR(64) NULL DEFAULT NULL,
|
||||||
|
ADD KEY `idx_users_fp_hash` (`fingerprint_hash`),
|
||||||
|
ADD KEY `idx_users_phone_bidx` (`phone_bidx`),
|
||||||
|
ADD KEY `idx_users_email_bidx` (`email_bidx`);
|
||||||
|
|
||||||
|
-- UNIQUE على نصّ مشفّر عشوائي بلا فائدة (وسيرفض NULL المتكرر فقط).
|
||||||
|
ALTER TABLE `users` DROP INDEX `email`;
|
||||||
|
ALTER TABLE `users` DROP INDEX `phone`;
|
||||||
|
ALTER TABLE `users` ADD UNIQUE KEY `uniq_users_phone_bidx` (`phone_bidx`);
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- 4) token_verification_admin — أخطر فجوة: الكود يكتب اليوم
|
||||||
|
-- phone_number = otpPhoneKey() ≈ 66 حرفاً، و token = GCM ≈ 90 حرفاً
|
||||||
|
-- (auth/otp/request.php:131) بينما العمودان VARCHAR(20)/VARCHAR(10).
|
||||||
|
-- النتيجة: قطع صامت للقيمة ⇒ لا يمكن التحقق من أي OTP للأدمن.
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE `token_verification_admin`
|
||||||
|
MODIFY COLUMN `phone_number` VARCHAR(120) NOT NULL,
|
||||||
|
MODIFY COLUMN `token` VARCHAR(255) NOT NULL,
|
||||||
|
ADD COLUMN `verified` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN `created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
-- auth/otp/verify.php:88 يحدّث verified، وrequest.php يعتمد ON DUPLICATE KEY
|
||||||
|
-- على phone_number، فالـ UNIQUE يبقى لازماً بعد توسيع العمود.
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- 5) جداول التحقق الأخرى — أعمدة المفتاح تتّسع للـ K:hmac (66 حرفاً)
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE `phone_verification`
|
||||||
|
MODIFY COLUMN `phone_number` VARCHAR(120) NOT NULL,
|
||||||
|
MODIFY COLUMN `token_code` VARCHAR(255) NULL DEFAULT NULL,
|
||||||
|
MODIFY COLUMN `email` VARCHAR(255) NOT NULL DEFAULT 'yet',
|
||||||
|
ADD KEY `idx_pv_phone_number` (`phone_number`);
|
||||||
|
|
||||||
|
ALTER TABLE `phone_verification_passenger`
|
||||||
|
MODIFY COLUMN `phone_number` VARCHAR(120) NULL DEFAULT NULL,
|
||||||
|
ADD KEY `idx_pvp_phone_number` (`phone_number`);
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- 6) adminUser — العمودان status/approved_* موجودان في
|
||||||
|
-- 2026_07_25_blind_index.sql لكن schema_primary لا يحويهما.
|
||||||
|
-- نفّذ هذا فقط إن لم تكن المايغريشن السابقة قد طُبّقت.
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- ALTER TABLE `adminUser`
|
||||||
|
-- ADD COLUMN `status` VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||||
|
-- ADD COLUMN `approved_by` VARCHAR(32) NULL DEFAULT NULL,
|
||||||
|
-- ADD COLUMN `approved_at` TIMESTAMP NULL DEFAULT NULL;
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# ترحيل 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` وقت التشغيل فقط.
|
||||||
@@ -0,0 +1,619 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* scripts/migrate_v2_reencrypt.php
|
||||||
|
*
|
||||||
|
* ترحيل بيانات intaleqDBV2 (التشفير القديم AES-256-CBC) إلى قاعدة v3
|
||||||
|
* (schema_primary.sql) بالتشفير الجديد AES-256-GCM + الفهارس العمياء.
|
||||||
|
*
|
||||||
|
* لماذا سكربت وليس mysqldump؟ لأن النص المشفّر نفسه هو ما يجب أن يتغيّر:
|
||||||
|
* كل قيمة تُفكّ بالمفتاح القديم ثم يُعاد تشفيرها عشوائياً، وتُشتقّ منها
|
||||||
|
* أعمدة جديدة (phone_bidx, name_bidx, phone_key, fingerprint_hash) لا وجود
|
||||||
|
* لها في المصدر ولا يمكن حسابها إلا والقيمة مكشوفة لحظةً واحدة داخل الذاكرة.
|
||||||
|
*
|
||||||
|
* ─── الاستخدام ───────────────────────────────────────────────
|
||||||
|
* php migrate_v2_reencrypt.php --dry-run # تحليل بلا كتابة (ابدأ من هنا)
|
||||||
|
* php migrate_v2_reencrypt.php --probe # فحص المفتاح على عيّنة فقط
|
||||||
|
* php migrate_v2_reencrypt.php --table=driver # جدول واحد
|
||||||
|
* php migrate_v2_reencrypt.php --truncate # تفريغ الهدف قبل الإدخال
|
||||||
|
* php migrate_v2_reencrypt.php # تنفيذ فعلي
|
||||||
|
* php migrate_v2_reencrypt.php --verify # تدقيق بعد الترحيل
|
||||||
|
*
|
||||||
|
* ─── متغيّرات البيئة المطلوبة ─────────────────────────────────
|
||||||
|
* المصدر (القديم): SRC_DB_HOST SRC_DB_NAME SRC_DB_USER SRC_DB_PASS
|
||||||
|
* الهدف (الجديد): DB_PRIMARY_*_V2 (عبر Database::get('main'))
|
||||||
|
* المفتاح: ENCRYPTION_KEY_PATH أو ENC_KEY (32 بايت — نفسه للقديم والجديد)
|
||||||
|
* الـ IV القديم: initializationVector (16 بايت — الثابت القديم)
|
||||||
|
* الفهرس الأعمى: BLIND_INDEX_PEPPER
|
||||||
|
*
|
||||||
|
* ملاحظة مهمة: هذا السكربت يفرض GCM على الكتابة بغضّ النظر عن
|
||||||
|
* ENCRYPTION_MODE، لأن الغاية منه هي التحويل نفسه.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit("This script runs from the command line only.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../core/bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../core/Security/BlindIndex.php';
|
||||||
|
|
||||||
|
$opt = getopt('', ['dry-run', 'probe', 'verify', 'truncate', 'force',
|
||||||
|
'table::', 'limit::', 'batch::', 'legacy-format::']);
|
||||||
|
$dryRun = isset($opt['dry-run']);
|
||||||
|
$onlyTable = $opt['table'] ?? null;
|
||||||
|
$limit = isset($opt['limit']) ? max(1, (int) $opt['limit']) : 0;
|
||||||
|
$batch = max(50, (int) ($opt['batch'] ?? 500));
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 1) فكّاك التشفير القديم
|
||||||
|
// ============================================================
|
||||||
|
/**
|
||||||
|
* قاعدة v2 تحوي *ثلاث* صيغ متداخلة، لأن ملفين مختلفين كانا يكتبان فيها:
|
||||||
|
*
|
||||||
|
* أ) CBC بـ IV ثابت، بلا بادئة → core/Security/EncryptionHelper::encryptDataCBC
|
||||||
|
* ب) CBC بـ IV عشوائي مُلحق في الأول → encrypt_decrypt.php::encryptData (الجذر)
|
||||||
|
* ج) نص عادي (yet / none / تواريخ / hex ids)
|
||||||
|
*
|
||||||
|
* الصيغة (ب) هي الفخّ: EncryptionHelper الجديد لا يعرفها إطلاقاً (يجرّب الـ IV
|
||||||
|
* الثابت فقط)، فلو رحّلنا اعتماداً عليه لخرجت الصفوف بقيَم false صامتة.
|
||||||
|
*
|
||||||
|
* ⚠️ الصيغتان غير قابلتين للتمييز بالحشو وحده: في CBC يؤثّر الـ IV على البلوك
|
||||||
|
* الأول فقط، فأي نصّ من بلوكين فأكثر يمرّ في المسارين بحشو صحيح — أحدهما
|
||||||
|
* يُنتج 16 بايت قمامة في البداية والآخر يبتر أول بلوك. لذلك:
|
||||||
|
* - نتحقّق من صلاحية UTF-8 (تُسقط القمامة في الغالب الأعمّ)، و
|
||||||
|
* - نعتمد صيغة مفضّلة صريحة (--legacy-format) بدل التخمين لكل قيمة، و
|
||||||
|
* - نعدّ الحالات الملتبسة ونعرضها حتى لا يمرّ خطأ صامت.
|
||||||
|
* الافتراضي `fixed` لأن بيانات v2 تُظهر نصوصاً مشفّرة متطابقة لنفس القيمة
|
||||||
|
* (نفس ciphertext للجنس/الموقع عبر آلاف الصفوف) وهذا برهان على IV ثابت.
|
||||||
|
*/
|
||||||
|
final class LegacyDecryptor
|
||||||
|
{
|
||||||
|
public int $statFixedIv = 0;
|
||||||
|
public int $statRandomIv = 0;
|
||||||
|
public int $statGcm = 0;
|
||||||
|
public int $statPlain = 0;
|
||||||
|
public int $statFailed = 0;
|
||||||
|
public int $statAmbiguous = 0;
|
||||||
|
|
||||||
|
/** @param string $prefer 'fixed'|'random' */
|
||||||
|
public function __construct(private string $key, private string $iv, private string $prefer = 'fixed') {}
|
||||||
|
|
||||||
|
/** @return array{0:string,1:string} [plaintext, format] — format ∈ cbc_fixed|cbc_random|gcm|plain|fail */
|
||||||
|
public function decrypt(?string $value): array
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '') return ['', 'plain'];
|
||||||
|
|
||||||
|
// مشفّر بالنظام الجديد أصلاً (إعادة تشغيل السكربت مثلاً)
|
||||||
|
if (str_starts_with($value, 'GCM:')) {
|
||||||
|
global $encryptionHelper;
|
||||||
|
$p = $encryptionHelper->decryptData($value);
|
||||||
|
if ($p !== false) { $this->statGcm++; return [$p, 'gcm']; }
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = base64_decode($value, true);
|
||||||
|
if ($decoded !== false && strlen($decoded) >= 16 && strlen($decoded) % 16 === 0) {
|
||||||
|
$fixed = $this->tryCbc($decoded, $this->iv);
|
||||||
|
$random = strlen($decoded) >= 32
|
||||||
|
? $this->tryCbc(substr($decoded, 16), substr($decoded, 0, 16))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if ($fixed !== null && $random !== null) $this->statAmbiguous++;
|
||||||
|
|
||||||
|
$first = $this->prefer === 'random' ? $random : $fixed;
|
||||||
|
$second = $this->prefer === 'random' ? $fixed : $random;
|
||||||
|
|
||||||
|
if ($first !== null) {
|
||||||
|
$this->prefer === 'random' ? $this->statRandomIv++ : $this->statFixedIv++;
|
||||||
|
return [$first, $this->prefer === 'random' ? 'cbc_random' : 'cbc_fixed'];
|
||||||
|
}
|
||||||
|
if ($second !== null) {
|
||||||
|
$this->prefer === 'random' ? $this->statFixedIv++ : $this->statRandomIv++;
|
||||||
|
return [$second, $this->prefer === 'random' ? 'cbc_fixed' : 'cbc_random'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// لم يفكّ: إمّا نص عادي (yet/none/hex id) وهذا الغالب، أو تلف فعلي.
|
||||||
|
// نميّز بينهما بشكل القيمة: ما يبدو base64 بطول بلوك ولم يفكّ = مشكلة.
|
||||||
|
if ($decoded !== false && strlen($decoded) >= 32 && strlen($decoded) % 16 === 0
|
||||||
|
&& preg_match('#^[A-Za-z0-9+/]+={0,2}$#', $value)) {
|
||||||
|
$this->statFailed++;
|
||||||
|
return [$value, 'fail'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->statPlain++;
|
||||||
|
return [$value, 'plain'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function tryCbc(string $payload, string $iv): ?string
|
||||||
|
{
|
||||||
|
$out = openssl_decrypt($payload, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA, $iv);
|
||||||
|
if ($out === false || $out === '') return null;
|
||||||
|
|
||||||
|
$pad = ord($out[strlen($out) - 1]);
|
||||||
|
if ($pad < 1 || $pad > 16 || $pad > strlen($out)) return null;
|
||||||
|
// تحقق من صحة كل بايتات الحشو — بدونه يمرّ فكّ خاطئ بنسبة 1/16
|
||||||
|
if (substr($out, -$pad) !== str_repeat(chr($pad), $pad)) return null;
|
||||||
|
|
||||||
|
$plain = substr($out, 0, -$pad);
|
||||||
|
// المفتاح الخاطئ يُنتج بايتات عشوائية؛ صلاحية UTF-8 هي المصفاة الأخيرة
|
||||||
|
if ($plain !== '' && !mb_check_encoding($plain, 'UTF-8')) return null;
|
||||||
|
|
||||||
|
return $plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 2) خريطة الترحيل
|
||||||
|
// ============================================================
|
||||||
|
/**
|
||||||
|
* لكل جدول:
|
||||||
|
* src/dst : اسم الجدول في المصدر/الهدف
|
||||||
|
* pk : المفتاح المستخدم لكشف التكرار عند إعادة التشغيل
|
||||||
|
* copy : أعمدة تُنقل كما هي
|
||||||
|
* encrypt : أعمدة تُفكّ ثم يُعاد تشفيرها بـ GCM
|
||||||
|
* otp_key : أعمدة تُفكّ ثم تُحوَّل إلى مفتاح otpPhoneKey() الحتمي
|
||||||
|
* derive : أعمدة جديدة تُحسب من نصّ صريح
|
||||||
|
* drop : أعمدة مصدر مقصود إسقاطها (توثيق فقط)
|
||||||
|
*
|
||||||
|
* `sentinels` قيَم نصّية ثابتة يقارنها الكود حرفياً (WHERE email = 'yet').
|
||||||
|
* تشفيرها يكسر تلك المقارنات، فتمرّ كما هي.
|
||||||
|
*/
|
||||||
|
const SENTINELS = ['yet', 'none', 'sos', 'unknown', 'null', 'NULL', '0', 'active', 'notDeleted'];
|
||||||
|
|
||||||
|
$MAP = [
|
||||||
|
|
||||||
|
// ── driver ────────────────────────────────────────────────
|
||||||
|
'driver' => [
|
||||||
|
'pk' => 'id',
|
||||||
|
'copy' => ['idn','id','password','license_type','issue_date','expiry_date',
|
||||||
|
'license_categories','licenseIssueDate','status','accountBank','bankCode',
|
||||||
|
'employmentType','maritalStatus','expirationDate','created_at','updated_at'],
|
||||||
|
'encrypt' => ['phone','email','gender','national_number','name_arabic','address',
|
||||||
|
'birthdate','site','first_name','last_name','fullNameMaritial'],
|
||||||
|
'derive' => [
|
||||||
|
'phone_bidx' => ['bidx', 'driver.phone', ['phone']],
|
||||||
|
'email_bidx' => ['bidx', 'driver.email', ['email']],
|
||||||
|
'name_bidx' => ['bidx', 'driver.name', ['first_name','last_name']],
|
||||||
|
'national_bidx' => ['bidx', 'driver.national', ['national_number']],
|
||||||
|
'phone_key' => ['otpkey', 'otp.phone', ['phone']],
|
||||||
|
],
|
||||||
|
// api_key/api_secret أُسقطا من المخطط الجديد — مفاتيح HMAC انتقلت إلى
|
||||||
|
// جدول api_keys المستقل. البيانات القديمة لا تُنقل عمداً.
|
||||||
|
'drop' => ['api_key','api_secret'],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── passengers ────────────────────────────────────────────
|
||||||
|
'passengers' => [
|
||||||
|
'pk' => 'id',
|
||||||
|
'copy' => ['id','password','status','created_at','updated_at'],
|
||||||
|
'encrypt' => ['phone','email','gender','birthdate','site','first_name','last_name',
|
||||||
|
'sosPhone','education','employmentType','maritalStatus'],
|
||||||
|
'derive' => [
|
||||||
|
'phone_bidx' => ['bidx', 'passengers.phone', ['phone']],
|
||||||
|
'email_bidx' => ['bidx', 'passengers.email', ['email']],
|
||||||
|
'name_bidx' => ['bidx', 'passengers.name', ['first_name','last_name']],
|
||||||
|
'phone_key' => ['otpkey', 'otp.phone', ['phone']],
|
||||||
|
],
|
||||||
|
'drop' => ['api_key','api_secret'],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── users (حسابات خدمة العملاء) ────────────────────────────
|
||||||
|
'users' => [
|
||||||
|
'pk' => 'id',
|
||||||
|
'copy' => ['id','gender','password','birthdate','site','created_at','updated_at',
|
||||||
|
'user_type','status'],
|
||||||
|
'encrypt' => ['fingerprint','phone','email','first_name','last_name'],
|
||||||
|
'derive' => [
|
||||||
|
// serviceapp/login.php:24 يبحث بـ sha256 للبصمة الخام — يجب أن
|
||||||
|
// يُعاد حسابه من النص الصريح لا نقله، لأن نصّ البصمة نفسه لم يتغيّر
|
||||||
|
// لكن سلسلة sha256 القديمة قد تكون محسوبة على صيغة مختلفة.
|
||||||
|
'fingerprint_hash' => ['sha256', null, ['fingerprint']],
|
||||||
|
'phone_bidx' => ['bidx', 'users.phone', ['phone']],
|
||||||
|
'email_bidx' => ['bidx', 'users.email', ['email']],
|
||||||
|
],
|
||||||
|
'drop' => ['api_key','api_secret'],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── CarRegistration ───────────────────────────────────────
|
||||||
|
'CarRegistration' => [
|
||||||
|
'pk' => 'id',
|
||||||
|
'copy' => ['id','driverID','make','model','year','expiration_date','color',
|
||||||
|
'color_hex','fuel','isDefault','created_at','status',
|
||||||
|
'vehicle_category_id','fuel_type_id'],
|
||||||
|
'encrypt' => ['vin','car_plate','owner'],
|
||||||
|
'derive' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── employee (نص عادي بالكامل — يُنقل كما هو) ───────────────
|
||||||
|
'employee' => [
|
||||||
|
'pk' => 'id',
|
||||||
|
'copy' => ['id','name','education','site','phone','created_at','status'],
|
||||||
|
'encrypt' => [],
|
||||||
|
'derive' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── tokens / driverToken (جلسات) ──────────────────────────
|
||||||
|
// ملاحظة: هذه جلسات JWT وبصمات أجهزة. الأسلم إهمالها وإجبار إعادة تسجيل
|
||||||
|
// الدخول مرة واحدة. تُرحَّل فقط عند تمرير --table صراحةً.
|
||||||
|
'tokens' => [
|
||||||
|
'pk' => 'passengerID',
|
||||||
|
'opt_in' => true,
|
||||||
|
'copy' => ['id','passengerID','status'],
|
||||||
|
'encrypt' => ['token','fingerPrint'],
|
||||||
|
'derive' => [],
|
||||||
|
],
|
||||||
|
'driverToken' => [
|
||||||
|
'pk' => 'id',
|
||||||
|
'opt_in' => true,
|
||||||
|
'copy' => ['id','captain_id','created_at'],
|
||||||
|
'encrypt' => ['token','fingerPrint'],
|
||||||
|
'derive' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── جداول OTP ─────────────────────────────────────────────
|
||||||
|
// صلاحية كل رمز 5 دقائق، وكل صفوف v2 منتهية منذ شهور. ترحيلها بلا فائدة
|
||||||
|
// وبخطر: صيغة المفتاح تغيّرت إلى otpPhoneKey() الحتمي. تُرحَّل بـ --table فقط.
|
||||||
|
'phone_verification' => [
|
||||||
|
'pk' => 'id',
|
||||||
|
'opt_in' => true,
|
||||||
|
'copy' => ['id','expiration_time','is_verified','created_at'],
|
||||||
|
'encrypt' => ['email'],
|
||||||
|
'otp_key' => ['phone_number' => 'otp.phone', 'token_code' => 'otp.phone'],
|
||||||
|
'copy_as_is' => ['driverId'],
|
||||||
|
'derive' => [],
|
||||||
|
],
|
||||||
|
'phone_verification_passenger' => [
|
||||||
|
'pk' => 'id',
|
||||||
|
'opt_in' => true,
|
||||||
|
'copy' => ['id','expiration_time','verified','created_at','status'],
|
||||||
|
'otp_key' => ['phone_number' => 'otp.phone', 'token' => 'otp.phone'],
|
||||||
|
'encrypt' => [],
|
||||||
|
'derive' => [],
|
||||||
|
],
|
||||||
|
'token_verification' => [
|
||||||
|
'pk' => 'id',
|
||||||
|
'opt_in' => true,
|
||||||
|
'copy' => ['id','expiration_time','verified','created_at'],
|
||||||
|
'otp_key' => ['phone_number' => 'otp.phone', 'token' => 'otp.phone'],
|
||||||
|
'encrypt' => [],
|
||||||
|
'derive' => [],
|
||||||
|
],
|
||||||
|
'token_verification_driver' => [
|
||||||
|
'pk' => 'id',
|
||||||
|
'opt_in' => true,
|
||||||
|
'copy' => ['id','expiration_time','verified','created_at'],
|
||||||
|
'otp_key' => ['phone_number' => 'otp.phone', 'token' => 'otp.phone'],
|
||||||
|
'encrypt' => [],
|
||||||
|
'derive' => [],
|
||||||
|
],
|
||||||
|
'token_verification_admin' => [
|
||||||
|
'pk' => 'id',
|
||||||
|
'opt_in' => true,
|
||||||
|
'copy' => ['id','expiration_time'],
|
||||||
|
// بيانات v2 هنا نصّ عادي (962798583052 / 95692) — تُحوَّل إلى صيغة
|
||||||
|
// المفتاح الجديدة كما يكتبها auth/otp/request.php اليوم.
|
||||||
|
'otp_key' => ['phone_number' => 'otp.phone'],
|
||||||
|
'encrypt' => ['token'],
|
||||||
|
'derive' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 3) التهيئة
|
||||||
|
// ============================================================
|
||||||
|
/** @var EncryptionHelper $encryptionHelper */
|
||||||
|
global $encryptionHelper;
|
||||||
|
|
||||||
|
$encKey = getenv('ENCRYPTION_KEY_PATH') && file_exists((string) getenv('ENCRYPTION_KEY_PATH'))
|
||||||
|
? trim((string) file_get_contents((string) getenv('ENCRYPTION_KEY_PATH')))
|
||||||
|
: (string) (getenv('ENC_KEY') ?: '');
|
||||||
|
|
||||||
|
$legacyIv = (string) (getenv('initializationVector') ?: '');
|
||||||
|
|
||||||
|
if (strlen($encKey) !== 32) exit("✘ ENC key must be exactly 32 bytes (got " . strlen($encKey) . ").\n");
|
||||||
|
if (strlen($legacyIv) !== 16) exit("✘ initializationVector must be exactly 16 bytes (got " . strlen($legacyIv) . ").\n");
|
||||||
|
|
||||||
|
$prefer = ($opt['legacy-format'] ?? 'fixed') === 'random' ? 'random' : 'fixed';
|
||||||
|
$legacy = new LegacyDecryptor($encKey, $legacyIv, $prefer);
|
||||||
|
echo " legacy CBC format: $prefer IV" . ($prefer === 'fixed' ? '' : ' (16-byte prefix)') . "\n";
|
||||||
|
|
||||||
|
// الكتابة بـ GCM حصراً — هذا هو الغرض، ولا نتركه لإعداد بيئة قد يكون 'cbc'
|
||||||
|
$writer = new EncryptionHelper($encKey, $legacyIv, 'gcm');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$blind = new BlindIndex();
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
exit("✘ " . $e->getMessage() . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// المصدر
|
||||||
|
$srcName = (string) (getenv('SRC_DB_NAME') ?: '');
|
||||||
|
if ($srcName === '') exit("✘ SRC_DB_NAME is not set (source = the old intaleqDBV2).\n");
|
||||||
|
$src = new PDO(
|
||||||
|
sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', getenv('SRC_DB_HOST') ?: 'localhost', $srcName),
|
||||||
|
(string) getenv('SRC_DB_USER'),
|
||||||
|
(string) getenv('SRC_DB_PASS'),
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||||
|
);
|
||||||
|
|
||||||
|
$dst = Database::get('main');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 4) أدوات مساعدة
|
||||||
|
// ============================================================
|
||||||
|
function otpKeyOf(BlindIndex $b, string $scope, string $plain): ?string
|
||||||
|
{
|
||||||
|
$i = $b->index($scope, $plain);
|
||||||
|
return $i === null ? null : 'K:' . $i; // نفس صيغة core/helpers.php::otpPhoneKey
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSentinel(?string $v): bool
|
||||||
|
{
|
||||||
|
return $v === null || $v === '' || in_array($v, SENTINELS, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetColumns(PDO $pdo, string $table): array
|
||||||
|
{
|
||||||
|
$cols = [];
|
||||||
|
foreach ($pdo->query("SHOW COLUMNS FROM `$table`") as $r) $cols[$r['Field']] = true;
|
||||||
|
return $cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 5) --probe : فحص المفتاح قبل أي شيء
|
||||||
|
// ============================================================
|
||||||
|
if (isset($opt['probe'])) {
|
||||||
|
echo "── Probing legacy key/IV against live v2 data ──\n\n";
|
||||||
|
foreach (['driver' => ['phone','email','first_name'],
|
||||||
|
'passengers' => ['phone','email','first_name'],
|
||||||
|
'CarRegistration' => ['vin','car_plate']] as $t => $fields) {
|
||||||
|
try {
|
||||||
|
$rows = $src->query("SELECT " . implode(',', array_map(fn($f) => "`$f`", $fields)) . " FROM `$t` LIMIT 20")->fetchAll();
|
||||||
|
} catch (PDOException $e) { echo " ! $t: {$e->getMessage()}\n"; continue; }
|
||||||
|
|
||||||
|
foreach ($fields as $f) {
|
||||||
|
$ok = $fail = 0; $sample = '';
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
[$p, $fmt] = $legacy->decrypt($r[$f] ?? null);
|
||||||
|
if ($fmt === 'fail') { $fail++; continue; }
|
||||||
|
if (in_array($fmt, ['cbc_fixed','cbc_random','gcm'], true)) {
|
||||||
|
$ok++;
|
||||||
|
if ($sample === '') $sample = mb_substr($p, 0, 3) . str_repeat('*', max(0, mb_strlen($p) - 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printf(" %-18s %-14s ok=%-3d fail=%-3d %s\n", $t, $f, $ok, $fail, $sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printf("\n formats seen: cbc_fixed=%d cbc_random=%d gcm=%d plain=%d FAILED=%d ambiguous=%d\n",
|
||||||
|
$legacy->statFixedIv, $legacy->statRandomIv, $legacy->statGcm,
|
||||||
|
$legacy->statPlain, $legacy->statFailed, $legacy->statAmbiguous);
|
||||||
|
if ($legacy->statAmbiguous > 0) {
|
||||||
|
echo " ! {$legacy->statAmbiguous} value(s) decrypted under BOTH IV schemes.\n"
|
||||||
|
. " Compare a sample against the live app before trusting the chosen format,\n"
|
||||||
|
. " or re-run with --legacy-format=random to see which output reads correctly.\n";
|
||||||
|
}
|
||||||
|
echo $legacy->statFailed === 0
|
||||||
|
? "\n✔ key and IV look correct — proceed with --dry-run.\n"
|
||||||
|
: "\n✘ some values did not decrypt. Do NOT migrate until this is 0.\n";
|
||||||
|
exit($legacy->statFailed === 0 ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 6) --verify : تدقيق ما بعد الترحيل
|
||||||
|
// ============================================================
|
||||||
|
if (isset($opt['verify'])) {
|
||||||
|
echo "── Verifying migrated data ──\n\n";
|
||||||
|
$bad = 0;
|
||||||
|
foreach (['driver' => ['phone','email','first_name','last_name'],
|
||||||
|
'passengers' => ['phone','email','first_name'],
|
||||||
|
'users' => ['phone','email'],
|
||||||
|
'CarRegistration' => ['vin','car_plate','owner']] as $t => $fields) {
|
||||||
|
$sel = implode(',', array_map(fn($f) => "`$f`", $fields));
|
||||||
|
try { $rows = $dst->query("SELECT $sel FROM `$t`")->fetchAll(); }
|
||||||
|
catch (PDOException $e) { echo " ! $t: {$e->getMessage()}\n"; continue; }
|
||||||
|
|
||||||
|
$gcm = $legacyLeft = $broken = 0;
|
||||||
|
foreach ($rows as $r) foreach ($fields as $f) {
|
||||||
|
$v = $r[$f] ?? '';
|
||||||
|
if (isSentinel($v)) continue;
|
||||||
|
if (str_starts_with((string) $v, 'GCM:')) {
|
||||||
|
$gcm++;
|
||||||
|
if ($writer->decryptData($v) === false) $broken++;
|
||||||
|
} else {
|
||||||
|
$legacyLeft++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$bad += $broken;
|
||||||
|
printf(" %-18s gcm=%-6d still-legacy=%-6d undecryptable=%d\n", $t, $gcm, $legacyLeft, $broken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// انحراف الفهارس: الفهرس المخزَّن مقابل المحسوب من القيمة المشفّرة
|
||||||
|
echo "\n blind-index drift:\n";
|
||||||
|
foreach ([['driver','driver.phone','phone','phone_bidx'],
|
||||||
|
['driver','driver.email','email','email_bidx'],
|
||||||
|
['passengers','passengers.phone','phone','phone_bidx'],
|
||||||
|
['passengers','passengers.email','email','email_bidx']] as [$t,$scope,$srcCol,$idxCol]) {
|
||||||
|
try { $rows = $dst->query("SELECT `$srcCol`,`$idxCol` FROM `$t`")->fetchAll(); }
|
||||||
|
catch (PDOException $e) { echo " ! $t.$idxCol: {$e->getMessage()}\n"; continue; }
|
||||||
|
|
||||||
|
$stale = $missing = 0;
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$p = $writer->decryptData($r[$srcCol] ?? null);
|
||||||
|
if ($p === false || $p === '' || isSentinel($p)) continue;
|
||||||
|
$exp = $blind->index($scope, $p);
|
||||||
|
if ($exp === null) continue;
|
||||||
|
if (empty($r[$idxCol])) $missing++;
|
||||||
|
elseif ($r[$idxCol] !== $exp) $stale++;
|
||||||
|
}
|
||||||
|
$bad += $stale + $missing;
|
||||||
|
printf(" %-26s missing=%-5d stale=%d\n", "$t.$idxCol", $missing, $stale);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $bad === 0 ? "\n✔ verification passed.\n" : "\n✘ $bad problem(s) found.\n";
|
||||||
|
exit($bad === 0 ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 7) الترحيل
|
||||||
|
// ============================================================
|
||||||
|
echo $dryRun ? "── DRY RUN — nothing will be written ──\n" : "── Migrating v2 → v3 (CBC → GCM) ──\n";
|
||||||
|
echo " source: $srcName\n\n";
|
||||||
|
|
||||||
|
$grand = ['read' => 0, 'written' => 0, 'skipped' => 0, 'failed' => 0];
|
||||||
|
|
||||||
|
foreach ($MAP as $table => $spec) {
|
||||||
|
if ($onlyTable && $onlyTable !== $table) continue;
|
||||||
|
// الجداول ذات opt_in لا تُرحَّل ضمن التشغيل الكامل — تحتاج --table صراحةً
|
||||||
|
if (!$onlyTable && !empty($spec['opt_in'])) {
|
||||||
|
printf("[%s] skipped (session/OTP table — pass --table=%s to migrate it)\n", $table, $table);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "[$table]\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dstCols = targetColumns($dst, $table);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo " ✘ target table missing: {$e->getMessage()}\n\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM `$table`" . ($limit ? " LIMIT $limit" : '');
|
||||||
|
try { $rows = $src->query($sql)->fetchAll(); }
|
||||||
|
catch (PDOException $e) { echo " ✘ source read failed: {$e->getMessage()}\n\n"; continue; }
|
||||||
|
|
||||||
|
$total = count($rows);
|
||||||
|
echo " source rows: $total\n";
|
||||||
|
if ($total === 0) { echo "\n"; continue; }
|
||||||
|
|
||||||
|
if (isset($opt['truncate']) && !$dryRun) {
|
||||||
|
$dst->exec("DELETE FROM `$table`");
|
||||||
|
echo " target emptied\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// المفاتيح الموجودة مسبقاً — يجعل السكربت قابلاً لإعادة التشغيل
|
||||||
|
$pk = $spec['pk'];
|
||||||
|
$existing = [];
|
||||||
|
if (isset($dstCols[$pk])) {
|
||||||
|
foreach ($dst->query("SELECT `$pk` FROM `$table`") as $r) $existing[(string) $r[$pk]] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$written = $skipped = $failed = 0;
|
||||||
|
$unmapped = [];
|
||||||
|
|
||||||
|
foreach (array_chunk($rows, $batch) as $chunk) {
|
||||||
|
if (!$dryRun) $dst->beginTransaction();
|
||||||
|
|
||||||
|
foreach ($chunk as $row) {
|
||||||
|
if (isset($existing[(string) ($row[$pk] ?? '')]) && !isset($opt['force'])) { $skipped++; continue; }
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
$plainMap = []; // القيَم الصريحة، لحساب الفهارس بعد التشفير
|
||||||
|
|
||||||
|
// (أ) أعمدة تُنقل كما هي
|
||||||
|
foreach (array_merge($spec['copy'] ?? [], $spec['copy_as_is'] ?? []) as $c) {
|
||||||
|
if (array_key_exists($c, $row) && isset($dstCols[$c])) $out[$c] = $row[$c];
|
||||||
|
}
|
||||||
|
|
||||||
|
// (ب) أعمدة تُفكّ ويُعاد تشفيرها
|
||||||
|
$rowFailed = false;
|
||||||
|
foreach ($spec['encrypt'] ?? [] as $c) {
|
||||||
|
if (!array_key_exists($c, $row) || !isset($dstCols[$c])) continue;
|
||||||
|
$raw = $row[$c];
|
||||||
|
|
||||||
|
if ($raw === null) { $out[$c] = null; continue; }
|
||||||
|
if (isSentinel($raw)) { $out[$c] = $raw; $plainMap[$c] = null; continue; }
|
||||||
|
|
||||||
|
[$plain, $fmt] = $legacy->decrypt((string) $raw);
|
||||||
|
if ($fmt === 'fail') {
|
||||||
|
// لا نكتب قيمة لا نفهمها ولا نُسقط الصف بصمت
|
||||||
|
$rowFailed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$plainMap[$c] = $plain;
|
||||||
|
$out[$c] = $plain === '' ? '' : $writer->encryptDataGCM($plain);
|
||||||
|
}
|
||||||
|
if ($rowFailed) { $failed++; continue; }
|
||||||
|
|
||||||
|
// (ج) أعمدة تتحوّل إلى مفتاح OTP الحتمي
|
||||||
|
foreach ($spec['otp_key'] ?? [] as $c => $scope) {
|
||||||
|
if (!array_key_exists($c, $row) || !isset($dstCols[$c])) continue;
|
||||||
|
$raw = $row[$c];
|
||||||
|
if (isSentinel($raw)) { $out[$c] = $raw; continue; }
|
||||||
|
|
||||||
|
[$plain, $fmt] = $legacy->decrypt((string) $raw);
|
||||||
|
if ($fmt === 'fail') { $rowFailed = true; break; }
|
||||||
|
$plainMap[$c] = $plain;
|
||||||
|
$out[$c] = $plain === '' ? '' : otpKeyOf($blind, $scope, $plain);
|
||||||
|
}
|
||||||
|
if ($rowFailed) { $failed++; continue; }
|
||||||
|
|
||||||
|
// (د) الأعمدة المشتقّة
|
||||||
|
foreach ($spec['derive'] ?? [] as $col => [$kind, $scope, $sources]) {
|
||||||
|
if (!isset($dstCols[$col])) { $unmapped[$col] = true; continue; }
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
foreach ($sources as $s) {
|
||||||
|
$v = $plainMap[$s] ?? null;
|
||||||
|
if ($v !== null && $v !== '') $parts[] = $v;
|
||||||
|
}
|
||||||
|
if (!$parts) { $out[$col] = null; continue; }
|
||||||
|
$value = implode(' ', $parts);
|
||||||
|
|
||||||
|
$out[$col] = match ($kind) {
|
||||||
|
'bidx' => $blind->index($scope, $value),
|
||||||
|
'otpkey' => otpKeyOf($blind, $scope, $value),
|
||||||
|
'sha256' => hash('sha256', $value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) { $written++; continue; }
|
||||||
|
|
||||||
|
$cols = array_keys($out);
|
||||||
|
$stmt = $dst->prepare(sprintf(
|
||||||
|
'INSERT INTO `%s` (%s) VALUES (%s)',
|
||||||
|
$table,
|
||||||
|
implode(',', array_map(fn($c) => "`$c`", $cols)),
|
||||||
|
implode(',', array_fill(0, count($cols), '?'))
|
||||||
|
));
|
||||||
|
try {
|
||||||
|
$stmt->execute(array_values($out));
|
||||||
|
$written++;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$failed++;
|
||||||
|
error_log("[migrate_v2] $table pk={$row[$pk]}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$dryRun) $dst->commit();
|
||||||
|
usleep(30_000);
|
||||||
|
echo " … $written/$total\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($unmapped) {
|
||||||
|
echo " ! target is missing derived column(s): " . implode(', ', array_keys($unmapped))
|
||||||
|
. " → run migrations/2026_07_29_v2_migration_schema_fixes.sql first\n";
|
||||||
|
}
|
||||||
|
printf(" ✔ written=%d skipped(existing)=%d failed=%d\n\n", $written, $skipped, $failed);
|
||||||
|
|
||||||
|
$grand['read'] += $total;
|
||||||
|
$grand['written'] += $written;
|
||||||
|
$grand['skipped'] += $skipped;
|
||||||
|
$grand['failed'] += $failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("──────────────\nread=%d written=%d skipped=%d failed=%d\n",
|
||||||
|
$grand['read'], $grand['written'], $grand['skipped'], $grand['failed']);
|
||||||
|
printf("legacy formats: cbc_fixed=%d cbc_random=%d gcm=%d plain=%d undecryptable=%d ambiguous=%d\n",
|
||||||
|
$legacy->statFixedIv, $legacy->statRandomIv, $legacy->statGcm,
|
||||||
|
$legacy->statPlain, $legacy->statFailed, $legacy->statAmbiguous);
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
echo "\n(dry run — re-run without --dry-run to apply, then --verify)\n";
|
||||||
|
} elseif ($grand['failed'] === 0) {
|
||||||
|
echo "\n✔ done. Next: php migrate_v2_reencrypt.php --verify\n";
|
||||||
|
} else {
|
||||||
|
echo "\n✘ {$grand['failed']} row(s) failed — see the PHP error log before switching traffic.\n";
|
||||||
|
}
|
||||||
Executable
+112
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# run_v2_migration.sh — تشغيل ترحيل intaleqDBV2 → intaleqDB1 على السيرفر
|
||||||
|
#
|
||||||
|
# يُشغَّل على السيرفر فقط. كل خطوة خطرة مسبوقة بفحص وقائي.
|
||||||
|
#
|
||||||
|
# bash backend/scripts/run_v2_migration.sh
|
||||||
|
#
|
||||||
|
# يتوقّع في البيئة (أو في ~/.env):
|
||||||
|
# SRC_DB_HOST SRC_DB_NAME SRC_DB_USER SRC_DB_PASS ← القديمة (intaleqDBV2)
|
||||||
|
# DB_PRIMARY_*_V2 ← الجديدة (intaleqDB1)
|
||||||
|
# ENCRYPTION_KEY_PATH أو ENC_KEY, initializationVector, BLIND_INDEX_PEPPER
|
||||||
|
# ============================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BACKEND="$(dirname "$HERE")"
|
||||||
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
BACKUP="$HOME/backup_intaleqDB1_before_migration_$STAMP.sql"
|
||||||
|
|
||||||
|
# ── تحميل .env إن وُجد ───────────────────────────────────────
|
||||||
|
for f in "$HOME/.env" "$BACKEND/.env"; do
|
||||||
|
if [[ -f "$f" ]]; then set -a; . "$f"; set +a; echo "loaded env: $f"; break; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
SRC_DB_HOST="${SRC_DB_HOST:-localhost}"
|
||||||
|
: "${SRC_DB_NAME:?SRC_DB_NAME is not set — it must be the OLD database (intaleqDBV2)}"
|
||||||
|
: "${SRC_DB_USER:?SRC_DB_USER is not set}"
|
||||||
|
: "${DB_PRIMARY_NAME_V2:?DB_PRIMARY_NAME_V2 is not set — it must be the NEW database (intaleqDB1)}"
|
||||||
|
: "${BLIND_INDEX_PEPPER:?BLIND_INDEX_PEPPER is not set}"
|
||||||
|
|
||||||
|
DST="$DB_PRIMARY_NAME_V2"
|
||||||
|
DST_HOST="${DB_PRIMARY_HOST_V2:-localhost}"
|
||||||
|
DST_USER="${DB_PRIMARY_USER_V2:?DB_PRIMARY_USER_V2 is not set}"
|
||||||
|
DST_PASS="${DB_PRIMARY_PASS_V2:-}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "══════════════════════════════════════════"
|
||||||
|
echo " SOURCE (read-only) : $SRC_DB_NAME @ $SRC_DB_HOST"
|
||||||
|
echo " TARGET (written) : $DST @ $DST_HOST"
|
||||||
|
echo "══════════════════════════════════════════"
|
||||||
|
|
||||||
|
# ── حارس الانعكاس: لا يجوز أن يكون الاثنان نفس القاعدة ──────
|
||||||
|
if [[ "$SRC_DB_NAME" == "$DST" ]]; then
|
||||||
|
echo "✘ ABORT: source and target are the same database ($DST)."
|
||||||
|
echo " SRC_DB_NAME must be the OLD db; DB_PRIMARY_NAME_V2 the NEW one."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── حارس إضافي: الهدف يجب أن يكون شبه فارغ ──────────────────
|
||||||
|
mysql_dst() { mysql -h "$DST_HOST" -u "$DST_USER" ${DST_PASS:+-p"$DST_PASS"} "$DST" "$@"; }
|
||||||
|
DRIVERS_IN_TARGET=$(mysql_dst -N -B -e "SELECT COUNT(*) FROM driver" 2>/dev/null || echo "?")
|
||||||
|
echo " target currently holds $DRIVERS_IN_TARGET driver row(s)"
|
||||||
|
if [[ "$DRIVERS_IN_TARGET" != "0" && "$DRIVERS_IN_TARGET" != "?" ]]; then
|
||||||
|
echo " ⚠️ target is NOT empty — --truncate would delete those rows."
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -r -p $'\nType the TARGET database name to confirm: ' CONFIRM
|
||||||
|
[[ "$CONFIRM" == "$DST" ]] || { echo "✘ mismatch — aborted."; exit 1; }
|
||||||
|
|
||||||
|
# ── 1) نسخة احتياطية ────────────────────────────────────────
|
||||||
|
echo
|
||||||
|
echo "── [1/6] backing up target → $BACKUP"
|
||||||
|
mysqldump -h "$DST_HOST" -u "$DST_USER" ${DST_PASS:+-p"$DST_PASS"} \
|
||||||
|
--single-transaction --routines --triggers "$DST" > "$BACKUP"
|
||||||
|
[[ -s "$BACKUP" ]] || { echo "✘ backup is empty — aborted."; exit 1; }
|
||||||
|
echo " ✔ $(du -h "$BACKUP" | cut -f1)"
|
||||||
|
|
||||||
|
# ── 2) إصلاحات المخطط ───────────────────────────────────────
|
||||||
|
echo
|
||||||
|
echo "── [2/6] applying schema fixes"
|
||||||
|
mysql_dst < "$BACKEND/migrations/2026_07_29_v2_migration_schema_fixes.sql"
|
||||||
|
echo " ✔ applied"
|
||||||
|
|
||||||
|
# ── 3) فحص المفتاح ──────────────────────────────────────────
|
||||||
|
echo
|
||||||
|
echo "── [3/6] probing legacy key/IV (must report FAILED=0)"
|
||||||
|
php "$HERE/migrate_v2_reencrypt.php" --probe
|
||||||
|
|
||||||
|
# ── 4) تشغيل جاف ────────────────────────────────────────────
|
||||||
|
echo
|
||||||
|
echo "── [4/6] dry run"
|
||||||
|
php "$HERE/migrate_v2_reencrypt.php" --dry-run
|
||||||
|
|
||||||
|
read -r -p $'\nDry run looks correct? proceed with the real migration [yes/N]: ' GO
|
||||||
|
[[ "$GO" == "yes" ]] || { echo "stopped before writing. Backup kept at $BACKUP"; exit 0; }
|
||||||
|
|
||||||
|
# ── 5) التنفيذ ──────────────────────────────────────────────
|
||||||
|
echo
|
||||||
|
echo "── [5/6] migrating"
|
||||||
|
php "$HERE/migrate_v2_reencrypt.php" --truncate
|
||||||
|
|
||||||
|
# ── 6) التدقيق ──────────────────────────────────────────────
|
||||||
|
echo
|
||||||
|
echo "── [6/6] verifying"
|
||||||
|
php "$HERE/migrate_v2_reencrypt.php" --verify
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
══════════════════════════════════════════
|
||||||
|
✔ migration finished.
|
||||||
|
|
||||||
|
backup: $BACKUP
|
||||||
|
|
||||||
|
الخطوة الأخيرة يدوياً — فعّل الكتابة بـ GCM ثم أعد تحميل PHP:
|
||||||
|
echo 'ENCRYPTION_MODE=gcm' >> ~/.env
|
||||||
|
sudo systemctl reload php8.3-fpm
|
||||||
|
|
||||||
|
للتراجع:
|
||||||
|
mysql -u $DST_USER -p $DST < $BACKUP
|
||||||
|
══════════════════════════════════════════
|
||||||
|
EOF
|
||||||
+18934
File diff suppressed because it is too large
Load Diff
@@ -299,10 +299,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
@@ -369,10 +373,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
@@ -516,12 +524,13 @@
|
|||||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.navigation";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.siro.siro-driver";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -701,12 +710,13 @@
|
|||||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.navigation";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.siro.siro-driver";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@@ -726,12 +736,13 @@
|
|||||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.navigation";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.siro.siro-driver";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|||||||
@@ -30,8 +30,6 @@
|
|||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -45,10 +43,26 @@
|
|||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>FlutterDeepLinkingEnabled</key>
|
<key>CFBundleVersion</key>
|
||||||
<true/>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>CPApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CPApplicationDriverManeuver</key>
|
||||||
|
<dict>
|
||||||
|
<key>supportsAudio</key>
|
||||||
|
<false/>
|
||||||
|
<key>supportsCommunication</key>
|
||||||
|
<false/>
|
||||||
|
<key>supportsNavigation</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>FirebaseAppDelegateProxyEnabled</key>
|
<key>FirebaseAppDelegateProxyEnabled</key>
|
||||||
<string>NO</string>
|
<string>NO</string>
|
||||||
|
<key>FlutterDeepLinkingEnabled</key>
|
||||||
|
<true/>
|
||||||
<key>GMSApiKey</key>
|
<key>GMSApiKey</key>
|
||||||
<string>YOUR_API_KEY</string>
|
<string>YOUR_API_KEY</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
@@ -95,20 +109,6 @@
|
|||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>This app needs access to your photo library to allow you to upload and manage
|
<string>This app needs access to your photo library to allow you to upload and manage
|
||||||
images.</string>
|
images.</string>
|
||||||
<key>CPApplication</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CPApplicationDriverManeuver</key>
|
|
||||||
<dict>
|
|
||||||
<key>supportsAudio</key>
|
|
||||||
<false/>
|
|
||||||
<key>supportsCommunication</key>
|
|
||||||
<false/>
|
|
||||||
<key>supportsNavigation</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
|
|||||||
Reference in New Issue
Block a user