feat: المجموعة E (كشف الاحتيال) + تشفير الهاتف at-rest
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1eb79fee99
commit
253fbb4b2a
+10
-6
@@ -17,15 +17,19 @@ name: string;
|
||||
```
|
||||
|
||||
## المطبّق حالياً
|
||||
- `users.name` · `drivers.vehicle_plate`.
|
||||
- `users.name` · `users.phone` · `drivers.vehicle_plate`.
|
||||
|
||||
## الحقول القابلة للبحث (مثل الهاتف) — الحل الصحيح
|
||||
تشفير GCM عشوائي **يمنع المطابقة بالتساوي** (كل تشفير مختلف). للبحث نستخدم **فهرس أعمى (blind index)**: عمود إضافي = `HMAC-SHA256(القيمة)` حتمي للبحث + العمود المشفّر للتخزين. مخطّط عند بناء بحث الهاتف/الهوية.
|
||||
## الحقول القابلة للبحث (مثل الهاتف) — ✅ منفَّذ (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:`) — فلا تنكسر القراءة على بيانات قديمة؛ والكتابة الجديدة تُشفّر.
|
||||
- **عند نقل بيانات حقيقية من سيرو:** تُقرأ مفكوكةً من سيرو ثم تُكتب فتُشفَّر عندنا تلقائياً.
|
||||
- **لا تغيير مخطّط لـ`varchar`:** الأعمدة تتّسع للنص المشفّر (base64). لا هجرة لتحويل النوع.
|
||||
- **تسامح انتقالي:** `decrypt` يُرجع النص القديم غير المشفّر كما هو (يبدأ بلا `v1:`) — فلا تنكسر القراءة على بيانات قديمة.
|
||||
- **الهاتف استثناء متعمَّد لهذا التسامح**: هجرة `EncryptPhone` تُشفّر الصفوف القائمة **فوراً** (لا تتركها صافية للأبد بالاعتماد على `decrypt` المتسامح) — لأن `phone` عمود بحث نشط يومياً (كل تسجيل دخول)، ونصّه الصافي في القاعدة أخطر من `name` (نادراً ما يُقرأ مباشرة كنص خام). نفس الهجرة تبني `phone_bidx` بأثر رجعي لكل صف قائم.
|
||||
- **عند نقل بيانات حقيقية من سيرو:** تُقرأ مفكوكةً من سيرو ثم تُكتب فتُشفَّر عندنا تلقائياً — ويجب تمريرها عبر `PhoneService.normalize` أولاً قبل الكتابة، وإلا فسدت البصمة.
|
||||
|
||||
## ما يُشفَّر مستقبلاً
|
||||
وثائق السائق وأرقام الهوية/الرخصة (وحدة الوثائق)، ووجهات السحب الحساسة، وأي PII يُضاف — بنفس المحوّل.
|
||||
|
||||
@@ -76,12 +76,12 @@
|
||||
|
||||
---
|
||||
|
||||
## المجموعة E — كشف الاحتيال (من `driver_ride_scam`)
|
||||
## المجموعة E — كشف الاحتيال (من `driver_ride_scam`) — ✅ منفَّذة
|
||||
|
||||
| # | البند | التفصيل |
|
||||
|---|-------|---------|
|
||||
| E1 | **تسجيل زر الاتصال** | `isDriverCallPassenger` لكل رحلة (سيرو). |
|
||||
| E2 | **ربط الاتصال بالإلغاء** | اتصال ثم إلغاء = مؤشر اتفاق خارج التطبيق. **3 إلغاءات/يوم → إنذار**؛ التكرار → إجراء. |
|
||||
| # | البند | الحالة |
|
||||
|---|-------|--------|
|
||||
| E1 | **تسجيل زر الاتصال** | ✅ `driver_called_passenger` + `last_call_at`/`last_call_by`/`call_count` على الرحلة نفسها (لا Redis فقط — حقيقة دائمة تفيد التحقيق بعد انتهاء الرحلة). يُسجَّل عند `call:offer` في `RealtimeGateway` — لحظة **بدء** الاتصال الفعلية، لا `answer`/`ice`/`end` (إشارات تفاوض لاحقة لنفس المكالمة). |
|
||||
| E2 | **ربط الاتصال بالإلغاء** | ✅ `FraudService.recordCallThenCancel` — عدّاد **يومي منفصل** عن عدّاد الإلغاء العام (ذاك بالساعة ويعدّ كل إلغاء؛ هذا باليوم ويعدّ فقط الإلغاء **المسبوق باتصال خلال 30 دقيقة**). **3/يوم = إنذار (لا يمنع الإلغاء) — 6/يوم = حظر مؤقت**، طبق طلب المالك حرفياً. يُنادى من `TripsService.cancel` بقراءة خفيفة لعمود واحد قبل التحديث. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user