وبلاگ

جفت‌سازی امن در 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 (بلوتوث کم‌مصرف) پیدا کرد یا دستی وارد کرد. این کد هیچ ارزش رمزنگاری‌ای ندارد؛ فقط باعث می‌شود هر دو دستگاه همان سند 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

مسیرهای ارتباطی به‌طور دقیق:

جایگزین: وارد کردن دستی کد

اگر بلوتوث در دسترس نباشد (مثلاً در دستگاه‌های قدیمی‌تر)، کد 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 از رسانه. مهاجم باید این زنجیره را در چند نقطه بشکند، بدون اینکه دستگاه‌ها یا والدین متوجه شوند.


مقاله‌های بیشتر