Sebelum Baby Monitor Timmy mengirimkan audio dan video, dua perangkat perlu saling menemukan dan saling memercayai. penyandingan adalah langkah paling penting dalam seluruh proses. Di sini saya menjelaskan cara Timmy melakukan penyandingan, kriptografi di baliknya, dan alasan penyerang di sekitar tidak dapat mengambil alih koneksi tanpa diketahui.
Masalahnya: Bagaimana perangkat saya tahu dengan siapa ia berkomunikasi?
Saat dua perangkat terhubung untuk pertama kali, pertanyaan utamanya adalah: apakah Perangkat A benar-benar berbicara dengan Perangkat B, atau ada seseorang di tengah? Dalam kriptografi, ini disebut serangan man-in-the-middle (MITM).
Timmy mengatasinya dengan pertukaran kunci Elliptic Curve Diffie-Hellman (ECDH) melalui Firebase, dipadukan dengan verifikasi visual oleh pengguna.
Diagram berikut menampilkan seluruh alur penyandingan secara ringkas:
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
Urutan lengkap protokol penyandingan — sumber yang dapat diedit: docs/diagrams/pairing-sequence.mmd
Langkah 1: Setiap perangkat membuat pasangan kunci
Saat membuka layar penyandingan, setiap perangkat membuat pasangan kunci ECDH sementara pada kurva P-256 (secp256r1):
- Sebuah kunci privat — hanya tersimpan di perangkat
- Sebuah kunci publik — ditukar melalui Firebase
Kunci dibuat menggunakan generator angka acak yang aman secara kriptografis (Random.secure())
dan hanya berlaku untuk satu percobaan penyandingan ini. Kunci baru dibuat
untuk setiap percobaan baru.
Langkah 2: Menukar kunci publik melalui Firebase
Agar dua perangkat dapat saling menemukan, Timmy menggunakan kode 4 karakter sebagai titik temu. Kode ini dapat ditemukan otomatis melalui Nearby Connections (Bluetooth Low Energy) atau dimasukkan secara manual. Kode ini tidak memiliki nilai kriptografis; kode ini hanya membantu kedua perangkat menemukan dokumen Firebase Firestore yang sama.
Setelah kedua perangkat mengetahui kode tersebut, masing-masing menulis kunci publik ECDH-nya ke sebuah dokumen Firestore yang digunakan bersamabersama. Lalu setiap perangkat membaca kunci publik perangkat lain dari dokumen tersebut.
Yang penting: hanya kunci publik yang dikirim. Kunci privat tidak pernah meninggalkan perangkat. Siapa pun yang mengamati lalu lintas Firebase dapat melihat kunci publik, tetapi tidak dapat menghitung rahasia bersama darinya. Hal ini bergantung pada sulitnya Masalah Logaritma Diskret Kurva Eliptik (ECDLP).
Langkah 3: Menghitung rahasia bersama
Setelah kedua perangkat menemukan kunci publik satu sama lain, keduanya secara mandiri menghitung rahasia bersama:
sharedSecret = ECDH(myPrivateKey, remotePublicKey)
→ 32 bytes (identical on both devices)
Matematika kurva eliptik menjamin kedua perhitungan menghasilkan nilai yang sama, meskipun setiap perangkat hanya mengetahui kunci privatnya sendiri dan kunci publik perangkat lain.
Langkah 4: Angka verifikasi (SAS)
Dari rahasia bersama tersebut, diturunkan sebuah Short Authentication String (SAS) — angka dua digit yang ditampilkan di kedua perangkat:
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
Kedua perangkat menampilkan angka yang sama — misalnya, 42. Pengguna membandingkan secara visual apakah angka di kedua layar cocok, lalu mengonfirmasi pada masing-masing perangkat.
Mengapa penyerang tidak dapat memalsukannya
Penyerang man-in-the-middle harus mencegat pertukaran kunci di Firebase. Secara khusus, mereka perlu:
- Mengganti kunci publik asli yang tersimpan dalam dokumen Firestore dengan kunci mereka sendiri
- Membuat rahasia bersama terpisah dengan setiap perangkat
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)
Deteksi man-in-the-middle melalui ketidakcocokan SAS — sumber yang dapat diedit: docs/diagrams/mitm-detection.mmd
Dalam kasus ini, penyerang menghitung rahasia bersama S_A dengan Perangkat A dan
rahasia bersama yang berbeda S_B dengan Perangkat B. Karena S_A ≠ S_B,
perangkat-perangkat tersebut menghitung angka verifikasi yang berbeda.
Penyerang tidak dapat membuat angkanya cocok karena:
- Mereka tidak mengetahui kunci privat perangkat
- SHA-256 tidak dapat dibalik
- Peluang kecocokan acak hanya 1 banding 100
Pengguna melihat angka yang berbeda di kedua layar lalu membatalkan penyandingan. Pada saat itu, serangan menjadi terdeteksi.
Langkah 5: Menyelesaikan penyandingan
Hanya setelah pengguna mengonfirmasi verifikasi di kedua perangkat barulah penyandingan selesai:
- Sebuah kunci penyandingan 64 karakter (256 bit) diturunkan dari rahasia bersama:
SHA-256("pair:" + sharedSecret) → pairingKey - Kunci dokumen diturunkan sebagai
SHA-256("doc:" + pairingKey)dan berfungsi sebagai kunci dokumen Firestore - Kunci enkripsi diturunkan sebagai
SHA-256("enc:" + pairingKey)dan menyediakan kunci AES-256-GCM untuk pensinyalan terenkripsi - Kedua perangkat menyimpan kunci penyandingan yang sama dan beralih ke pemilihan mode
Mulai titik ini, semua percobaan koneksi berikutnya (pensinyalan Firestore, penyiapan WebRTC) dienkripsi dengan kunci AES-256-GCM bersama. Kunci penyandingan tidak pernah dikirim ke backend; hanya hash SHA-256-nya yang digunakan sebagai pengenal dokumen.
Arsitektur sistem
Diagram berikut menunjukkan komponen yang terlibat dalam penyandingan dan komunikasi:
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
Ringkasan arsitektur sistem — sumber yang dapat diedit: docs/diagrams/pairing-architecture.mmd
Jalur komunikasi secara rinci:
- WebRTC peer-to-peer (garis tebal): Audio, video, dan DataChannel mengalir langsung antarperangkat — dienkripsi dengan DTLS-SRTP. Tidak ada server yang melihat data ini.
- Firebase Firestore (garis penuh): Data penyandingan (kunci ECDH) dan pensinyalan (SDP/ICE) melalui Firestore — dienkripsi end-to-end dengan AES-256-GCM. Firebase tidak dapat mendekripsi data tersebut.
- Server STUN: Kedua perangkat menemukan alamat IP publiknya agar koneksi peer-to-peer langsung dapat dibuat.
- Relay TURN: Jika koneksi langsung tidak memungkinkan (misalnya, melalui data seluler), server TURN lokal atau Cloudflare yang dipilih meneruskan media terenkripsi. Kredensial berumur singkat (24 jam) diambil melalui Firebase Cloud Functions.
- Bluetooth LE (garis putus-putus): Nearby Connections menemukan perangkat di sekitar secara otomatis — hanya kode pertemuan yang dikirim, tanpa materi kunci.
Alternatif: Memasukkan kode secara manual
Jika Bluetooth tidak tersedia (misalnya, pada perangkat lama), kode 4 karakter juga dapat diketik secara manual. Entri manual menggunakan pertukaran kunci ECDH dan verifikasi SAS yang sama seperti penyandingan otomatis. Satu-satunya perbedaan adalah kode dibaca dan diketik oleh pengguna, bukan ditemukan melalui BLE.
Karena pertukaran kunci ECDH dilakukan melalui Firebase pada kedua kasus, keamanannya identik. Kode 4 karakter tersebut hanya berfungsi sebagai titik temu; enkripsi sebenarnya didasarkan pada kunci 256 bit yang diturunkan dari ECDH.
Ringkasan
| Mekanisme keamanan | Melindungi dari |
|---|---|
| Pertukaran kunci ECDH (P-256) | Penyadapan lalu lintas pertukaran kunci |
| Pasangan kunci sementara | Kerahasiaan maju (forward secrecy) — penyandingan sebelumnya tetap aman |
| Angka verifikasi visual (SAS) | Man-in-the-middle (MITM) saat pertukaran kunci |
| Hash SHA-256 sebagai kunci dokumen | Pengambilan kode dari Firestore |
| Enkripsi AES-256-GCM | Penyadapan data pensinyalan |
| Konfirmasi di kedua sisi | Penyandingan sepihak tanpa sepengetahuan pengguna |
| DTLS-SRTP (WebRTC) | Penyadapan audio/video |
Lapisan-lapisan ini saling melengkapi: ECDH melindungi pertukaran kunci, angka verifikasi melindungi dari MITM, AES-256-GCM melindungi pensinyalan, dan WebRTC melindungi media. Penyerang harus membobol rangkaian ini di beberapa titik tanpa disadari perangkat atau orang tua.