บล็อก

การจับคู่อย่างปลอดภัยใน Baby Monitor Timmy

ECDH, หมายเลข SAS และการส่งสัญญาณที่เข้ารหัสทำงานร่วมกันอย่างไร

ก่อนที่ 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):

กุญแจถูกสร้างด้วยตัวสร้างตัวเลขสุ่มที่ปลอดภัยเชิงการเข้ารหัส (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 ตัวอักษรเป็นเพียงจุดนัดพบ การเข้ารหัสจริงอาศัยกุญแจ 256 บิตที่ได้จาก ECDH

สรุป

กลไกความปลอดภัย ป้องกัน
การแลกเปลี่ยนกุญแจ ECDH (P-256) การดักฟังทราฟฟิกการแลกเปลี่ยนกุญแจ
คู่กุญแจแบบชั่วคราว การรักษาความลับย้อนหลัง (Forward secrecy) — การจับคู่ในอดีตยังคงปลอดภัย
หมายเลขตรวจสอบด้วยสายตา (SAS) การโจมตีแบบคนกลาง (MITM) ระหว่างแลกเปลี่ยนกุญแจ
แฮช SHA-256 เป็นกุญแจเอกสาร การดึงรหัสจาก Firestore
การเข้ารหัส AES-256-GCM การดักฟังข้อมูลการส่งสัญญาณ
การยืนยันจากทั้งสองฝ่าย การจับคู่ฝ่ายเดียวโดยผู้ใช้ไม่รู้ตัว
DTLS-SRTP (WebRTC) การดักฟังเสียง/วิดีโอ

ชั้นความปลอดภัยเหล่านี้ทำงานร่วมกัน: ECDH ปกป้องการแลกเปลี่ยนกุญแจ หมายเลขยืนยันป้องกัน MITM, AES-256-GCM ปกป้องการส่งสัญญาณ และ WebRTC ปกป้องสื่อ ผู้โจมตีจะต้องเจาะห่วงโซ่นี้หลายจุดโดยที่อุปกรณ์หรือผู้ปกครองไม่ทันสังเกต


บทความเพิ่มเติม