Files
OwnCord/docs/architecture/voice-e2ee.md
T
Claude cf8e5aae24 docs: add architecture blueprints and 2026-07-19 audit
Add docs/architecture/ — a curated blueprint set with 10 Mermaid diagrams
covering system context, deployment topology, server package map, REST
request lifecycle, WebSocket auth/replay/dispatch, the full data model
(migrations 001-015), voice/E2EE flow, and the client module map.

Add docs/audit-2026-07-19.md — successor to audit-2026-04-07.md:
re-verifies carried-over findings, catalogues spec-vs-code drift in
api.md/protocol.md/schema.md (incl. the announcement channel-type
contradiction and the undocumented voice-E2EE protocol surface), records
server/client/CI findings with file:line evidence, and closes with a
12-item prioritized improvement backlog.

Link both from the README docs index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 12:18:20 +00:00

3.4 KiB

Voice and End-to-End Encryption

Verified against: commit ddc49f0, 2026-07-19

Voice/video runs on LiveKit. The Go server issues short-lived scoped tokens and relays E2EE key-exchange messages; media flows client↔LiveKit directly. On the client, everything funnels through src/lib/livekitSession.ts, and — because self-hosted servers commonly use self-signed certificates — the LiveKit connection is tunneled through a local Rust TLS proxy pinned to the same TOFU fingerprint as the main WebSocket.

D6 — Voice join + E2EE key exchange

sequenceDiagram
    autonumber
    participant UI as Client UI
    participant LKS as livekitSession.ts
    participant RP as Rust livekit_proxy<br/>(loopback TCP→TLS, TOFU-pinned)
    participant WS as App WebSocket (Hub)
    participant SRV as Go server
    participant LK as LiveKit server

    UI->>WS: voice_join {channel_id}
    WS->>SRV: permission check (channel-scoped)
    SRV-->>WS: voice_token (5-min JWT,<br/>CanPublishSources scoped by permission)
    WS-->>LKS: voice_token payload
    LKS->>RP: connect ws://127.0.0.1:{port}
    RP->>LK: TLS (fingerprint-pinned)
    LKS->>LK: LiveKit signaling + media (via tunnel)

    rect rgba(120,160,220,0.15)
        Note over LKS,WS: E2EE key exchange (relayed via app WS)
        LKS->>WS: voice_e2ee_announce {ECDH pubkey}
        WS-->>LKS: voice_e2ee_announce broadcast to channel
        Note over SRV: Hub tracks per-channel key holder<br/>(lowest user ID)
        LKS->>WS: voice_e2ee_offer {wrapped room key, target user}
        WS-->>LKS: voice_e2ee_offer relayed to target
        Note over LKS: unwrap room key → LiveKit<br/>ExternalE2EEKeyProvider
        Note over LKS: on participant leave,<br/>key holder rotates room key
    end

What this shows. The server never holds the room key — it only relays announce/offer messages and tracks who the key holder is (deterministically the lowest user ID in the channel). Keys are wrapped per-recipient via ECDH, and the key holder rotates the room key when a participant leaves so departed members cannot decrypt future media. Voice permission enforcement happens twice: at voice_join (channel permission) and inside the LiveKit JWT itself (CanPublishSources restricts camera/screenshare per role permission).

Supporting pieces:

  • Server: Server/ws/voice_e2ee.go (relay + key-holder map), Server/ws/livekit.go (token minting), Server/ws/livekit_process.go (optional managed livekit-server subprocess), Server/ws/livekit_webhook.go (webhook validated by LiveKit JWT and admin-IP-restricted), Server/api/livekit_proxy.go (HTTP reverse proxy).
  • Client: src/lib/livekitSession.ts (state machine: idle/connecting/ connected/reconnecting with a monotonic joinGeneration to discard superseded joins), src/lib/e2eeCrypto.ts (ECDH, key wrap/unwrap), src/lib/audioPipeline.ts + src/lib/noise-suppression.ts (RNNoise WASM), src/lib/screenShare.ts, src-tauri/src/livekit_proxy.rs (tunnel), src-tauri/src/ptt.rs (push-to-talk key polling).

This message flow (voice_e2ee_announce / voice_e2ee_offer / voice_speakers) is currently absent from docs/protocol.md — recorded as spec drift in audit-2026-07-19.md §2.

Source of truth: Server/ws/voice_e2ee.go, Server/ws/livekit.go, Client/tauri-client/src/lib/livekitSession.ts, Client/tauri-client/src/lib/e2eeCrypto.ts, Client/tauri-client/src-tauri/src/livekit_proxy.rs.