블로그

Baby Monitor Timmy의 안전한 페어링

ECDH, SAS 번호, 암호화된 시그널링이 함께 작동하는 방식.

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)에서 생성합니다:

키는 암호학적으로 안전한 난수 생성기(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

통신 경로 상세:

대안: 코드 직접 입력

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는 미디어를 보호합니다. 공격자는 기기나 부모가 알아차리지 못하는 상태에서 이 연결 고리를 여러 곳에서 무너뜨려야 합니다.


더 많은 글