Baby Monitor Timmy가 오디오와 영상을 전송하기 전에 두 기기는 서로를 찾고 신뢰해야 합니다. 이 페어링 단계는 전체 과정에서 가장 중요한 순간입니다. 여기에서는 Timmy가 페어링하는 방식, 그 기반이 되는 암호 기술, 그리고 근처의 공격자가 들키지 않고 연결을 가로챌 수 없는 이유를 설명합니다.
문제: 내 기기는 누구와 통신하는지 어떻게 알까요?
두 기기가 처음 연결할 때 핵심 질문은 이것입니다. 기기 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())를 사용해 생성되며 이번 한 번의 페어링 시도에만 유효합니다. 새로 시도할 때마다 새로운 키가 생성됩니다.
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에서 키 교환을 가로채야 합니다. 구체적으로는 다음이 필요합니다:
- 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
이 경우 공격자는 공유 비밀 S_A 을 기기 A와 계산하고, 이와 다른 공유 비밀 S_B 을 기기 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 P2P (굵은 선): 오디오, 영상, DataChannel은 기기 간에 직접 흐르며 DTLS-SRTP로 암호화됩니다. 어떤 서버도 이 데이터를 볼 수 없습니다.
- Firebase Firestore (실선): 페어링 데이터(ECDH 키)와 시그널링(SDP/ICE)은 Firestore를 거치며 AES-256-GCM으로 종단 간 암호화됩니다. Firebase는 데이터를 복호화할 수 없습니다.
- STUN 서버: 두 기기는 직접 P2P 연결을 설정할 수 있도록 자신의 공인 IP 주소를 찾습니다.
- TURN 릴레이: 직접 연결이 불가능한 경우(예: 모바일 데이터 사용 시), 선택한 로컬 또는 Cloudflare TURN 서버가 암호화된 미디어를 중계합니다. Firebase Cloud Functions를 통해 수명이 짧은 자격 증명(24시간)을 가져옵니다.
- 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는 미디어를 보호합니다. 공격자는 기기나 부모가 알아차리지 못하는 상태에서 이 연결 고리를 여러 곳에서 무너뜨려야 합니다.