Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/simplify-session-e2ee
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
minor type="added" "Simplify enabling E2EE with Session API"
43 changes: 42 additions & 1 deletion lib/src/agent/session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import 'package:uuid/uuid.dart';

import '../core/room.dart';
import '../core/room_preconnect.dart';
import '../e2ee/key_provider.dart';
import '../e2ee/options.dart';
import '../events.dart';
import '../logger.dart';
import '../managers/event.dart';
Expand Down Expand Up @@ -150,6 +152,7 @@ class Session extends DisposableChangeNotifier {
final _TokenSourceConfiguration _tokenSourceConfiguration;

final Agent _agent = Agent();
bool _isStarting = false;
Agent get agent => _agent;

SessionError? get error => _error;
Expand All @@ -175,12 +178,19 @@ class Session extends DisposableChangeNotifier {
EventsListener<RoomEvent>? _roomListener;
Timer? _agentTimeoutTimer;

/// Enables or disables end-to-end encryption for the session.
///
/// Requires that encryption was configured via [SessionOptions.encryption]
/// or that the [Room] was created with [E2EEOptions].
Future<void> setEncryptionEnabled(bool enabled) => room.setE2EEEnabled(enabled);

/// Starts the session by fetching credentials and connecting to the room.
Future<void> start() async {
if (room.connectionState != ConnectionState.disconnected) {
if (_isStarting || room.connectionState != ConnectionState.disconnected) {
logger.info('Session.start() ignored: room already connecting or connected.');
return;
}
_isStarting = true;

_setError(null);
_agentTimeoutTimer?.cancel();
Expand All @@ -197,6 +207,29 @@ class Session extends DisposableChangeNotifier {
}

try {
// Configure E2EE on the room before connecting if encryption options
// are set. Skipped when a custom room was provided, since the user is
// responsible for configuring E2EE on the room directly.
final encryption = _options.encryption;
if (encryption != null && !_options.isRoomProvided) {
if (room.e2eeManager != null) {
// Restart: the E2EE manager already exists from a previous session.
// Re-enable in case it was toggled off, before connect publishes
// any tracks.
await room.setE2EEEnabled(true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curiously, do we need to pass the encryption over to the room in this case ?

} else {
// First start: create the key provider and configure room options.
// The E2EE manager will be created during connect() with encryption
// enabled by default.
final BaseKeyProvider keyProvider = switch (encryption.key) {
SharedKeyEncryption(:final sharedKey) => await _createSharedKeyProvider(sharedKey),
KeyProviderEncryption(:final keyProvider) => keyProvider,
};
room.engine.roomOptions = room.engine.roomOptions.copyWith(
encryption: E2EEOptions(keyProvider: keyProvider),
);
}
}
final bool dispatchesAgent;
if (_options.preConnectAudio) {
dispatchesAgent = await room.withPreConnectAudio(
Expand Down Expand Up @@ -229,6 +262,8 @@ class Session extends DisposableChangeNotifier {
_setError(SessionError.connection(error));
_setConnectionState(ConnectionState.disconnected);
_agent.disconnected();
} finally {
_isStarting = false;
}
}

Expand Down Expand Up @@ -380,6 +415,12 @@ class Session extends DisposableChangeNotifier {
_error = newError;
notifyListeners();
}

static Future<BaseKeyProvider> _createSharedKeyProvider(String sharedKey) async {
final keyProvider = await BaseKeyProvider.create();
await keyProvider.setSharedKey(sharedKey);
return keyProvider;
}
}

enum SessionErrorKind {
Expand Down
85 changes: 83 additions & 2 deletions lib/src/agent/session_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,59 @@
// limitations under the License.

import '../core/room.dart';
import '../e2ee/key_provider.dart';

/// Encryption key configuration for a [Session].
///
/// Use one of the named constructors to specify either a shared passphrase
/// or a pre-configured [BaseKeyProvider].
sealed class SessionEncryptionKey {
const SessionEncryptionKey();

/// Use a shared passphrase string.
///
/// A [BaseKeyProvider] is created internally using the string as a shared
/// key (recommended for maximum compatibility across SDKs).
const factory SessionEncryptionKey.sharedKey(String key) = SharedKeyEncryption;

/// Use a pre-configured [BaseKeyProvider] for custom key management.
const factory SessionEncryptionKey.keyProvider(BaseKeyProvider provider) = KeyProviderEncryption;
}

/// A shared passphrase used to derive encryption keys.
class SharedKeyEncryption extends SessionEncryptionKey {
final String sharedKey;
const SharedKeyEncryption(this.sharedKey);
}

/// A pre-configured [BaseKeyProvider] instance.
class KeyProviderEncryption extends SessionEncryptionKey {
final BaseKeyProvider keyProvider;
const KeyProviderEncryption(this.keyProvider);
}

/// Encryption configuration for a [Session].
class SessionEncryptionOptions {
/// The encryption key, either a shared passphrase or a custom key provider.
final SessionEncryptionKey key;

const SessionEncryptionOptions({required this.key});

/// Creates encryption options with a shared passphrase string.
SessionEncryptionOptions.sharedKey(String key) : key = SharedKeyEncryption(key);

/// Creates encryption options with a pre-configured [BaseKeyProvider].
SessionEncryptionOptions.keyProvider(BaseKeyProvider provider) : key = KeyProviderEncryption(provider);
}

/// Options for creating a [Session].
class SessionOptions {
/// The underlying [Room] used by the session.
final Room room;

/// Whether a custom [Room] was explicitly provided.
final bool isRoomProvided;

/// Whether to enable audio pre-connect with [PreConnectAudioBuffer].
///
/// If enabled, the microphone is activated before connecting to the room.
Expand All @@ -30,21 +77,55 @@ class SessionOptions {
/// to a failed state.
final Duration agentConnectTimeout;

/// Optional encryption configuration for end-to-end encryption.
///
/// When provided, the session will configure E2EE on the room before
/// connecting. Use [Session.setEncryptionEnabled] to toggle encryption
/// after the session has started.
final SessionEncryptionOptions? encryption;

SessionOptions({
Room? room,
this.preConnectAudio = true,
this.agentConnectTimeout = const Duration(seconds: 20),
}) : room = room ?? Room();
this.encryption,
}) : isRoomProvided = room != null,
room = room ?? Room() {
_validateEncryptionConfiguration();
}

SessionOptions._({
required this.room,
required this.isRoomProvided,
required this.preConnectAudio,
required this.agentConnectTimeout,
this.encryption,
}) {
_validateEncryptionConfiguration();
}
Comment on lines 87 to +105
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a bad / nonsensical suggestion as I am not too familiar with typical flutter patterns, but would be worthwhile to get all session initialization (constructor and alternate constructor paths like the copyWith static method) going through the same core _ static method for initialization?


SessionOptions copyWith({
Room? room,
bool? preConnectAudio,
Duration? agentConnectTimeout,
SessionEncryptionOptions? encryption,
}) {
return SessionOptions(
return SessionOptions._(
room: room ?? this.room,
isRoomProvided: room != null ? true : isRoomProvided,
preConnectAudio: preConnectAudio ?? this.preConnectAudio,
agentConnectTimeout: agentConnectTimeout ?? this.agentConnectTimeout,
encryption: encryption ?? this.encryption,
);
}

void _validateEncryptionConfiguration() {
if (isRoomProvided && encryption != null) {
throw ArgumentError.value(
encryption,
'encryption',
'Cannot be provided when room is also provided. Configure E2EE on the Room directly.',
);
}
}
}
Loading