پیش از اینکه 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 (بلوتوث کممصرف) پیدا کرد یا دستی وارد کرد. این کد هیچ ارزش رمزنگاریای ندارد؛ فقط باعث میشود هر دو دستگاه همان سند 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 دریافت میشوند.
- بلوتوث کممصرف (خط نقطهچین): Nearby Connections دستگاههای نزدیک را بهطور خودکار پیدا میکند — فقط کد ملاقات منتقل میشود و هیچ اطلاعاتی مربوط به کلیدها انتقال نمییابد.
جایگزین: وارد کردن دستی کد
اگر بلوتوث در دسترس نباشد (مثلاً در دستگاههای قدیمیتر)، کد 4 کاراکتری را میتوان دستی هم وارد کرد. ورود دستی از همان تبادل کلید ECDH و همان تأیید SAS مانند جفتسازی خودکار استفاده میکند. تنها تفاوت این است که کاربر کد را میخواند و وارد میکند، نه اینکه از طریق BLE پیدا شود.
چون تبادل کلید ECDH در هر دو حالت از طریق Firebase انجام میشود، امنیت یکساناست. کد 4 کاراکتری فقط یک نقطه ملاقات است؛ رمزنگاری واقعی بر پایه کلید 256 بیتیِ مشتقشده از ECDH است.
خلاصه
| سازوکار امنیتی | محافظت در برابر |
|---|---|
| تبادل کلید ECDH (P-256) | شنود ترافیک تبادل کلید |
| جفت کلیدهای موقت | محرمانگی پیشرو — جفتسازیهای گذشته امن باقی میمانند |
| شماره تأیید دیداری (SAS) | حمله مرد میانی (MITM) هنگام تبادل کلید |
| هش SHA-256 بهعنوان کلید سند | استخراج کد از Firestore |
| رمزگذاری AES-256-GCM | شنود دادههای سیگنالدهی |
| تأیید از هر دو طرف | جفتسازی یکطرفه بدون اطلاع کاربر |
| DTLS-SRTP (WebRTC) | شنود صدا/تصویر |
این لایهها با هم کار میکنند: ECDH از تبادل کلید محافظت میکند، شماره تأیید در برابر MITM محافظت میکند، AES-256-GCM از سیگنالدهی محافظت میکند و WebRTC از رسانه. مهاجم باید این زنجیره را در چند نقطه بشکند، بدون اینکه دستگاهها یا والدین متوجه شوند.