部落格

Baby Monitor Timmy 的安全配對

瞭解 ECDH、SAS 驗證碼與加密訊號交換如何搭配運作。

Baby Monitor Timmy 傳送音訊與視訊前,兩台裝置必須先找到彼此並建立信任。 這個 配對 步驟是整個流程中最關鍵的時刻。以下說明 Timmy 如何配對、背後使用哪些密碼技術,以及為什麼附近的攻擊者無法在不被察覺的情況下接管連線。

問題:我的裝置怎麼知道它正在和誰連線?

兩台裝置第一次連線時,核心問題是:裝置 A 真的是在和裝置 B 通訊嗎,還是有人從中攔截? 在密碼學中,這稱為 中間人攻擊 (MITM)。

Timmy 透過 橢圓曲線 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 與裝置 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

各通訊路徑說明:

備用方式:手動輸入代碼

如果無法使用藍牙(例如較舊的裝置),也可以手動輸入 4 字元代碼。 手動輸入採用 相同的 ECDH 金鑰交換與相同的 SAS 驗證 ,與自動配對一樣。 唯一差別是代碼由使用者讀取並輸入,而非透過 BLE 自動找到。

由於兩種方式的 ECDH 金鑰交換都經由 Firebase 進行,安全性 完全相同。 4 字元代碼只是一個碰面識別碼;真正的加密是以從 ECDH 衍生的 256 位元金鑰為基礎。

摘要

安全機制 可防護的風險
ECDH 金鑰交換(P-256) 竊聽金鑰交換流量
暫時性金鑰對 前向保密性 — 過去的配對依然安全
目視驗證碼(SAS) 金鑰交換期間的中間人攻擊(MITM)
以 SHA-256 雜湊作為文件金鑰 從 Firestore 擷取代碼
AES-256-GCM 加密 竊聽訊號交換資料
雙方確認 使用者不知情的單方配對
DTLS-SRTP(WebRTC) 竊聽音訊/視訊

這些防護層彼此搭配:ECDH 保護金鑰交換,驗證碼防範 MITM,AES-256-GCM 保護訊號交換,而 WebRTC 保護媒體資料。 攻擊者得在裝置與家長都沒發現的情況下,同時突破這條防護鏈上的多個環節。


更多文章