Πριν το Baby Monitor Timmy μεταδώσει ήχο και βίντεο, οι δύο συσκευές πρέπει να βρουν η μία την άλλη και να εμπιστευτούν η μία την άλλη. Αυτό το βήμα σύζευξης είναι η πιο κρίσιμη στιγμή σε όλη τη διαδικασία. Εδώ εξηγώ πώς κάνει σύζευξη το Timmy, ποια κρυπτογραφία χρησιμοποιεί και γιατί ένας κοντινός επιτιθέμενος δεν μπορεί να υποκλέψει τη σύνδεση χωρίς να γίνει αντιληπτός.
Το πρόβλημα: Πώς ξέρει η συσκευή μου με ποιον μιλάει;
Όταν δύο συσκευές συνδέονται για πρώτη φορά, το βασικό ερώτημα είναι: επικοινωνεί όντως η Συσκευή Α με τη Συσκευή Β ή παρεμβάλλεται κάποιος; Στην κρυπτογραφία, αυτό ονομάζεται επίθεση man-in-the-middle (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)
Από το κοινό μυστικό προκύπτει ένα Short Authentication String (SAS) — ένας διψήφιος αριθμός που εμφανίζεται και στις δύο συσκευές:
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
Και οι δύο συσκευές εμφανίζουν τον ίδιο αριθμό — για παράδειγμα, 42. Ο χρήστης συγκρίνει οπτικά αν οι αριθμοί στις δύο οθόνες ταιριάζουν και έπειτα επιβεβαιώνει σε κάθε συσκευή ξεχωριστά.
Γιατί ένας επιτιθέμενος δεν μπορεί να το πλαστογραφήσει
Ένας man-in-the-middle θα έπρεπε να υποκλέψει την ανταλλαγή κλειδιών στο 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)
Εντοπισμός man-in-the-middle μέσω ασυμφωνίας SAS — επεξεργάσιμη πηγή: docs/diagrams/mitm-detection.mmd
Σε αυτή την περίπτωση, ο επιτιθέμενος υπολογίζει ένα κοινό μυστικό S_A με τη Συσκευή Α και ένα
διαφορετικό κοινό μυστικό S_B με τη Συσκευή Β. Εφόσον S_A ≠ S_B,
οι συσκευές υπολογίζουν διαφορετικούς αριθμούς επαλήθευσης.
Ο επιτιθέμενος δεν μπορεί να κάνει τους αριθμούς να ταιριάξουν, επειδή:
- Δεν γνωρίζει τα ιδιωτικά κλειδιά των συσκευών
- Το SHA-256 δεν αντιστρέφεται
- Η πιθανότητα τυχαίας αντιστοίχισης είναι μόλις 1 στις 100
Ο χρήστης βλέπει διαφορετικούς αριθμούς στις οθόνες και ακυρώνει τη σύζευξη. Σε εκείνο το σημείο, η επίθεση έχει γίνει ορατή.
Βήμα 5: Ολοκλήρωση της σύζευξης
Μόνο αφού ο χρήστης επιβεβαιώσει την επαλήθευση και στις δύο συσκευές ολοκληρώνεται η σύζευξη:
- Ένα κλειδί σύζευξης 64 χαρακτήρων (256 bit) προκύπτει από το κοινό μυστικό:
SHA-256("pair:" + sharedSecret) → pairingKey - Το κλειδί εγγράφου προκύπτει ως
SHA-256("doc:" + pairingKey)και χρησιμοποιείται ως κλειδί εγγράφου Firestore - Το κλειδί κρυπτογράφησης προκύπτει ως
SHA-256("enc:" + pairingKey)και παρέχει το κλειδί AES-256-GCM για κρυπτογραφημένη σηματοδότηση - Και οι δύο συσκευές αποθηκεύουν το ίδιο κλειδί σύζευξης και μεταβαίνουν στην επιλογή λειτουργίας
Από αυτό το σημείο και μετά, όλες οι επόμενες προσπάθειες σύνδεσης (σηματοδότηση Firestore, ρύθμιση WebRTC) κρυπτογραφούνται με το κοινό κλειδί AES-256-GCM. Το κλειδί σύζευξης δεν αποστέλλεται ποτέ στο backend· μόνο το hash 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
Οι διαδρομές επικοινωνίας αναλυτικά:
- Peer-to-peer WebRTC (παχιά γραμμή): Ο ήχος, το βίντεο και το DataChannel ρέουν απευθείας μεταξύ των συσκευών — κρυπτογραφημένα με DTLS-SRTP. Κανένας διακομιστής δεν βλέπει αυτά τα δεδομένα.
- Firebase Firestore (συνεχής γραμμή): Τα δεδομένα σύζευξης (κλειδιά ECDH) και η σηματοδότηση (SDP/ICE) περνούν από το Firestore — κρυπτογραφημένα end-to-end με AES-256-GCM. Το Firebase δεν μπορεί να αποκρυπτογραφήσει τα δεδομένα.
- Διακομιστής STUN: Και οι δύο συσκευές εντοπίζουν τη δημόσια διεύθυνση IP τους, ώστε να μπορεί να δημιουργηθεί απευθείας σύνδεση peer-to-peer.
- Αναμετάδοση TURN: Αν δεν είναι δυνατή μια απευθείας σύνδεση (π.χ. μέσω δεδομένων κινητής), ο επιλεγμένος τοπικός διακομιστής TURN ή ο Cloudflare TURN αναμεταδίδει τα κρυπτογραφημένα πολυμέσα. Προσωρινά διαπιστευτήρια (24 ώρες) λαμβάνονται μέσω Firebase Cloud Functions.
- Bluetooth LE (διακεκομμένη γραμμή): Το Nearby Connections εντοπίζει αυτόματα κοντινές συσκευές — μεταδίδεται μόνο ο κωδικός συνάντησης, χωρίς υλικό κλειδιών.
Εναλλακτική: Χειροκίνητη εισαγωγή κωδικού
Αν το Bluetooth δεν είναι διαθέσιμο (π.χ. σε παλαιότερες συσκευές), ο 4-χαρακτήρων κωδικός μπορεί επίσης να πληκτρολογηθεί χειροκίνητα. Η χειροκίνητη εισαγωγή χρησιμοποιεί την ίδια ανταλλαγή κλειδιών ECDH και την ίδια επαλήθευση SAS όπως και η αυτόματη σύζευξη. Η μόνη διαφορά είναι ότι ο χρήστης διαβάζει και πληκτρολογεί τον κωδικό αντί να εντοπίζεται μέσω BLE.
Επειδή η ανταλλαγή κλειδιών ECDH γίνεται μέσω Firebase και στις δύο περιπτώσεις, η ασφάλεια είναι ίδια. Ο 4-χαρακτήρων κωδικός είναι μόνο ένα σημείο συνάντησης· η πραγματική κρυπτογράφηση βασίζεται στο κλειδί 256 bit που προκύπτει από το ECDH.
Σύνοψη
| Μηχανισμός ασφαλείας | Προστατεύει από |
|---|---|
| Ανταλλαγή κλειδιών ECDH (P-256) | Υποκλοπή της κίνησης ανταλλαγής κλειδιών |
| Προσωρινά ζεύγη κλειδιών | Πρόσθια μυστικότητα — οι προηγούμενες συζεύξεις παραμένουν ασφαλείς |
| Αριθμός οπτικής επαλήθευσης (SAS) | Man-in-the-middle (MITM) κατά την ανταλλαγή κλειδιών |
| Hash SHA-256 ως κλειδί εγγράφου | Εξαγωγή κωδικού από το Firestore |
| Κρυπτογράφηση AES-256-GCM | Υποκλοπή δεδομένων σηματοδότησης |
| Επιβεβαίωση και από τις δύο πλευρές | Μονόπλευρη σύζευξη χωρίς να το γνωρίζει ο χρήστης |
| DTLS-SRTP (WebRTC) | Υποκλοπή ήχου/βίντεο |
Αυτά τα επίπεδα συνεργάζονται: το ECDH προστατεύει την ανταλλαγή κλειδιών, ο αριθμός επαλήθευσης προστατεύει από MITM, το AES-256-GCM προστατεύει τη σηματοδότηση και το WebRTC προστατεύει τα πολυμέσα. Ένας επιτιθέμενος θα έπρεπε να σπάσει αυτή την αλυσίδα σε πολλά σημεία χωρίς να το αντιληφθούν οι συσκευές ή οι γονείς.