المدونة

الاقتران الآمن في Baby Monitor Timmy

كيف تعمل ECDH ورقم SAS والإشارات المشفرة معًا.

قبل أن يرسل 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):

تُنشأ المفاتيح باستخدام مولّد أرقام عشوائية آمن تشفيريًا (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. وبالتحديد، سيحتاج إلى:

  1. استبدال المفاتيح العامة الحقيقية المخزنة في مستند Firestore بمفاتيحه الخاصة
  2. إنشاء أسرار مشتركة منفصلة مع كل جهاز
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، يحسب الجهازان رقمي تحقق مختلفين.

لا يستطيع المهاجم جعل الرقمين متطابقين لأن:

يرى المستخدم أرقامًا مختلفة على الشاشتين ويلغي الاقتران. عندها يصبح الهجوم ظاهرًا.

الخطوة 5: إتمام الاقتران

لا يكتمل الاقتران إلا بعد أن يؤكد المستخدم التحقق على كلا الجهازين ، وذلك على النحو التالي:

  1. مفتاح اقتران من 64 حرفًا (256 بت) يُشتق من السر المشترك: SHA-256("pair:" + sharedSecret) → pairingKey
  2. يُشتق مفتاح المستند بالشكل SHA-256("doc:" + pairingKey) ويُستخدم كمفتاح لمستند Firestore
  3. ويُشتق مفتاح التشفير بالشكل SHA-256("enc:" + pairingKey) ويوفر مفتاح AES-256-GCM للإشارات المشفرة
  4. يخزن الجهازان مفتاح الاقتران نفسه وينتقلان إلى اختيار الوضع

من هذه النقطة، تُشفّر كل محاولات الاتصال اللاحقة (إشارات 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

مسارات الاتصال بالتفصيل:

بديل: إدخال الرمز يدويًا

إذا لم يتوفر 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 الوسائط. وسيحتاج المهاجم إلى كسر هذه السلسلة في عدة نقاط من دون أن تلاحظ الأجهزة أو الأهل ذلك.


مقالات أخرى