قبل أن يرسل Baby Monitor Timmy الصوت والفيديو، يجب أن يجد الجهازان بعضهما ويثق كل منهما بالآخر. هذه عملية الاقتران هي أكثر خطوة حساسية في العملية كلها. أشرح هنا كيف يقترن Timmy، وما التشفير المستخدم في الخلفية، ولماذا لا يستطيع مهاجم قريب الاستيلاء على الاتصال من دون أن يُكتشف.
المشكلة: كيف يعرف جهازي الطرف الذي يتحدث إليه؟
عند اتصال جهازين للمرة الأولى، يكون السؤال الأساسي: هل يتحدث الجهاز A فعلًا مع الجهاز B، أم أن هناك شخصًا يتوسط بينهما؟ في علم التشفير، يُسمى ذلك هجوم الوسيط (MITM).
يحل Timmy ذلك من خلال تبادل مفاتيح ديفي–هيلمان باستخدام المنحنيات الإهليلجية (ECDH) عبر Firebase، إلى جانب تحقق مرئي يجريه المستخدم.
يوضح المخطط التالي تسلسل الاقتران الكامل في لمحة واحدة:
sequenceDiagram
autonumber
participant A as 📱 Device A
participant F as ☁️ Firebase
participant B as 📱 Device B
Note over A,B: Phase 1 — Discovery
A->>A: Generate ECDH key pair (P-256)
B->>B: Generate ECDH key pair (P-256)
alt Auto-Pairing (Nearby BLE)
A-->>B: BLE broadcast: SBM:XKQM
B-->>A: BLE broadcast: SBM:R7NP
Note over A,B: Lower code wins → determines creator/joiner
else Manual Pairing
A->>A: Display 4-char code
Note right of A: User reads code
B->>B: User enters code
end
Note over A,B: Phase 2 — ECDH Key Exchange
A->>F: Write public key (PubA) to meeting doc
B->>F: Write public key (PubB) to meeting doc
F-->>B: Read PubA
F-->>A: Read PubB
Note over A,B: Phase 3 — Shared Secret
A->>A: sharedSecret = ECDH(privA, PubB)
B->>B: sharedSecret = ECDH(privB, PubA)
Note over A,B: Both compute identical 32-byte secret
A->>A: SAS = SHA-256("sas:" + sort(PubA,PubB) + secret) → 2-digit number
B->>B: SAS = SHA-256("sas:" + sort(PubA,PubB) + secret) → 2-digit number
Note over A,B: Phase 4 — Visual Verification
A->>A: Display SAS: 42
B->>B: Display SAS: 42
Note over A,B: 👤 User compares numbers on both screens
A->>A: User confirms ✓
B->>B: User confirms ✓
Note over A,B: Phase 5 — Key Derivation
A->>A: pairingKey = SHA-256("pair:" + secret)
B->>B: pairingKey = SHA-256("pair:" + secret)
A->>A: docKey = SHA-256("doc:" + pairingKey)
A->>A: encKey = SHA-256("enc:" + pairingKey)
Note over A,B: ✅ Paired — all future signaling encrypted with AES-256-GCM
تسلسل بروتوكول الاقتران الكامل — المصدر القابل للتعديل: docs/diagrams/pairing-sequence.mmd
الخطوة 1: ينشئ كل جهاز زوج مفاتيح
عند فتح شاشة الاقتران، ينشئ كل جهاز زوجًا مؤقتًا من مفاتيح ECDH على منحنى P-256 (secp256r1):
- مفتاح خاص — يبقى على الجهاز فقط
- مفتاح عام — يُتبادل عبر Firebase
تُنشأ المفاتيح باستخدام مولّد أرقام عشوائية آمن تشفيريًا (Random.secure())
وتكون صالحة لمحاولة الاقتران هذه فقط. تُنشأ مفاتيح جديدة
لكل محاولة جديدة.
الخطوة 2: تبادل المفاتيح العامة عبر Firebase
لكي يتمكن جهازان من العثور على بعضهما، يستخدم Timmy رمزًا من 4 أحرف كنقطة التقاء. يمكن اكتشاف هذا الرمز تلقائيًا عبر Nearby Connections (Bluetooth Low Energy) أو إدخاله يدويًا. وليس له أي قيمة تشفيرية؛ فهو فقط يجعل الجهازين يعثران على مستند Firebase Firestore نفسه.
عندما يعرف الجهازان الرمز، يكتب كل منهما مفتاح ECDH العام الخاص به في مستند Firestore. ثم يقرأ كل جهاز المفتاح العام للجهاز الآخر من ذلك المستند.
الأهم: يُرسل فقط المفتاح العام . لا يغادر المفتاح الخاص الجهاز أبدًا. من يراقب حركة Firebase يرى المفاتيح العامة، لكنه لا يستطيع حساب السر المشترك منها. ويعتمد ذلك على صعوبة مسألة اللوغاريتم المتقطع على المنحنيات الإهليلجية (ECDLP).
الخطوة 3: حساب السر المشترك
بعد أن يكتشف كل جهاز المفتاح العام للجهاز الآخر، يحسب كل منهما بشكل مستقل السر المشترك نفسه:
sharedSecret = ECDH(myPrivateKey, remotePublicKey)
→ 32 bytes (identical on both devices)
تضمن رياضيات المنحنيات الإهليلجية أن تنتج العمليتان النتيجة نفسها، رغم أن كل جهاز لا يعرف إلا مفتاحه الخاص والمفتاح العام للطرف الآخر.
الخطوة 4: رقم التحقق (SAS)
يُشتق من السر المشترك سلسلة مصادقة قصيرة (SAS) — وهو عدد مكوّن من رقمين يظهر على كلا الجهازين:
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
يعرض الجهازان الرقم نفسه — على سبيل المثال، 42. يقارن المستخدم بصريًا ما إذا كان الرقمان على الشاشتين متطابقين، ثم يؤكد على كل جهاز على حدة.
لماذا لا يستطيع المهاجم تزوير ذلك
سيحتاج مهاجم ينفّذ هجوم وسيط إلى اعتراض تبادل المفاتيح في Firebase. وبالتحديد، سيحتاج إلى:
- استبدال المفاتيح العامة الحقيقية المخزنة في مستند Firestore بمفاتيحه الخاصة
- إنشاء أسرار مشتركة منفصلة مع كل جهاز
sequenceDiagram
autonumber
participant A as 📱 Device A
participant M as 🕵️ Attacker (MITM)
participant B as 📱 Device B
Note over A,B: Attacker intercepts the Firebase key exchange
A->>A: Generate key pair (privA, PubA)
B->>B: Generate key pair (privB, PubB)
M->>M: Generate TWO key pairs (privM1, PubM1) + (privM2, PubM2)
A->>M: Write PubA to Firebase
M->>M: Replace PubA with PubM1
M->>B: B reads PubM1 (thinks it is PubA)
B->>M: Write PubB to Firebase
M->>M: Replace PubB with PubM2
M->>A: A reads PubM2 (thinks it is PubB)
Note over A,B: Each device computes a DIFFERENT shared secret
A->>A: secret_A = ECDH(privA, PubM2)
M->>M: secret_A = ECDH(privM2, PubA)
M->>M: secret_B = ECDH(privM1, PubB)
B->>B: secret_B = ECDH(privB, PubM1)
Note over A,M: secret_A ≠ secret_B
A->>A: SAS_A = SHA-256("sas:" + sort(PubA,PubM2) + secret_A) → 73
B->>B: SAS_B = SHA-256("sas:" + sort(PubM1,PubB) + secret_B) → 18
rect rgb(255, 230, 230)
Note over A,B: ❌ User sees DIFFERENT numbers!
A->>A: Display: 73
B->>B: Display: 18
Note over A,B: 👤 User notices mismatch → cancels pairing
end
Note over A,B: 🛡️ Attack detected — MITM cannot force SAS match (P = 1/100)
اكتشاف هجوم الوسيط عبر عدم تطابق SAS — المصدر القابل للتعديل: docs/diagrams/mitm-detection.mmd
في هذه الحالة، يحسب المهاجم سرًا مشتركًا S_A مع الجهاز A وسرًا
مشتركًا مختلفًا S_B مع الجهاز B. وبما أن S_A ≠ S_B،
يحسب الجهازان رقمي تحقق مختلفين.
لا يستطيع المهاجم جعل الرقمين متطابقين لأن:
- لا يعرف المفاتيح الخاصة بالأجهزة
- لا يمكن عكس SHA-256
- احتمال تطابق عشوائي هو فقط 1 من كل 100
يرى المستخدم أرقامًا مختلفة على الشاشتين ويلغي الاقتران. عندها يصبح الهجوم ظاهرًا.
الخطوة 5: إتمام الاقتران
لا يكتمل الاقتران إلا بعد أن يؤكد المستخدم التحقق على كلا الجهازين ، وذلك على النحو التالي:
- مفتاح اقتران من 64 حرفًا (256 بت) يُشتق من السر المشترك:
SHA-256("pair:" + sharedSecret) → pairingKey - يُشتق مفتاح المستند بالشكل
SHA-256("doc:" + pairingKey)ويُستخدم كمفتاح لمستند Firestore - ويُشتق مفتاح التشفير بالشكل
SHA-256("enc:" + pairingKey)ويوفر مفتاح AES-256-GCM للإشارات المشفرة - يخزن الجهازان مفتاح الاقتران نفسه وينتقلان إلى اختيار الوضع
من هذه النقطة، تُشفّر كل محاولات الاتصال اللاحقة (إشارات Firestore وإعداد WebRTC) بمفتاح AES-256-GCM المشترك. ومفتاح الاقتران لا يُرسل إلى الخادم الخلفي أبدًا؛ إذ تُستخدم فقط قيمة تجزئة SHA-256 الخاصة به معرّفًا للمستند.
بنية النظام
يوضح المخطط التالي المكونات المشاركة في الاقتران والاتصال:
flowchart TB
BABY["📱 Baby Phone
Baby Mode"]
PARENT["📱 Parent Phone
Parent Mode"]
BABY <==>|"🔒 WebRTC Peer-to-Peer · DTLS-SRTP
Audio · Video · DataChannel"| PARENT
BABY -.-|"🔵 Bluetooth LE · Nearby
Auto-Discovery"| PARENT
subgraph FIREBASE["☁️ Firebase (Google Cloud)"]
direction LR
AUTH["🪪 Anonymous
Authentication"]
FS["📄 Firestore
Pairing + Signaling"]
CF["⚡ Cloud Functions
getTurnCredentials"]
end
BABY <-->|"🔐 AES-256-GCM encrypted
SDP · ICE · ECDH keys"| FS
FS <-->|"🔐 AES-256-GCM encrypted
SDP · ICE · ECDH keys"| PARENT
BABY -.->|Token| AUTH
PARENT -.->|Token| AUTH
STUN["📡 STUN server
stun.cloudflare.com:3478"]
TURN["🔄 TURN relay
local or Cloudflare"]
BABY & PARENT -->|Short-lived credentials| CF
CF -->|local first, Cloudflare fallback| TURN
BABY & PARENT -.->|NAT Traversal| STUN
BABY -.->|"Relay Fallback"| TURN
TURN -.->|"Relay Fallback"| PARENT
style BABY fill:#FBF6F0,stroke:#B5734A,stroke-width:2px
style PARENT fill:#FBF6F0,stroke:#B5734A,stroke-width:2px
style FIREBASE fill:#fff5f5,stroke:#E9B44C,stroke-width:2px
style AUTH fill:#E9B44C,stroke:#2B2D42
style FS fill:#E9B44C,stroke:#2B2D42
style CF fill:#E9B44C,stroke:#2B2D42
style STUN fill:#F6E3D2,stroke:#B5734A
style TURN fill:#7BC47F,stroke:#2B2D42
نظرة عامة على بنية النظام — المصدر القابل للتعديل: docs/diagrams/pairing-architecture.mmd
مسارات الاتصال بالتفصيل:
- اتصال WebRTC مباشر بين الجهازين (خط عريض): تنتقل تدفقات الصوت والفيديو وDataChannel مباشرةً بين الجهازين، وتكون مشفّرة باستخدام DTLS-SRTP. ولا يطّلع أي خادم على هذه البيانات.
- Firebase Firestore (خط متصل): تمر بيانات الاقتران (مفاتيح ECDH) والإشارات (SDP/ICE) عبر Firestore — مشفرةً من طرف إلى طرف بـ AES-256-GCM. لا يستطيع Firebase فك تشفير البيانات.
- خادم STUN: يكتشف الجهازان عنوان IP العام الخاص بهما لكي يمكن إنشاء اتصال مباشر بين الجهازين.
- مرحّل TURN: إذا تعذر الاتصال المباشر (مثلًا عند استخدام بيانات الجوال)، يمرّر خادم TURN المحدد، سواء كان محليًا أو تابعًا لـ Cloudflare، الوسائط المشفرة. وتُجلب بيانات اعتماد قصيرة الأجل (24 ساعة) عبر Firebase Cloud Functions.
- Bluetooth LE (خط منقط): يكتشف Nearby Connections الأجهزة القريبة تلقائيًا — ولا يُرسل إلا رمز الالتقاء، من دون إرسال أي بيانات خاصة بالمفاتيح.
بديل: إدخال الرمز يدويًا
إذا لم يتوفر Bluetooth (مثلًا في الأجهزة القديمة)، يمكن أيضًا كتابة الرمز المكوّن من 4 أحرف يدويًا. يستخدم الإدخال اليدوي تبادل مفاتيح ECDH نفسه والتحقق من SAS نفسه كما في الاقتران التلقائي. والفرق الوحيد هو أن المستخدم يقرأ الرمز ويكتبه بدلًا من اكتشافه عبر BLE.
لأن تبادل مفاتيح ECDH يحدث عبر Firebase في كلتا الحالتين، فإن مستوى الأمان متطابق. الرمز المكوّن من 4 أحرف ليس سوى نقطة التقاء؛ أما التشفير الحقيقي فيعتمد على مفتاح 256 بت المشتق من ECDH.
الخلاصة
| آلية الأمان | تحمي من |
|---|---|
| تبادل مفاتيح ECDH (P-256) | التنصت على حركة تبادل المفاتيح |
| أزواج مفاتيح مؤقتة | السرية التقدمية — تبقى عمليات الاقتران السابقة آمنة |
| رقم التحقق المرئي (SAS) | هجوم الوسيط (MITM) أثناء تبادل المفاتيح |
| قيمة تجزئة SHA-256 كمفتاح للمستند | استخراج الرمز من Firestore |
| تشفير AES-256-GCM | التنصت على بيانات الإشارات |
| تأكيد من الطرفين | اقتران من طرف واحد من دون علم المستخدم |
| DTLS-SRTP (WebRTC) | التنصت على الصوت/الفيديو |
تتكامل هذه الطبقات معًا: تحمي ECDH تبادل المفاتيح، ويحمي رقم التحقق من MITM، ويحمي AES-256-GCM الإشارات، ويحمي WebRTC الوسائط. وسيحتاج المهاجم إلى كسر هذه السلسلة في عدة نقاط من دون أن تلاحظ الأجهزة أو الأهل ذلك.