Skip to content

Group Call Flow

sequenceDiagram
    participant UserA as User A (Initiator)<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<br/>iOS Device
    participant KeychainC as iOS Keychain C
    participant UserC as User C<br/>iOS Device

    Note over UserA,UserC: PRE-REQUISITE: All users authenticated with RSA keys registered

    Note over UserA,UserC: PHASE 1: GROUP CALL INITIATION (3 PARTICIPANTS)

    UserA->>UserA: Click "Start Group Call"<br/>Select User B and User C

    UserA->>Server: GET User B's Public Key
    Server-->>UserA: User B's RSA Public Key
    UserA->>Server: GET User C's Public Key
    Server-->>UserA: User C's RSA Public Key

    UserA->>UserA: Generate AES-256 Session Key<br/>(32 random bytes)
    Note right of UserA: Single shared key<br/>for entire group call

    UserA->>UserA: Encrypt AES Key with<br/>User B's RSA Public Key<br/>(OAEP-SHA256)
    UserA->>UserA: Encrypt AES Key with<br/>User C's RSA Public Key<br/>(OAEP-SHA256)
    Note right of UserA: Each participant gets<br/>their own encrypted copy

    UserA->>UserA: Setup Local Media Encryption<br/>(CustomAudioCrypto + Video)

    UserA->>Server: POST /group-calls/start<br/>{roomId, calleeIds: [UserB_ID, UserC_ID],<br/>encryptedAESKeys: {<br/>"UserB_ID_DeviceB_ID": encryptedKeyB,<br/>"UserC_ID_DeviceC_ID": encryptedKeyC},<br/>isVideo: true}
    Note over Server: Server stores call record<br/>Cannot decrypt AES keys

    Server->>UserB: VoIP Push Notification<br/>{callerId, callerName, callId,<br/>encryptedAESKey, roomId,<br/>isVideo: true}
    Server->>UserC: VoIP Push Notification<br/>{callerId, callerName, callId,<br/>encryptedAESKey, roomId,<br/>isVideo: true}

    Note over UserA,UserC: PHASE 2: KEY DECRYPTION & SETUP

    UserB->>KeychainB: Load RSA Private Key
    KeychainB-->>UserB: Private Key
    UserB->>UserB: Decrypt AES Key using<br/>RSA Private Key<br/>(OAEP-SHA256)
    UserB->>UserB: Setup Media Encryption<br/>with decrypted AES Key

    UserC->>KeychainC: Load RSA Private Key
    KeychainC-->>UserC: Private Key
    UserC->>UserC: Decrypt AES Key using<br/>RSA Private Key<br/>(OAEP-SHA256)
    UserC->>UserC: Setup Media Encryption<br/>with decrypted AES Key

    Note over UserA,UserC: ✅ All participants have shared AES-256 key

    Note over UserA,UserC: PHASE 3: JANUS SFU CONNECTION

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

    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 keepalive<br/>(every 30s)

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

    UserA->>Janus: Join Room<br/>{roomId, display: "UserA_ID:DisplayName",<br/>ptype: "publisher"}
    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)<br/>{audio: true, video: true}
    Janus-->>UserA: SDP Answer
    Note over UserA,Janus: Publisher connection established<br/>Trickle ICE candidates

    UserA->>Janus: Start publishing encrypted media

    Note over UserB: User B joins the room

    UserB->>Server: GET /credentials
    Server-->>UserB: TURN credentials

    UserB->>Janus: Connect & Create Session
    UserB->>Janus: Attach Publisher Handle
    UserB->>Janus: Join Room<br/>{roomId, display: "UserB_ID:DisplayName"}
    Janus-->>UserB: Publishers List: [UserA]
    Note right of UserB: Sees UserA already in room

    UserB->>UserB: Setup WebRTC with COTURN
    UserB->>COTURN: STUN/TURN requests
    UserB->>Janus: Send SDP Offer (Publisher)
    Janus-->>UserB: SDP Answer

    Janus-->>UserA: Event: New Publisher (UserB)
    Note right of UserA: Notified of new participant

    Note over UserC: User C joins the room

    UserC->>Server: GET /credentials
    Server-->>UserC: TURN credentials

    UserC->>Janus: Connect & Create Session
    UserC->>Janus: Attach Publisher Handle
    UserC->>Janus: Join Room<br/>{roomId, display: "UserC_ID:DisplayName"}
    Janus-->>UserC: Publishers List: [UserA, UserB]
    Note right of UserC: Sees UserA and UserB

    UserC->>UserC: Setup WebRTC with COTURN
    UserC->>COTURN: STUN/TURN requests
    UserC->>Janus: Send SDP Offer (Publisher)
    Janus-->>UserC: SDP Answer

    Janus-->>UserA: Event: New Publisher (UserC)
    Janus-->>UserB: Event: New Publisher (UserC)
    Note over UserA,UserB: All participants notified

    Note over UserA,UserC: PHASE 4: SUBSCRIPTION SETUP

    Note over UserA,UserC: Each user subscribes to all other participants

    UserA->>Janus: Attach Subscriber Handle
    Janus-->>UserA: {subscriber_handle_id}
    UserA->>Janus: Subscribe to feeds<br/>{streams: [UserB_feed, UserC_feed],<br/>ptype: "subscriber"}
    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: [UserA_feed, UserC_feed],<br/>ptype: "subscriber"}
    Janus-->>UserB: SDP Offer (Subscriber)
    UserB->>Janus: SDP Answer (Subscriber)

    UserC->>Janus: Attach Subscriber Handle
    Janus-->>UserC: {subscriber_handle_id}
    UserC->>Janus: Subscribe to feeds<br/>{streams: [UserA_feed, UserB_feed],<br/>ptype: "subscriber"}
    Janus-->>UserC: SDP Offer (Subscriber)
    UserC->>Janus: SDP Answer (Subscriber)

    Note over UserA,UserC: ✅ Group call fully established - 3 participants

    Note over UserA,UserC: PHASE 5: ENCRYPTED MEDIA FLOW (SFU)

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

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

    UserB->>UserB: Encrypt outgoing media:<br/>Audio/Video → AES-GCM
    UserB->>Janus: Encrypted RTP packets
    Janus->>UserA: Forward encrypted packets
    Janus->>UserC: Forward encrypted packets

    UserC->>UserC: Decrypt incoming media:<br/>AES-GCM → Opus/H.264

    UserC->>UserC: Encrypt outgoing media:<br/>Audio/Video → AES-GCM
    UserC->>Janus: Encrypted RTP packets
    Janus->>UserA: Forward encrypted packets
    Janus->>UserB: Forward encrypted packets

    Note over UserA,UserC: 🔒 All streams encrypted with shared AES-256 key<br/>Janus forwards encrypted media to all subscribers

    Note over UserA,UserC: PHASE 6: DYNAMIC PARTICIPANT MANAGEMENT

    participant UserD as User D<br/>iOS Device

    Note over UserA,UserD: User A invites User D mid-call

    UserA->>Server: GET User D's Public Key
    Server-->>UserA: User D's RSA Public Key

    UserA->>UserA: Encrypt EXISTING AES Key<br/>with User D's RSA Public Key
    Note right of UserA: Reuse same session key<br/>for consistency

    UserA->>Server: POST /group-calls/{callId}/invite<br/>{calleeIds: [UserD_ID],<br/>encryptedAESKeys: {<br/>"UserD_ID_DeviceD_ID": encryptedKeyD},<br/>isVideo: true, roomId}

    Server->>UserD: VoIP Push Notification<br/>{callerId, callerName, callId,<br/>encryptedAESKey, roomId,<br/>isOnGoing: true}
    Note right of UserD: isOnGoing=true indicates<br/>joining active call

    UserD->>UserD: Decrypt AES Key using<br/>RSA Private Key
    UserD->>UserD: Setup Media Encryption

    UserD->>Janus: Connect & Create Session
    UserD->>Janus: Join Room {roomId}
    Janus-->>UserD: Publishers List: [UserA, UserB, UserC]
    Note right of UserD: Sees all 3 existing participants

    UserD->>Janus: Attach Publisher Handle
    UserD->>Janus: Publish Stream
    Janus-->>UserA: Event: New Publisher (UserD)
    Janus-->>UserB: Event: New Publisher (UserD)
    Janus-->>UserC: Event: New Publisher (UserD)

    UserD->>Janus: Attach Subscriber Handle
    UserD->>Janus: Subscribe to all existing feeds<br/>{streams: [UserA_feed, UserB_feed, UserC_feed]}

    UserA->>Janus: Update subscription: add UserD_feed
    UserB->>Janus: Update subscription: add UserD_feed
    UserC->>Janus: Update subscription: add UserD_feed

    Note over UserA,UserD: ✅ User D joined - Now 4 participants in call

    Note over UserA,UserD: Media flows to/from all 4 participants

    UserD->>UserD: Encrypt media with AES-256
    UserD->>Janus: Encrypted RTP packets
    Janus->>UserA: Forward to UserA
    Janus->>UserB: Forward to UserB
    Janus->>UserC: Forward to UserC

    Note over UserA,UserD: PHASE 7: PARTICIPANT LEAVING

    UserC->>UserC: Leave Call / Hang Up
    UserC->>Janus: Unpublish (Publisher Handle)
    UserC->>Janus: Leave (Subscriber Handle)
    UserC->>Janus: Detach Handles
    UserC->>Janus: Destroy Session
    UserC->>Janus: Close WebSocket
    UserC->>UserC: CallEncryptionManager cleanup<br/>(sessionAESKey = nil)

    Janus-->>UserA: Event: Participant Left (UserC)
    Janus-->>UserB: Event: Participant Left (UserC)
    Janus-->>UserD: Event: Participant Left (UserC)

    UserA->>UserA: Remove UserC from UI<br/>Update participant list
    UserB->>UserB: Remove UserC from UI<br/>Update participant list
    UserD->>UserD: Remove UserC from UI<br/>Update participant list

    Note over UserA,UserD: ✅ Call continues with 3 participants<br/>(UserA, UserB, UserD)

    Note over UserA,UserD: PHASE 8: CALL TERMINATION

    Note over UserA,UserD: Last participant or initiator ends call

    UserA->>UserA: End Group Call
    UserA->>Janus: Unpublish & Leave
    UserA->>Janus: Destroy Session
    UserA->>Server: End Call Signal
    Server->>UserB: Call Ended Notification
    Server->>UserD: Call Ended Notification

    UserB->>Janus: Cleanup Janus session
    UserD->>Janus: Cleanup Janus session

    UserA->>UserA: CallEncryptionManager cleanup
    UserB->>UserB: CallEncryptionManager cleanup
    UserD->>UserD: CallEncryptionManager cleanup

    Note over UserA,UserD: ✅ Group call terminated<br/>All encryption keys destroyed<br/>Forward secrecy maintained

    Note over UserA,UserD: KEY FEATURES SUMMARY

    Note over UserA,UserD: 🔐 END-TO-END ENCRYPTION<br/>• Single AES-256 key shared by all participants<br/>• Each participant receives encrypted copy via RSA-2048<br/>• RSA key exchange with OAEP-SHA256 padding<br/>• AES-GCM for audio and video encryption<br/>• Private keys never leave device (iOS Keychain)

    Note over UserA,UserD: 🏗️ SFU ARCHITECTURE<br/>• Janus Gateway: Selective Forwarding Unit<br/>• Server/Janus cannot decrypt media<br/>• COTURN for NAT traversal (STUN/TURN)<br/>• WebSocket signaling (janus-protocol)<br/>• Scales to 200+ participants

    Note over UserA,UserD: 🔄 DYNAMIC FEATURES<br/>• Add participants during active call<br/>• Participants can join/leave anytime<br/>• Automatic subscription updates<br/>• Real-time publisher/subscriber events<br/>• Room-based architecture

    Note over UserA,UserD: ✅ SECURITY GUARANTEES<br/>• Forward secrecy (keys destroyed on termination)<br/>• No persistent encryption keys<br/>• Server cannot access plaintext media<br/>• Each device gets unique encrypted key copy<br/>• Multi-device support per user

    Note over UserA,UserD: RELATED DIAGRAMS

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

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

    Note over UserA,UserD: 📝 1-to-1 Calls<br/>→ e2ee_03_1to1_call_flow.mermaid