博客

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 (低功耗蓝牙)自动发现,也可手动输入。 它 不具备任何密码学价值;它只用于让两台设备定位到同一份 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。用户目视比对两块屏幕上的数字是否一致,然后在 每一台 设备上分别确认。

SAS 如何揭示中间人攻击

中间人攻击者需要拦截 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 保护媒体数据。 在设备本身未失陷且用户认真核对验证数字的前提下,攻击者很难在不被设备或家长察觉的情况下绕过整套保护。


更多文章