# 16 — التشفير (معيار at-rest) > تصحيح لخطأ سيرو الموثّق (CBC بـ IV ثابت). المعيار عندنا: **AES-256-GCM + IV عشوائي لكل قيمة**. ## المعيار - **الخوارزمية:** AES-256-GCM (تشفير موثَّق authenticated — يكشف أي عبث). - **IV:** عشوائي **96-بت لكل عملية** (`randomBytes(12)`) — **لا IV ثابت إطلاقاً**. - **صيغة التخزين:** `v1:base64(iv | authTag | ciphertext)` — الإصدار في المقدمة للترقية المستقبلية. - **المفتاح:** `ENCRYPTION_KEY` (32 بايت hex؛ `openssl rand -hex 32`). إلزامي للإنتاج؛ بدونه مفتاح تطوير غير آمن + تحذير عند الإقلاع. - **الكود:** `common/crypto/crypto.util.ts` — `encrypt` / `decrypt` / `blindIndex` / `EncryptedTransformer`. ## كيف يُطبَّق (بلا تعقيد) عبر **TypeORM transformer** على العمود: يشفّر عند الكتابة ويفكّ عند القراءة **تلقائياً**. لا يتغير منطق الخدمات ولا استجابات الـ API (تظهر مفكوكة). ```ts @Column({ type: 'varchar', nullable: true, transformer: EncryptedTransformer }) name: string; ``` ## المطبّق حالياً - `users.name` · `users.phone` · `drivers.vehicle_plate`. ## الحقول القابلة للبحث (مثل الهاتف) — ✅ منفَّذ (docs/17 — D1 إضافة) تشفير GCM عشوائي **يمنع المطابقة بالتساوي** (كل تشفير مختلف) — `WHERE phone = :x` لا يطابق أبداً حتى مع نفس الرقم. الحل: **فهرس أعمى (blind index)** — `users.phone_bidx = HMAC-SHA256(canonicalPhone)` حتمي، عمود إضافي غير مشفَّر (بصمة أحادية الاتجاه، لا تكشف الرقم). كل بحث وتفرّد على الهاتف يمرّ عبره لا عبر `phone`: - `UsersService.findByPhone` يحسب `blindIndex(phone)` ويبحث به. - القيد الفريد `(tenant_id, phone_bidx)` — لا `(tenant_id, phone)` (ذاك كان سيقبل تكرار نفس الرقم الحقيقي لأن كل تشفير مختلف شكلاً). - **يعتمد على تطبيع D1**: البصمة حتمية لنفس *النص*، فلا بد أن يصل `phone` مطبَّعاً (`common/phone/phone.service.ts`) قبل الوصول لهذا الكود — وإلا اعتُبر نفس الرقم بصيغتين مختلفتين رقمين مختلفين مرة أخرى. ## الميغريشن (صحيح ومتوافق) - **لا تغيير مخطّط لـ`varchar`:** الأعمدة تتّسع للنص المشفّر (base64). لا هجرة لتحويل النوع. - **تسامح انتقالي:** `decrypt` يُرجع النص القديم غير المشفّر كما هو (يبدأ بلا `v1:`) — فلا تنكسر القراءة على بيانات قديمة. - **الهاتف استثناء متعمَّد لهذا التسامح**: هجرة `EncryptPhone` تُشفّر الصفوف القائمة **فوراً** (لا تتركها صافية للأبد بالاعتماد على `decrypt` المتسامح) — لأن `phone` عمود بحث نشط يومياً (كل تسجيل دخول)، ونصّه الصافي في القاعدة أخطر من `name` (نادراً ما يُقرأ مباشرة كنص خام). نفس الهجرة تبني `phone_bidx` بأثر رجعي لكل صف قائم. - **عند نقل بيانات حقيقية من سيرو:** تُقرأ مفكوكةً من سيرو ثم تُكتب فتُشفَّر عندنا تلقائياً — ويجب تمريرها عبر `PhoneService.normalize` أولاً قبل الكتابة، وإلا فسدت البصمة. ## ما يُشفَّر مستقبلاً وثائق السائق وأرقام الهوية/الرخصة (وحدة الوثائق)، ووجهات السحب الحساسة، وأي PII يُضاف — بنفس المحوّل. ← يُقرأ مع: [14-server-conventions](14-server-conventions.md) · [08-data-model](08-data-model.md)