Skip to content

1-to-1 Call Flow

sequenceDiagram
    participant UserA as User A (Caller)<br/>iOS Device
    participant KeychainA as iOS Keychain A
    participant Server as Signaling Server
    participant Janus as Janus Gateway<br/>(SFU/Videoroom)
    participant COTURN as COTURN Server<br/>(TURN/STUN)
    participant KeychainB as iOS Keychain B
    participant UserB as User B (Callee)<br/>iOS Device

    Note over UserA,UserB: PRE-REQUISITE: AUTHENTICATION & KEY REGISTRATION

    Note over UserA,UserB: 📝 Both users must be authenticated first<br/>See e2ee_01_authentication_flow.mermaid

    Note over UserA: ✅ User A authenticated<br/>RSA keys registered
    Note over UserB: ✅ User B authenticated<br/>RSA keys registered

    Note over UserA,UserB: PHASE 1: CALL INITIATION & KEY EXCHANGE

    UserA->>UserA: Click "Call User B"
    UserA->>Server: Request User B's Public Key
    Server-->>UserA: Return User B's Public Key

    UserA->>UserA: Generate Random<br/>AES-256 Session Key<br/>(32 random bytes)

    UserA->>UserA: Encrypt AES Key with<br/>User B's RSA Public Key<br/>(OAEP-SHA256)
    Note right of UserA: CallEncryptionManager

    UserA->>UserA: Setup Local Media Encryption<br/>(CustomAudioCrypto,<br/>CRTEncryptionManager)

    UserA->>Server: Send Call Invitation +<br/>Encrypted AES Key
    Note over Server: Server cannot decrypt<br/>the AES key (no private key)

    Server->>UserB: Forward Call Invitation +<br/>Encrypted AES Key

    UserB->>UserB: Receive Call Invitation
    UserB->>KeychainB: Load RSA Private Key
    KeychainB-->>UserB: Return Private Key

    UserB->>UserB: Decrypt AES Key using<br/>RSA Private Key<br/>(OAEP-SHA256)
    Note right of UserB: CallEncryptionManager

    UserB->>UserB: Setup Media Encryption<br/>with decrypted AES Key

    UserB->>Server: Accept Call (WebRTC Signaling)
    Server->>UserA: Call Accepted

    Note over UserA,UserB: PHASE 2: JANUS SFU CONNECTION & SECURE COMMUNICATION

    UserA->>Server: GET /credentials
    Server-->>UserA: {turnUsername, turnPassword,<br/>tlsUrl, noneTlsUrl}
    Note right of UserA: COTURN credentials<br/>for NAT traversal

    UserA->>Janus: Connect via WebSocket<br/>(janus-protocol)
    Janus-->>UserA: WebSocket Connected

    UserA->>Janus: Create Session
    Janus-->>UserA: {session_id}
    Note right of UserA: Start session keepalive<br/>(every 30 seconds)

    UserA->>Janus: Attach Publisher Handle<br/>(janus.plugin.videoroom)
    Janus-->>UserA: {publisher_handle_id}

    UserA->>Janus: Create/Join Room<br/>{roomId, display: "userId:displayName"}
    Janus-->>UserA: Room Joined + Publishers List

    UserA->>UserA: Setup WebRTC Publisher<br/>with COTURN ICE servers
    UserA->>COTURN: STUN/TURN requests<br/>(NAT traversal)
    COTURN-->>UserA: ICE candidates

    UserA->>Janus: Send SDP Offer (Publisher)
    Janus-->>UserA: SDP Answer
    Note over UserA,Janus: Publisher connection established<br/>Trickle ICE candidates

    UserB->>Server: GET /credentials
    Server-->>UserB: {turnUsername, turnPassword,<br/>tlsUrl, noneTlsUrl}

    UserB->>Janus: Connect via WebSocket<br/>(janus-protocol)
    Janus-->>UserB: WebSocket Connected

    UserB->>Janus: Create Session
    Janus-->>UserB: {session_id}
    Note right of UserB: Start session keepalive<br/>(every 30 seconds)

    UserB->>Janus: Attach Publisher Handle<br/>(janus.plugin.videoroom)
    Janus-->>UserB: {publisher_handle_id}

    UserB->>Janus: Join Room<br/>{roomId, display: "userId:displayName"}
    Janus-->>UserB: Room Joined + Publishers List

    UserB->>UserB: Setup WebRTC Publisher<br/>with COTURN ICE servers
    UserB->>COTURN: STUN/TURN requests<br/>(NAT traversal)
    COTURN-->>UserB: ICE candidates

    UserB->>Janus: Send SDP Offer (Publisher)
    Janus-->>UserB: SDP Answer

    Note over UserA,UserB: Both users now publishing to Janus

    UserA->>Janus: Attach Subscriber Handle
    Janus-->>UserA: {subscriber_handle_id}

    UserA->>Janus: Subscribe to feeds<br/>{streams: [User B's feed]}
    Janus-->>UserA: SDP Offer (Subscriber)
    UserA->>Janus: SDP Answer (Subscriber)

    UserB->>Janus: Attach Subscriber Handle
    Janus-->>UserB: {subscriber_handle_id}

    UserB->>Janus: Subscribe to feeds<br/>{streams: [User A's feed]}
    Janus-->>UserB: SDP Offer (Subscriber)
    UserB->>Janus: SDP Answer (Subscriber)

    Note over UserA,UserB: ✅ SFU connections established<br/>All media encrypted with shared AES-256 key

    UserA->>UserA: Encrypt outgoing media:<br/>Audio: PCM → Opus → AES-GCM<br/>Video: H.264 slices → AES-GCM
    UserA->>Janus: Encrypted Audio/Video RTP packets
    Note over Janus: Janus CANNOT decrypt<br/>Forwards encrypted packets as-is
    Janus->>UserB: Forward encrypted packets

    UserB->>UserB: Decrypt incoming media:<br/>Audio: AES-GCM → Opus → PCM<br/>Video: AES-GCM → H.264

    UserB->>UserB: Encrypt outgoing media:<br/>Audio: PCM → Opus → AES-GCM<br/>Video: H.264 slices → AES-GCM
    UserB->>Janus: Encrypted Audio/Video RTP packets
    Janus->>UserA: Forward encrypted packets

    UserA->>UserA: Decrypt incoming media:<br/>Audio: AES-GCM → Opus → PCM<br/>Video: AES-GCM → H.264

    Note over UserA,UserB: 🔒 Real-time E2EE Communication via SFU<br/>Janus forwards encrypted media (cannot decrypt)

    Note over UserA,UserB: PHASE 3: CALL TERMINATION

    UserA->>UserA: Hang Up Call
    UserA->>Janus: Unpublish (Publisher Handle)
    UserA->>Janus: Leave (Subscriber Handle)
    UserA->>Janus: Detach Handles
    UserA->>Janus: Destroy Session
    UserA->>Janus: Close WebSocket
    UserA->>UserA: CallEncryptionManager cleanup<br/>(sessionAESKey = nil)
    Note right of UserA: All encryption keys destroyed

    UserA->>Server: End Call Signal
    Server->>UserB: Call Ended Notification

    UserB->>Janus: Unpublish (Publisher Handle)
    UserB->>Janus: Leave (Subscriber Handle)
    UserB->>Janus: Detach Handles
    UserB->>Janus: Destroy Session
    UserB->>Janus: Close WebSocket
    UserB->>UserB: CallEncryptionManager cleanup<br/>(sessionAESKey = nil)
    UserB->>UserB: Destroy Key Material<br/>Reset Encryption State
    Note right of UserB: All encryption keys destroyed

    Note over UserA,UserB: ✅ Forward Secrecy Achieved<br/>No persistent session keys<br/>Janus session cleaned up

    Note over UserA,UserB: RELATED DIAGRAMS

    Note over UserA,UserB: 📝 Authentication & Registration<br/>→ e2ee_01_authentication_flow.mermaid

    Note over UserA,UserB: 📝 Token Refresh & Management<br/>→ e2ee_02_token_refresh_flow.mermaid

    Note over UserA,UserB: 📝 Group Calls (3+ participants)<br/>→ e2ee_04_group_call_flow.mermaid

    Note over UserA,UserB: KEY SECURITY FEATURES

    Note over UserA,UserB: 🔐 END-TO-END ENCRYPTION<br/>• RSA-2048 for key exchange (OAEP-SHA256)<br/>• AES-256-GCM for media encryption<br/>• Private keys stored in iOS Keychain (never leave device)<br/>• Server/Janus cannot decrypt media or session keys

    Note over UserA,UserB: 🏗️ ARCHITECTURE<br/>• Janus Gateway: SFU (Selective Forwarding Unit)<br/>• COTURN: STUN/TURN server for NAT traversal<br/>• WebRTC: Peer connections via Janus<br/>• WebSocket: Signaling with Janus (janus-protocol)

    Note over UserA,UserB: ✅ FORWARD SECRECY<br/>• Session keys generated per call<br/>• Keys destroyed on call termination<br/>• No persistent encryption keys<br/>• Past communications cannot be decrypted