ก่อนที่ Baby Monitor Timmy จะส่งเสียงและวิดีโอ อุปกรณ์สองเครื่องต้องค้นหากันและเชื่อใจกันได้ ขั้นตอน การจับคู่ นี้เป็นช่วงเวลาที่สำคัญที่สุดของกระบวนการทั้งหมด ที่นี่ผมจะอธิบายว่า Timmy จับคู่อย่างไร ใช้การเข้ารหัสใดบ้าง และทำไมผู้โจมตีที่อยู่ใกล้ ๆ จึงไม่สามารถยึดการเชื่อมต่อโดยที่ไม่มีใครรู้ตัวได้
ปัญหา: อุปกรณ์ของฉันรู้ได้อย่างไรว่ากำลังคุยกับใคร?
เมื่ออุปกรณ์สองเครื่องเชื่อมต่อกันเป็นครั้งแรก คำถามสำคัญคือ: อุปกรณ์ A กำลังคุยกับอุปกรณ์ B จริงหรือมีใครแทรกอยู่ตรงกลาง? ในวิทยาการเข้ารหัส สิ่งนี้เรียกว่า การโจมตีแบบคนกลาง (MITM)
Timmy แก้ปัญหานี้ด้วย การแลกเปลี่ยนกุญแจ Elliptic Curve 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):
- กุญแจ ส่วนตัว — อยู่บนอุปกรณ์เท่านั้น
- กุญแจ สาธารณะ — แลกเปลี่ยนผ่าน 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 ไม่สามารถย้อนกลับได้
- โอกาสที่สุ่มแล้วตรงกันมีเพียง 1 ใน 100
ผู้ใช้จะเห็นหมายเลขต่างกันบนหน้าจอและยกเลิกการจับคู่ ณ จุดนั้น การโจมตีก็ถูกเปิดเผยแล้ว
ขั้นตอนที่ 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: หากเชื่อมต่อโดยตรงไม่ได้ (เช่น ใช้ ข้อมูลมือถือ) เซิร์ฟเวอร์ TURN ในพื้นที่หรือของ Cloudflare ที่เลือกไว้จะรีเลย์สื่อที่เข้ารหัส ข้อมูลรับรองอายุสั้น (24 ชม.) จะดึงมา ผ่าน Firebase Cloud Functions
- Bluetooth LE (เส้นประ): Nearby Connections ค้นหาอุปกรณ์ที่อยู่ใกล้ โดยอัตโนมัติ — ส่งเฉพาะรหัสนัดพบ ไม่มีข้อมูลกุญแจ
ทางเลือกสำรอง: ป้อนรหัสด้วยตนเอง
หากใช้ Bluetooth ไม่ได้ (เช่น ในอุปกรณ์รุ่นเก่า) สามารถพิมพ์รหัส 4 ตัวอักษรด้วยตนเองได้ การป้อนด้วยตนเองใช้ การแลกเปลี่ยนกุญแจ ECDH และการตรวจสอบ SAS แบบเดียวกัน กับการจับคู่อัตโนมัติ ความต่างเพียงอย่างเดียวคือผู้ใช้อ่านและพิมพ์รหัสเอง แทนที่จะค้นหาผ่าน BLE
เนื่องจากทั้งสองกรณีแลกเปลี่ยนกุญแจ ECDH ผ่าน Firebase ความปลอดภัยจึง เหมือนกัน. รหัส 4 ตัวอักษรเป็นเพียงจุดนัดพบ การเข้ารหัสจริงอาศัยกุญแจ 256 บิตที่ได้จาก ECDH
สรุป
| กลไกความปลอดภัย | ป้องกัน |
|---|---|
| การแลกเปลี่ยนกุญแจ ECDH (P-256) | การดักฟังทราฟฟิกการแลกเปลี่ยนกุญแจ |
| คู่กุญแจแบบชั่วคราว | การรักษาความลับย้อนหลัง (Forward secrecy) — การจับคู่ในอดีตยังคงปลอดภัย |
| หมายเลขตรวจสอบด้วยสายตา (SAS) | การโจมตีแบบคนกลาง (MITM) ระหว่างแลกเปลี่ยนกุญแจ |
| แฮช SHA-256 เป็นกุญแจเอกสาร | การดึงรหัสจาก Firestore |
| การเข้ารหัส AES-256-GCM | การดักฟังข้อมูลการส่งสัญญาณ |
| การยืนยันจากทั้งสองฝ่าย | การจับคู่ฝ่ายเดียวโดยผู้ใช้ไม่รู้ตัว |
| DTLS-SRTP (WebRTC) | การดักฟังเสียง/วิดีโอ |
ชั้นความปลอดภัยเหล่านี้ทำงานร่วมกัน: ECDH ปกป้องการแลกเปลี่ยนกุญแจ หมายเลขยืนยันป้องกัน MITM, AES-256-GCM ปกป้องการส่งสัญญาณ และ WebRTC ปกป้องสื่อ ผู้โจมตีจะต้องเจาะห่วงโซ่นี้หลายจุดโดยที่อุปกรณ์หรือผู้ปกครองไม่ทันสังเกต