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