Baby Monitor Timmyが音声や映像を送信する前に、2台の端末がお互いを見つけ、信頼関係を確立する必要があります。 この ペアリング の手順は、全体の中でもっとも重要な場面です。ここでは、Timmyがどのようにペアリングするのか、その背後にある暗号技術、そして近くにいる攻撃者が気づかれずに接続を乗っ取れない理由を説明します。
問題:端末は通信相手をどう確認するのか?
2台の端末を初めて接続するとき、中心となる問いはこれです。端末Aは本当に端末Bと通信しているのか、それとも誰かが間に入っているのか? 暗号技術では、これを 中間者攻撃 (MITM)と呼びます。
Timmyでは、 楕円曲線ディフィー・ヘルマン(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())で作成され、 この1回のペアリング試行でのみ有効です。新しい試行のたびに、新しい鍵が生成されます。
ステップ2:Firebase経由で公開鍵を交換する
2台の端末がお互いを見つけられるよう、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) が導き出されます。 これは両方の端末に表示される2桁の番号です:
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
この場合、攻撃者は端末Aとの共有秘密 S_A と、端末Bとの
別の共有秘密 S_B を計算します。 S_A ≠ S_Bそのため、
端末は 異なる確認番号.
を計算します。攻撃者が番号を一致させられない理由は次のとおりです:
- 端末の秘密鍵を知らない
- SHA-256には一方向性があり、ハッシュ値から元の値を復元できない
- 偶然一致する確率はわずか 100分の1
です。ユーザーは画面上の番号が異なることに気づき、ペアリングを中止します。その時点で、攻撃は発覚します。
ステップ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リレー:直接接続ができない場合(例:モバイルデータ通信時)は、選択したローカルまたはCloudflareのTURNサーバーが暗号化されたメディアを中継します。24時間有効の短期認証情報は Firebase Cloud Functions経由で取得します。
- Bluetooth LE (点線):Nearby Connectionsが近くの端末を 自動検出します。送信されるのは待ち合わせコードだけで、鍵情報は送信されません。
代替手段:コードを手動入力
Bluetoothが使えない場合(例:古い端末)でも、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がメディアを守ります。 攻撃者は、端末や保護者に気づかれることなく、この防御の連鎖を複数箇所で破らなければなりません。