בלוג

צימוד מאובטח ב-Baby Monitor Timmy

איך ECDH, מספר ה-SAS והאיתות המוצפן פועלים יחד.

לפני ש-Baby Monitor Timmy מעביר אודיו ווידאו, שני המכשירים צריכים למצוא זה את זה ולתת אמון זה בזה. שלב ה צימוד הוא הרגע הקריטי ביותר בכל התהליך. כאן אסביר כיצד Timmy מבצע צימוד, איזו קריפטוגרפיה עומדת מאחוריו, ומדוע תוקף שנמצא בקרבת מקום לא יכול להשתלט על החיבור בלי שיבחינו בכך.

הבעיה: איך המכשיר שלי יודע עם מי הוא מדבר?

כששני מכשירים מתחברים בפעם הראשונה, השאלה המרכזית היא: האם מכשיר א׳ באמת מדבר עם מכשיר ב׳, או שמישהו נמצא באמצע? בקריפטוגרפיה, זה נקרא מתקפת אדם באמצע (MITM).

Timmy פותר את זה באמצעות החלפת מפתחות Elliptic Curve Diffie-Hellman ‏(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 עם מכשיר א׳ וסוד משותף אחר S_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 מגן על המדיה. תוקף יצטרך לשבור את השרשרת הזו בכמה נקודות בלי שהמכשירים או ההורים יבחינו בכך.


מאמרים נוספים