feat: Phase 3 — E2EE calls, Jitsi conferencing, help tab, web security model
- LiveKit call E2EE: CallE2EEManager exchanges encryption keys via Matrix to-device events (m.rtc.encryption_keys) for interop with Element X - Olm bootstrapped in index.html before Flutter init; main.dart logs result - Encrypted messages shown with lock icon and informative fallback text - Profile screen: key restore dialog + security setup (cross-signing/backup) - Jitsi feature: welcome screen (public, no login), conference tab, full-screen embed via JitsiMeetExternalAPI, JitsiLink parser for all common link formats - Help tab: expandable cards for encryption, video calls, account management - Web security model: no session persistence — device ID only across visits - Media auth: MSC3916 authenticated endpoint for avatars (Synapse 1.120+) - Router: welcome route as public landing page; jitsi route as public - Manifest/index.html: M8Chat branding, dark theme colours Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
// Version: 1.3.0 | Created: 2026-04-01 | Updated: 2026-04-03
|
||||
// Version: 1.4.0 | Created: 2026-04-01 | Updated: 2026-04-10
|
||||
// LiveKitService — fetches a JWT from the Matrix server's /_matrix/livekit/jwt
|
||||
// endpoint, then connects a LiveKit Room using that token.
|
||||
//
|
||||
// The JWT endpoint is defined in AppConfig.livekitJwtUrl and uses the Matrix
|
||||
// access token as Bearer auth. The LiveKit server URL is the same host as the
|
||||
// Matrix server (as configured on chat.m8chat.au).
|
||||
// E2EE: Frame encryption is enabled via livekit_client's E2EEOptions.
|
||||
// Encryption keys are exchanged with Element X via m.rtc.encryption_keys
|
||||
// to-device events (see call_e2ee.dart).
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
@@ -19,6 +19,7 @@ import '../../../core/auth/auth_notifier.dart';
|
||||
import '../../../core/auth/auth_state.dart';
|
||||
import '../../../core/config/app_config.dart';
|
||||
import '../../../core/network/matrix_client.dart';
|
||||
import 'call_e2ee.dart';
|
||||
|
||||
part 'livekit_service.g.dart';
|
||||
|
||||
@@ -67,6 +68,7 @@ class LiveKitService {
|
||||
Room? _activeRoom;
|
||||
String? _activeMatrixRoomId;
|
||||
String? _membershipId;
|
||||
CallE2EEManager? _e2eeManager;
|
||||
|
||||
Room? get activeRoom => _activeRoom;
|
||||
|
||||
@@ -111,14 +113,37 @@ class LiveKitService {
|
||||
_activeMatrixRoomId = matrixRoomId;
|
||||
debugPrint('[LiveKit] Call member state event sent');
|
||||
|
||||
// Step 3 — connect to LiveKit
|
||||
final room = Room();
|
||||
// Step 3 — initialise E2EE key exchange
|
||||
_e2eeManager = CallE2EEManager(
|
||||
client: client,
|
||||
matrixRoomId: matrixRoomId,
|
||||
);
|
||||
final keyProvider = await _e2eeManager!.init();
|
||||
|
||||
// Step 4 — connect to LiveKit with frame encryption
|
||||
// COOP/COEP headers on the server enable SharedArrayBuffer for the
|
||||
// E2EE web worker. Without them the worker crashes.
|
||||
final room = Room(
|
||||
roomOptions: RoomOptions(
|
||||
e2eeOptions: E2EEOptions(keyProvider: keyProvider),
|
||||
),
|
||||
);
|
||||
try {
|
||||
await room.connect(livekitUrl, jwt);
|
||||
_activeRoom = room;
|
||||
|
||||
// Step 5 — set our local key and share with other participants
|
||||
final localIdentity = room.localParticipant?.identity;
|
||||
debugPrint('[LiveKit] Connected, local identity: $localIdentity');
|
||||
if (localIdentity != null) {
|
||||
await _e2eeManager!.setLocalKey(localIdentity);
|
||||
}
|
||||
await _e2eeManager!.sendKeyToParticipants();
|
||||
|
||||
return LiveKitConnected(room: room);
|
||||
} on Exception catch (e) {
|
||||
// Clean up the state event on failure
|
||||
await _e2eeManager?.dispose();
|
||||
_e2eeManager = null;
|
||||
await _clearCallMemberEvent(client, matrixRoomId, userId);
|
||||
await room.disconnect();
|
||||
await room.dispose();
|
||||
@@ -136,6 +161,10 @@ class LiveKitService {
|
||||
_activeMatrixRoomId = null;
|
||||
_membershipId = null;
|
||||
|
||||
// Clean up E2EE key exchange
|
||||
await _e2eeManager?.dispose();
|
||||
_e2eeManager = null;
|
||||
|
||||
// Step 1: Leave LiveKit — Element X will see us disappear from the SFU
|
||||
if (room != null) {
|
||||
await room.disconnect();
|
||||
@@ -255,6 +284,7 @@ class LiveKitService {
|
||||
}
|
||||
|
||||
/// Clear the call.member state event (remove our membership).
|
||||
/// Element X expects empty content `{}` — not `{'memberships': []}`.
|
||||
Future<void> _clearCallMemberEvent(
|
||||
Client client,
|
||||
String matrixRoomId,
|
||||
@@ -265,8 +295,9 @@ class LiveKitService {
|
||||
matrixRoomId,
|
||||
_kCallMemberEventType,
|
||||
userId,
|
||||
{'memberships': []},
|
||||
{},
|
||||
);
|
||||
debugPrint('[LiveKit] Call member event cleared for $userId');
|
||||
} catch (e) {
|
||||
debugPrint('[LiveKit] Failed to clear call member event: $e');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user