E — اتصال ثم إلغاء (من driver_ride_scam عند سيرو): - E1: driver_called_passenger + last_call_at/by/count على الرحلة نفسها لا Redis فقط — حقيقة دائمة تفيد التحقيق بعد انتهاء الرحلة وزوالها من الكاش. يُسجَّل عند call:offer (بدء المكالمة الفعلي) في RealtimeGateway عبر Repository<Trip> مباشرة — لا استيراد TripsModule كاملاً (يستورد RealtimeModule أصلاً، فدورة استيراد) - E2: FraudService.recordCallThenCancel — عدّاد يومي منفصل عن عدّاد الإلغاء العام بالساعة. 3/يوم = إنذار لا يمنع الإلغاء، 6/يوم = حظر مؤقت (طلب المالك حرفياً). يرتبط فقط إن كانت المكالمة خلال 30 دقيقة من الإلغاء تشفير الهاتف at-rest (ملاحظة المالك) — بفهرس أعمى منفصل، لا تشفير ساذج: - AES-256-GCM عشوائي IV يمنع WHERE phone = :x من المطابقة أصلاً (كل تشفير مختلف شكلاً حتى لنفس الرقم) — لو طُبِّق التشفير مباشرة بلا هذا لكسر تسجيل الدخول بالكامل بصمت - users.phone_bidx = HMAC-SHA256(الرقم المطبَّع) — حتمي، يصلح للبحث وللقيد الفريد. القيد انتقل من (tenant_id, phone) إلى (tenant_id, phone_bidx) — القديم كان سيقبل تكرار نفس الرقم الحقيقي فعلياً - findByPhone/create يحسبان البصمة؛ لا نقطة أخرى تقرأ phone بمساواة خام - هجرة EncryptPhone تُشفّر الصفوف القائمة فوراً وتبني البصمة بأثر رجعي — استثناء متعمَّد من تسامح decrypt() المعتاد (الهاتف عمود بحث يومي، خطره كنص صافٍ أعلى من name) - يعتمد على تطبيع D1: بصمة حتمية لنفس *النص*، فلا بد أن يصل الرقم مطبَّعاً قبلها وإلا عاد نفس الرقم بصيغتين ليصير رقمين مرة أخرى هجرات: TripCallSignal · EncryptPhone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.0 KiB
4.0 KiB
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 (تظهر مفكوكة).
@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 · 08-data-model