לפני ש-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):
- מפתח פרטי — נשאר אך ורק במכשיר
- מפתח ציבורי — מוחלף דרך 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 עם מכשיר א׳ וסוד משותף
אחר S_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 מגן על המדיה. תוקף יצטרך לשבור את השרשרת הזו בכמה נקודות בלי שהמכשירים או ההורים יבחינו בכך.