Trước khi Baby Monitor Timmy truyền âm thanh và hình ảnh, hai thiết bị cần tìm thấy nhau và tin cậy nhau. Quá trình này ghép nối là thời điểm quan trọng nhất trong toàn bộ quy trình. Ở đây, tôi giải thích cách Timmy ghép nối, cơ chế mật mã phía sau và vì sao kẻ tấn công ở gần không thể âm thầm chiếm quyền kết nối.
Vấn đề: Thiết bị của tôi biết đang kết nối với ai bằng cách nào?
Khi hai thiết bị kết nối lần đầu, câu hỏi cốt lõi là: Thiết bị A có thực sự đang nói chuyện với Thiết bị B không, hay có ai đó chen vào giữa? Trong mật mã học, đó được gọi là tấn công kẻ đứng giữa (MITM).
Timmy giải quyết việc này bằng trao đổi khóa Elliptic Curve Diffie-Hellman (ECDH) qua Firebase, kết hợp với xác minh trực quan bởi người dùng.
Sơ đồ sau cho thấy toàn bộ quy trình ghép nối trong nháy mắt:
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
Trình tự đầy đủ của giao thức ghép nối — nguồn có thể chỉnh sửa: docs/diagrams/pairing-sequence.mmd
Bước 1: Mỗi thiết bị tạo một cặp khóa
Khi mở màn hình ghép nối, mỗi thiết bị tạo một cặp khóa ECDH tạm thời trên đường cong P-256 (secp256r1):
- Một khóa riêng — chỉ nằm trên thiết bị
- Một khóa công khai — được trao đổi qua Firebase
Các khóa được tạo bằng trình tạo số ngẫu nhiên an toàn về mặt mật mã (Random.secure())
và chỉ có hiệu lực cho một lần ghép nối này. Các khóa mới được tạo
cho mỗi lần thử mới.
Bước 2: Trao đổi khóa công khai qua Firebase
Để hai thiết bị tìm thấy nhau, Timmy dùng một mã gồm 4 ký tự làm điểm gặp. Mã này có thể được tìm tự động qua Nearby Connections (Bluetooth Low Energy) hoặc nhập thủ công. Mã này không có giá trị mật mã; nó chỉ giúp cả hai thiết bị tìm đến cùng một tài liệu Firebase Firestore.
Khi cả hai thiết bị biết mã, mỗi thiết bị ghi khóa ECDH công khai của mình vào một tài liệu Firestoredùng chung. Sau đó, mỗi thiết bị đọc khóa công khai của thiết bị kia từ tài liệu đó.
Điều quan trọng: chỉ khóa công khai được gửi đi. Khóa riêng không bao giờ rời khỏi thiết bị. Bất kỳ ai theo dõi lưu lượng Firebase đều thấy khóa công khai, nhưng không thể tính được bí mật chung từ các khóa đó. Điều này dựa trên độ khó của Bài toán logarit rời rạc trên đường cong elliptic (ECDLP).
Bước 3: Tính bí mật chung
Khi cả hai thiết bị đã tìm thấy khóa công khai của nhau, chúng độc lập tính ra cùng một bí mật chung:
sharedSecret = ECDH(myPrivateKey, remotePublicKey)
→ 32 bytes (identical on both devices)
Toán học của đường cong elliptic bảo đảm hai phép tính cho cùng một kết quả, dù mỗi thiết bị chỉ biết khóa riêng của mình và khóa công khai của thiết bị kia.
Bước 4: Số xác minh (SAS)
Từ bí mật chung, một Chuỗi xác thực ngắn (SAS) được tạo ra — một số có hai chữ số hiển thị trên cả hai thiết bị:
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
Cả hai thiết bị hiển thị cùng một số — ví dụ: 42. Người dùng nhìn để so sánh xem các số trên hai màn hình có khớp không, rồi xác nhận trên từng thiết bị.
Vì sao kẻ tấn công không thể giả mạo điều này
Kẻ đứng giữa sẽ phải chặn việc trao đổi khóa trong Firebase. Cụ thể, họ sẽ cần:
- Thay thế các khóa công khai thật được lưu trong tài liệu Firestore bằng khóa của họ
- Thiết lập các bí mật chung riêng biệt với từng thiết bị
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)
Phát hiện kẻ đứng giữa qua SAS không khớp — nguồn có thể chỉnh sửa: docs/diagrams/mitm-detection.mmd
Trong trường hợp này, kẻ tấn công tính một bí mật chung S_A với Thiết bị A và một
bí mật chung khác S_B với Thiết bị B. Vì S_A ≠ S_B,
các thiết bị tính ra các số xác minh khác nhau.
Kẻ tấn công không thể làm cho các số trùng nhau vì:
- Kẻ tấn công không biết khóa riêng của các thiết bị
- SHA-256 không thể đảo ngược
- Xác suất trùng ngẫu nhiên chỉ là 1 trên 100
Người dùng thấy các số khác nhau trên màn hình và hủy ghép nối. Lúc đó, cuộc tấn công đã bị lộ.
Bước 5: Hoàn tất ghép nối
Chỉ sau khi người dùng đã xác nhận việc xác minh trên cả hai thiết bị thì quá trình ghép nối mới hoàn tất:
- Một khóa ghép nối 64 ký tự (256 bit) được tạo từ bí mật chung:
SHA-256("pair:" + sharedSecret) → pairingKey - Khóa tài liệu được tạo dưới dạng
SHA-256("doc:" + pairingKey)và dùng làm khóa tài liệu Firestore - Khóa mã hóa được tạo dưới dạng
SHA-256("enc:" + pairingKey)và cung cấp khóa AES-256-GCM để mã hóa dữ liệu trao đổi tín hiệu - Cả hai thiết bị lưu cùng một khóa ghép nối và chuyển đến phần chọn chế độ
Từ thời điểm này, mọi lần kết nối tiếp theo (trao đổi tín hiệu qua Firestore, thiết lập WebRTC) đều được mã hóa bằng khóa AES-256-GCM dùng chung. Khóa ghép nối không bao giờ được gửi đến hệ thống máy chủ; chỉ mã băm SHA-256 của nó được dùng làm mã định danh tài liệu.
Kiến trúc hệ thống
Sơ đồ sau cho thấy các thành phần tham gia vào việc ghép nối và giao tiếp:
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
Tổng quan kiến trúc hệ thống — nguồn có thể chỉnh sửa: docs/diagrams/pairing-architecture.mmd
Chi tiết các đường truyền giao tiếp:
- WebRTC ngang hàng (đường đậm): Âm thanh, video và DataChannel truyền trực tiếp giữa các thiết bị — được mã hóa bằng DTLS-SRTP. Không máy chủ nào nhìn thấy dữ liệu này.
- Firebase Firestore (đường liền): Dữ liệu ghép nối (khóa ECDH) và dữ liệu trao đổi tín hiệu (SDP/ICE) đi qua Firestore — được mã hóa đầu cuối bằng AES-256-GCM. Firebase không thể giải mã dữ liệu.
- Máy chủ STUN: Cả hai thiết bị tìm địa chỉ IP công khai của mình để có thể thiết lập kết nối ngang hàng trực tiếp.
- Máy chủ chuyển tiếp TURN: Nếu không thể kết nối trực tiếp (ví dụ khi dùng dữ liệu di động), máy chủ TURN cục bộ hoặc Cloudflare được chọn sẽ chuyển tiếp nội dung đa phương tiện đã mã hóa. Thông tin xác thực ngắn hạn (24 giờ) được lấy qua Firebase Cloud Functions.
- Bluetooth LE (đường chấm): Nearby Connections tự động tìm các thiết bị ở gần — chỉ mã điểm gặp được truyền đi, không có dữ liệu khóa.
Phương án dự phòng: Nhập mã thủ công
Nếu Bluetooth không khả dụng (ví dụ trên các thiết bị cũ), mã 4 ký tự cũng có thể được nhập thủ công. Việc nhập thủ công dùng cùng cơ chế trao đổi khóa ECDH và cùng bước xác minh SAS như ghép nối tự động. Điểm khác biệt duy nhất là người dùng đọc và nhập mã thay vì mã được tìm thấy qua BLE.
Vì việc trao đổi khóa ECDH đều diễn ra qua Firebase trong cả hai trường hợp, mức độ bảo mật là giống hệt nhau. Mã 4 ký tự chỉ là điểm gặp; việc mã hóa thực sự dựa trên khóa 256 bit được tạo từ ECDH.
Tóm tắt
| Cơ chế bảo mật | Bảo vệ khỏi |
|---|---|
| Trao đổi khóa ECDH (P-256) | Nghe lén lưu lượng trao đổi khóa |
| Cặp khóa tạm thời | Bảo mật chuyển tiếp — các lần ghép nối trước vẫn an toàn |
| Số xác minh trực quan (SAS) | Kẻ đứng giữa (MITM) trong lúc trao đổi khóa |
| Mã băm SHA-256 làm khóa tài liệu | Trích xuất mã từ Firestore |
| Mã hóa AES-256-GCM | Nghe lén dữ liệu trao đổi tín hiệu |
| Xác nhận từ cả hai bên | Ghép nối một phía mà người dùng không biết |
| DTLS-SRTP (WebRTC) | Nghe lén âm thanh/video |
Các lớp này phối hợp với nhau: ECDH bảo vệ việc trao đổi khóa, số xác minh bảo vệ khỏi MITM, AES-256-GCM bảo vệ dữ liệu trao đổi tín hiệu và WebRTC bảo vệ nội dung đa phương tiện. Kẻ tấn công sẽ phải phá vỡ chuỗi bảo vệ này ở nhiều điểm mà thiết bị hoặc cha mẹ không nhận ra.