* chore: e2ee.

* update.

* update.

* update.

* update.

* chore: Use feat/frame-encryption branch of flutter-webrtc.

* chore: Add E2EEKEY defines for dart environment, and e2ee switch.

* Add encodedInsertableStreams to RTCConfiguration.

* update.

* feat: Add e2ee indicator for Participant.

* feat: add e2ee worker js for flutter web.

* dart format.

* remove unused file.

* fix flutter analyze .

* update.

* update.

* add: indicate for decryption failure, and string key.

* remove .lock files.

* update.

* update.

* update e2ee.worker for web.

* feat: support setCodecPreferences.

* state TrackE2EEStateEvent.

* fix wrong import interface from dart_webrtc.

* update.

* update.

* update.

* update.

* update pubspec.lock.

* chore: update protocol and add EncryptionType for Participant.

* Update lib/src/e2ee/options.dart

Co-authored-by: Théo Monnom <theo.monnom@outlook.com>

* fix typo.

* revert changes for internal import.

* Add _cleanUp() for previous room.

* Add e2ee supports detection method for native/web.

* Remove redundant overriding methods.

* dart format.

* Add e2ee.worker code and deployment docs.

* chore: remove duplicate words.

* chore: using Pbkdf2 derive the key.

* Update pubspec.yaml

* fix e2ee for safari.

* fix key length.

* update e2ee.worker.dart.js.

* fix.

* update proto.

* update.

* chore: add simulate for rachetKey.

* update.

* chore: key ratchet for flutter web.

* update.

* update.

* update.

* chore: key ratchet export for web.

* bump version for xframeworks.

* update.

* chore: some changes for key safety ratcheting.

* update.

* fix typo.

* update.

* rename.

* magic bytes for web.

* bump version for flutter-webrtc.

* fix analyzer.

---------

Co-authored-by: Théo Monnom <theo.monnom@outlook.com>
This commit is contained in:
CloudWebRTC
2023-04-27 17:54:32 +08:00
committed by GitHub
parent 3407221fc4
commit 26947e96d6
42 changed files with 12523 additions and 39 deletions
+2
View File
@@ -13,4 +13,6 @@ proto:
format:
flutter format --set-exit-if-changed -l 100 .
e2ee: dart compile js .\web\e2ee.worker.dart -o .\example\web\e2ee.worker.dart.js
.PHONY: proto format
+22 -7
View File
@@ -20,13 +20,13 @@ More Docs and guides are available at [https://docs.livekit.io](https://docs.liv
## Current supported features
| Feature | Subscribe/Publish | Simulcast | Background audio | Screen sharing |
| :-----: | :---------------: | :-------: | :--------------: | :------------: |
| Web | 🟢 | 🟢 | 🟢 | 🟢 |
| iOS | 🟢 | 🟢 | 🟢 | 🟢 |
| Android | 🟢 | 🟢 | 🟢 | 🟢 |
| Mac | 🟢 | 🟢 | 🟢 | 🟢 |
| Windows | 🟢 | 🟢 | 🟢 | 🟢 |
| Feature | Subscribe/Publish | Simulcast | Background audio | Screen sharing | End to End Encryption |
| :-----: | :---------------: | :-------: | :--------------: | :------------: | :------------: |
| Web | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
| iOS | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
| Android | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
| Mac | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
| Windows | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
🟢 = Available
@@ -215,6 +215,21 @@ try {
}
```
### End to End Encryption
LiveKit supports end-to-end encryption for audio/video data sent over the network.
By default, the native platform can support E2EE without any settings, but for flutter web, you need to use the following steps to create `e2ee.worker.dart.js` file.
```bash
# for example app
dart compile js .\web\e2ee.worker.dart -o .\example\web\e2ee.worker.dart.js
# for your project
export YOU_PROJECT_DIR=your_project_dir
git clone https://github.com/livekit/client-sdk-flutter.git
cd client-sdk-flutter && flutter pub get
dart compile js .\web\e2ee.worker.dart -o ${YOU_PROJECT_DIR}\web\e2ee.worker.dart.js
```
### Advanced track manipulation
The setCameraEnabled/setMicrophoneEnabled helpers are wrappers around the Track API.
+2
View File
@@ -30,6 +30,7 @@ analyzer:
- '**/*.pbenum.dart'
- '**/*.pbjson.dart'
- '**/*.pbserver.dart'
- 'web/*.dart'
linter:
rules:
@@ -39,3 +40,4 @@ linter:
prefer_single_quotes: true
unnecessary_brace_in_string_interps: false
unawaited_futures: true
depend_on_referenced_packages: false
+1
View File
@@ -194,4 +194,5 @@ enum SimulateScenarioResult {
serverLeave,
switchCandidate,
clear,
e2eeKeyRatchet,
}
+46
View File
@@ -25,14 +25,18 @@ class _ConnectPageState extends State<ConnectPage> {
static const _storeKeyAdaptiveStream = 'adaptive-stream';
static const _storeKeyDynacast = 'dynacast';
static const _storeKeyFastConnect = 'fast-connect';
static const _storeKeyE2EE = 'e2ee';
static const _storeKeySharedKey = 'shared-key';
final _uriCtrl = TextEditingController();
final _tokenCtrl = TextEditingController();
final _sharedKeyCtrl = TextEditingController();
bool _simulcast = true;
bool _adaptiveStream = true;
bool _dynacast = true;
bool _busy = false;
bool _fastConnect = false;
bool _e2ee = false;
@override
void initState() {
@@ -56,11 +60,15 @@ class _ConnectPageState extends State<ConnectPage> {
_tokenCtrl.text = const bool.hasEnvironment('TOKEN')
? const String.fromEnvironment('TOKEN')
: prefs.getString(_storeKeyToken) ?? '';
_sharedKeyCtrl.text = const bool.hasEnvironment('E2EEKEY')
? const String.fromEnvironment('E2EEKEY')
: prefs.getString(_storeKeySharedKey) ?? '';
setState(() {
_simulcast = prefs.getBool(_storeKeySimulcast) ?? true;
_adaptiveStream = prefs.getBool(_storeKeyAdaptiveStream) ?? true;
_dynacast = prefs.getBool(_storeKeyDynacast) ?? true;
_fastConnect = prefs.getBool(_storeKeyFastConnect) ?? false;
_e2ee = prefs.getBool(_storeKeyE2EE) ?? false;
});
}
@@ -69,10 +77,12 @@ class _ConnectPageState extends State<ConnectPage> {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_storeKeyUri, _uriCtrl.text);
await prefs.setString(_storeKeyToken, _tokenCtrl.text);
await prefs.setString(_storeKeySharedKey, _sharedKeyCtrl.text);
await prefs.setBool(_storeKeySimulcast, _simulcast);
await prefs.setBool(_storeKeyAdaptiveStream, _adaptiveStream);
await prefs.setBool(_storeKeyDynacast, _dynacast);
await prefs.setBool(_storeKeyFastConnect, _fastConnect);
await prefs.setBool(_storeKeyE2EE, _e2ee);
}
Future<void> _connect(BuildContext ctx) async {
@@ -93,6 +103,13 @@ class _ConnectPageState extends State<ConnectPage> {
// Create a Listener before connecting
final listener = room.createListener();
E2EEOptions? e2eeOptions;
if (_e2ee) {
final keyProvider = await BaseKeyProvider.create();
e2eeOptions = E2EEOptions(keyProvider: keyProvider);
var sharedKey = _sharedKeyCtrl.text;
await keyProvider.setKey(sharedKey);
}
// Try to connect to the room
// This will throw an Exception if it fails for any reason.
@@ -107,6 +124,7 @@ class _ConnectPageState extends State<ConnectPage> {
),
defaultScreenShareCaptureOptions:
const ScreenShareCaptureOptions(useiOSBroadcastExtension: true),
e2eeOptions: e2eeOptions,
),
fastConnectOptions: _fastConnect
? FastConnectOptions(
@@ -115,6 +133,7 @@ class _ConnectPageState extends State<ConnectPage> {
)
: null,
);
await Navigator.push<void>(
ctx,
MaterialPageRoute(builder: (_) => RoomPage(room, listener)),
@@ -136,6 +155,13 @@ class _ConnectPageState extends State<ConnectPage> {
});
}
void _setE2EE(bool? value) async {
if (value == null || _e2ee == value) return;
setState(() {
_e2ee = value;
});
}
void _setAdaptiveStream(bool? value) async {
if (value == null || _adaptiveStream == value) return;
setState(() {
@@ -192,6 +218,26 @@ class _ConnectPageState extends State<ConnectPage> {
ctrl: _tokenCtrl,
),
),
Padding(
padding: const EdgeInsets.only(bottom: 25),
child: LKTextField(
label: 'Shared Key',
ctrl: _sharedKeyCtrl,
),
),
Padding(
padding: const EdgeInsets.only(bottom: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('E2EE'),
Switch(
value: _e2ee,
onChanged: (value) => _setE2EE(value),
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 5),
child: Row(
+5
View File
@@ -66,6 +66,7 @@ class _RoomPageState extends State<RoomPage> {
})
..on<LocalTrackPublishedEvent>((_) => _sortParticipants())
..on<LocalTrackUnpublishedEvent>((_) => _sortParticipants())
..on<TrackE2EEStateEvent>(_onE2EEStateEvent)
..on<ParticipantNameUpdatedEvent>((event) {
print(
'Participant name updated: ${event.participant.identity}, name => ${event.name}');
@@ -102,6 +103,10 @@ class _RoomPageState extends State<RoomPage> {
_sortParticipants();
}
void _onE2EEStateEvent(TrackE2EEStateEvent e2eeState) {
print('e2ee state: $e2eeState');
}
void _sortParticipants() {
List<ParticipantTrack> userMediaTracks = [];
List<ParticipantTrack> screenTracks = [];
+5
View File
@@ -226,6 +226,11 @@ class _ControlsWidgetState extends State<ControlsWidget> {
final result = await context.showSimulateScenarioDialog();
if (result != null) {
print('${result}');
if (SimulateScenarioResult.e2eeKeyRatchet == result) {
await widget.room.e2eeManager?.ratchetKey();
}
await widget.room.sendSimulateScenario(
signalReconnect:
result == SimulateScenarioResult.signalReconnect ? true : null,
+2
View File
@@ -153,6 +153,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
firstAudioPublication?.subscribed == true,
connectionQuality: widget.participant.connectionQuality,
isScreenShare: widget.isScreenShare,
enabledE2EE: widget.participant.firstTrackEncryptionType !=
EncryptionType.kNone,
),
],
),
+10
View File
@@ -18,12 +18,14 @@ class ParticipantInfoWidget extends StatelessWidget {
final bool audioAvailable;
final ConnectionQuality connectionQuality;
final bool isScreenShare;
final bool enabledE2EE;
const ParticipantInfoWidget({
this.title,
this.audioAvailable = true,
this.connectionQuality = ConnectionQuality.unknown,
this.isScreenShare = false,
this.enabledE2EE = false,
Key? key,
}) : super(key: key);
@@ -77,6 +79,14 @@ class ParticipantInfoWidget extends StatelessWidget {
size: 16,
),
),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Icon(
enabledE2EE ? EvaIcons.lock : EvaIcons.unlock,
color: enabledE2EE ? Colors.green : Colors.red,
size: 16,
),
),
],
),
);
File diff suppressed because one or more lines are too long
+232
View File
@@ -0,0 +1,232 @@
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/collection.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/algorithms.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/boollist.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/canonicalized_map.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/combined_wrappers/combined_iterable.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/combined_wrappers/combined_iterator.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/combined_wrappers/combined_list.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/combined_wrappers/combined_map.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/comparators.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/empty_unmodifiable_set.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/equality.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/equality_map.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/equality_set.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/functions.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/iterable_extensions.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/iterable_zip.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/list_extensions.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/priority_queue.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/queue_list.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/union_set.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/union_set_controller.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/unmodifiable_wrappers.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/utils.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/wrappers.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/js-0.6.5/lib/js.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/meta-1.8.0/lib/meta.dart
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/meta-1.8.0/lib/meta_meta.dart
file:///Users/weiweiduan/Desktop/projects/flutter-webrtc/lib/src/web/rtc_transform_stream.dart
file:///Users/weiweiduan/Desktop/projects/livekit/client-sdk-flutter/.dart_tool/package_config.json
file:///Users/weiweiduan/Desktop/projects/livekit/client-sdk-flutter/web/crypto.dart
file:///Users/weiweiduan/Desktop/projects/livekit/client-sdk-flutter/web/e2ee.cryptor.dart
file:///Users/weiweiduan/Desktop/projects/livekit/client-sdk-flutter/web/e2ee.utils.dart
file:///Users/weiweiduan/Desktop/projects/livekit/client-sdk-flutter/web/e2ee.worker.dart
file:///Users/weiweiduan/bin/flutter/bin/cache/dart-sdk/lib/_internal/dart2js_platform.dill
file:///Users/weiweiduan/bin/flutter/bin/cache/dart-sdk/lib/libraries.json
org-dartlang-sdk:///lib/_http/crypto.dart
org-dartlang-sdk:///lib/_http/embedder_config.dart
org-dartlang-sdk:///lib/_http/http.dart
org-dartlang-sdk:///lib/_http/http_date.dart
org-dartlang-sdk:///lib/_http/http_headers.dart
org-dartlang-sdk:///lib/_http/http_impl.dart
org-dartlang-sdk:///lib/_http/http_parser.dart
org-dartlang-sdk:///lib/_http/http_session.dart
org-dartlang-sdk:///lib/_http/http_testing.dart
org-dartlang-sdk:///lib/_http/overrides.dart
org-dartlang-sdk:///lib/_http/websocket.dart
org-dartlang-sdk:///lib/_http/websocket_impl.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/annotations.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/async_patch.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/collection_patch.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/constant_map.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/convert_patch.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/core_patch.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/dart2js_runtime_metrics.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/developer_patch.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/foreign_helper.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/instantiation.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/interceptors.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/internal_patch.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/io_patch.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/isolate_patch.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_array.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_helper.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_names.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_number.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_patch.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_primitives.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_string.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/late_helper.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/linked_hash_map.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/math_patch.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/native_helper.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/native_typed_data.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/regexp_helper.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/string_helper.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/synced/async_await_error_codes.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/synced/embedded_names.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/synced/load_library_priority.dart
org-dartlang-sdk:///lib/_internal/js_runtime/lib/typed_data_patch.dart
org-dartlang-sdk:///lib/_internal/js_shared/lib/js_util_patch.dart
org-dartlang-sdk:///lib/_internal/js_shared/lib/rti.dart
org-dartlang-sdk:///lib/_internal/js_shared/lib/synced/embedded_names.dart
org-dartlang-sdk:///lib/_internal/js_shared/lib/synced/recipe_syntax.dart
org-dartlang-sdk:///lib/async/async.dart
org-dartlang-sdk:///lib/async/async_error.dart
org-dartlang-sdk:///lib/async/broadcast_stream_controller.dart
org-dartlang-sdk:///lib/async/deferred_load.dart
org-dartlang-sdk:///lib/async/future.dart
org-dartlang-sdk:///lib/async/future_impl.dart
org-dartlang-sdk:///lib/async/schedule_microtask.dart
org-dartlang-sdk:///lib/async/stream.dart
org-dartlang-sdk:///lib/async/stream_controller.dart
org-dartlang-sdk:///lib/async/stream_impl.dart
org-dartlang-sdk:///lib/async/stream_pipe.dart
org-dartlang-sdk:///lib/async/stream_transformers.dart
org-dartlang-sdk:///lib/async/timer.dart
org-dartlang-sdk:///lib/async/zone.dart
org-dartlang-sdk:///lib/collection/collection.dart
org-dartlang-sdk:///lib/collection/collections.dart
org-dartlang-sdk:///lib/collection/hash_map.dart
org-dartlang-sdk:///lib/collection/hash_set.dart
org-dartlang-sdk:///lib/collection/iterable.dart
org-dartlang-sdk:///lib/collection/iterator.dart
org-dartlang-sdk:///lib/collection/linked_hash_map.dart
org-dartlang-sdk:///lib/collection/linked_hash_set.dart
org-dartlang-sdk:///lib/collection/linked_list.dart
org-dartlang-sdk:///lib/collection/list.dart
org-dartlang-sdk:///lib/collection/maps.dart
org-dartlang-sdk:///lib/collection/queue.dart
org-dartlang-sdk:///lib/collection/set.dart
org-dartlang-sdk:///lib/collection/splay_tree.dart
org-dartlang-sdk:///lib/convert/ascii.dart
org-dartlang-sdk:///lib/convert/base64.dart
org-dartlang-sdk:///lib/convert/byte_conversion.dart
org-dartlang-sdk:///lib/convert/chunked_conversion.dart
org-dartlang-sdk:///lib/convert/codec.dart
org-dartlang-sdk:///lib/convert/convert.dart
org-dartlang-sdk:///lib/convert/converter.dart
org-dartlang-sdk:///lib/convert/encoding.dart
org-dartlang-sdk:///lib/convert/html_escape.dart
org-dartlang-sdk:///lib/convert/json.dart
org-dartlang-sdk:///lib/convert/latin1.dart
org-dartlang-sdk:///lib/convert/line_splitter.dart
org-dartlang-sdk:///lib/convert/string_conversion.dart
org-dartlang-sdk:///lib/convert/utf.dart
org-dartlang-sdk:///lib/core/annotations.dart
org-dartlang-sdk:///lib/core/bigint.dart
org-dartlang-sdk:///lib/core/bool.dart
org-dartlang-sdk:///lib/core/comparable.dart
org-dartlang-sdk:///lib/core/core.dart
org-dartlang-sdk:///lib/core/date_time.dart
org-dartlang-sdk:///lib/core/double.dart
org-dartlang-sdk:///lib/core/duration.dart
org-dartlang-sdk:///lib/core/enum.dart
org-dartlang-sdk:///lib/core/errors.dart
org-dartlang-sdk:///lib/core/exceptions.dart
org-dartlang-sdk:///lib/core/function.dart
org-dartlang-sdk:///lib/core/int.dart
org-dartlang-sdk:///lib/core/invocation.dart
org-dartlang-sdk:///lib/core/iterable.dart
org-dartlang-sdk:///lib/core/iterator.dart
org-dartlang-sdk:///lib/core/list.dart
org-dartlang-sdk:///lib/core/map.dart
org-dartlang-sdk:///lib/core/null.dart
org-dartlang-sdk:///lib/core/num.dart
org-dartlang-sdk:///lib/core/object.dart
org-dartlang-sdk:///lib/core/pattern.dart
org-dartlang-sdk:///lib/core/print.dart
org-dartlang-sdk:///lib/core/record.dart
org-dartlang-sdk:///lib/core/regexp.dart
org-dartlang-sdk:///lib/core/set.dart
org-dartlang-sdk:///lib/core/sink.dart
org-dartlang-sdk:///lib/core/stacktrace.dart
org-dartlang-sdk:///lib/core/stopwatch.dart
org-dartlang-sdk:///lib/core/string.dart
org-dartlang-sdk:///lib/core/string_buffer.dart
org-dartlang-sdk:///lib/core/string_sink.dart
org-dartlang-sdk:///lib/core/symbol.dart
org-dartlang-sdk:///lib/core/type.dart
org-dartlang-sdk:///lib/core/uri.dart
org-dartlang-sdk:///lib/core/weak.dart
org-dartlang-sdk:///lib/developer/developer.dart
org-dartlang-sdk:///lib/developer/extension.dart
org-dartlang-sdk:///lib/developer/profiler.dart
org-dartlang-sdk:///lib/developer/service.dart
org-dartlang-sdk:///lib/developer/timeline.dart
org-dartlang-sdk:///lib/html/dart2js/html_dart2js.dart
org-dartlang-sdk:///lib/html/html_common/conversions.dart
org-dartlang-sdk:///lib/html/html_common/conversions_dart2js.dart
org-dartlang-sdk:///lib/html/html_common/css_class_set.dart
org-dartlang-sdk:///lib/html/html_common/device.dart
org-dartlang-sdk:///lib/html/html_common/filtered_element_list.dart
org-dartlang-sdk:///lib/html/html_common/html_common_dart2js.dart
org-dartlang-sdk:///lib/html/html_common/lists.dart
org-dartlang-sdk:///lib/html/html_common/metadata.dart
org-dartlang-sdk:///lib/indexed_db/dart2js/indexed_db_dart2js.dart
org-dartlang-sdk:///lib/internal/async_cast.dart
org-dartlang-sdk:///lib/internal/bytes_builder.dart
org-dartlang-sdk:///lib/internal/cast.dart
org-dartlang-sdk:///lib/internal/errors.dart
org-dartlang-sdk:///lib/internal/internal.dart
org-dartlang-sdk:///lib/internal/iterable.dart
org-dartlang-sdk:///lib/internal/linked_list.dart
org-dartlang-sdk:///lib/internal/list.dart
org-dartlang-sdk:///lib/internal/patch.dart
org-dartlang-sdk:///lib/internal/print.dart
org-dartlang-sdk:///lib/internal/sort.dart
org-dartlang-sdk:///lib/internal/symbol.dart
org-dartlang-sdk:///lib/io/common.dart
org-dartlang-sdk:///lib/io/data_transformer.dart
org-dartlang-sdk:///lib/io/directory.dart
org-dartlang-sdk:///lib/io/directory_impl.dart
org-dartlang-sdk:///lib/io/embedder_config.dart
org-dartlang-sdk:///lib/io/eventhandler.dart
org-dartlang-sdk:///lib/io/file.dart
org-dartlang-sdk:///lib/io/file_impl.dart
org-dartlang-sdk:///lib/io/file_system_entity.dart
org-dartlang-sdk:///lib/io/io.dart
org-dartlang-sdk:///lib/io/io_resource_info.dart
org-dartlang-sdk:///lib/io/io_service.dart
org-dartlang-sdk:///lib/io/io_sink.dart
org-dartlang-sdk:///lib/io/link.dart
org-dartlang-sdk:///lib/io/namespace_impl.dart
org-dartlang-sdk:///lib/io/network_profiling.dart
org-dartlang-sdk:///lib/io/overrides.dart
org-dartlang-sdk:///lib/io/platform.dart
org-dartlang-sdk:///lib/io/platform_impl.dart
org-dartlang-sdk:///lib/io/process.dart
org-dartlang-sdk:///lib/io/secure_server_socket.dart
org-dartlang-sdk:///lib/io/secure_socket.dart
org-dartlang-sdk:///lib/io/security_context.dart
org-dartlang-sdk:///lib/io/service_object.dart
org-dartlang-sdk:///lib/io/socket.dart
org-dartlang-sdk:///lib/io/stdio.dart
org-dartlang-sdk:///lib/io/string_transformer.dart
org-dartlang-sdk:///lib/io/sync_socket.dart
org-dartlang-sdk:///lib/isolate/capability.dart
org-dartlang-sdk:///lib/isolate/isolate.dart
org-dartlang-sdk:///lib/js/_js.dart
org-dartlang-sdk:///lib/js/_js_annotations.dart
org-dartlang-sdk:///lib/js/_js_client.dart
org-dartlang-sdk:///lib/js/js.dart
org-dartlang-sdk:///lib/js_util/js_util.dart
org-dartlang-sdk:///lib/math/math.dart
org-dartlang-sdk:///lib/math/point.dart
org-dartlang-sdk:///lib/math/random.dart
org-dartlang-sdk:///lib/math/rectangle.dart
org-dartlang-sdk:///lib/svg/dart2js/svg_dart2js.dart
org-dartlang-sdk:///lib/typed_data/typed_data.dart
org-dartlang-sdk:///lib/typed_data/unmodifiable_typed_data.dart
org-dartlang-sdk:///lib/web_audio/dart2js/web_audio_dart2js.dart
org-dartlang-sdk:///lib/web_gl/dart2js/web_gl_dart2js.dart
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+2 -2
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'livekit_client'
s.version = '1.1.13'
s.version = '1.2.2'
s.summary = 'Open source platform for real-time audio and video.'
s.description = 'Open source platform for real-time audio and video.'
s.homepage = 'https://livekit.io/'
@@ -16,5 +16,5 @@ Pod::Spec.new do |s|
s.static_framework = true
s.dependency 'Flutter'
s.dependency 'WebRTC-SDK', '104.5112.09'
s.dependency 'WebRTC-SDK', '104.5112.16'
end
+4
View File
@@ -4,6 +4,10 @@ library livekit_client;
export 'src/core/room.dart';
export 'src/events.dart';
export 'src/exceptions.dart';
export 'src/e2ee/e2ee_manager.dart';
export 'src/e2ee/events.dart';
export 'src/e2ee/options.dart';
export 'src/e2ee/key_provider.dart';
export 'src/extensions.dart' show WidgetsBindingCompatible;
export 'src/hardware/hardware.dart';
export 'src/livekit.dart';
+22
View File
@@ -6,6 +6,7 @@ import 'package:collection/collection.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:meta/meta.dart';
import '../e2ee/options.dart';
import '../events.dart';
import '../exceptions.dart';
import '../extensions.dart';
@@ -188,6 +189,21 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
}) async {
// TODO: Check if cid already published
lk_models.Encryption_Type encryptionType = lk_models.Encryption_Type.NONE;
if (roomOptions.e2eeOptions != null) {
switch (roomOptions.e2eeOptions!.encryptionType) {
case EncryptionType.kNone:
encryptionType = lk_models.Encryption_Type.NONE;
break;
case EncryptionType.kGcm:
encryptionType = lk_models.Encryption_Type.GCM;
break;
case EncryptionType.kCustom:
encryptionType = lk_models.Encryption_Type.CUSTOM;
break;
}
}
// send request to add track
signalClient.sendAddTrack(
cid: cid,
@@ -197,6 +213,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
dimensions: dimensions,
dtx: dtx,
videoLayers: videoLayers,
encryptionType: encryptionType,
);
// wait for response, or timeout
@@ -299,6 +316,11 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
);
}
if (kIsWeb && roomOptions.e2eeOptions != null) {
rtcConfiguration =
rtcConfiguration.copyWith(encodedInsertableStreams: true);
}
return rtcConfiguration;
}
+29 -8
View File
@@ -7,7 +7,9 @@ import 'package:livekit_client/src/support/app_state.dart';
import 'package:meta/meta.dart';
import '../core/signal_client.dart';
import '../e2ee/e2ee_manager.dart';
import '../events.dart';
import '../exceptions.dart';
import '../extensions.dart';
import '../internal/events.dart';
import '../logger.dart';
@@ -71,6 +73,9 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
String? get serverRegion => _serverRegion;
String? _serverRegion;
E2EEManager? get e2eeManager => _e2eeManager;
E2EEManager? _e2eeManager;
bool get isRecording => _isRecording;
bool _isRecording = false;
@@ -140,14 +145,22 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
ConnectOptions? connectOptions,
RoomOptions? roomOptions,
FastConnectOptions? fastConnectOptions,
}) =>
engine.connect(
url,
token,
connectOptions: connectOptions,
roomOptions: roomOptions,
fastConnectOptions: fastConnectOptions,
);
}) {
if (roomOptions?.e2eeOptions != null) {
if (!lkPlatformSupportsE2EE()) {
throw LiveKitE2EEException('E2EE is not supported on this platform');
}
_e2eeManager = E2EEManager(roomOptions!.e2eeOptions!.keyProvider);
_e2eeManager!.setup(this);
}
return engine.connect(
url,
token,
connectOptions: connectOptions,
roomOptions: roomOptions,
fastConnectOptions: fastConnectOptions,
);
}
void _setUpSignalListeners() => _signalListener
..on<SignalJoinResponseEvent>((event) {
@@ -378,6 +391,14 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
await _cleanUp();
}
Future<void> setE2EEEnabled(bool enabled) async {
if (_e2eeManager != null) {
await _e2eeManager!.setEnabled(enabled);
} else {
throw LiveKitE2EEException('_e2eeManager not setup!');
}
}
RemoteParticipant _getOrCreateRemoteParticipant(
String sid, lk_models.ParticipantInfo? info) {
RemoteParticipant? participant = _participants[sid];
+2
View File
@@ -350,6 +350,7 @@ extension SignalClientRequests on SignalClient {
required String name,
required lk_models.TrackType type,
required lk_models.TrackSource source,
required lk_models.Encryption_Type encryptionType,
VideoDimensions? dimensions,
bool? dtx,
Iterable<lk_models.VideoLayer>? videoLayers,
@@ -359,6 +360,7 @@ extension SignalClientRequests on SignalClient {
name: name,
type: type,
source: source,
encryption: encryptionType,
);
if (type == lk_models.TrackType.VIDEO) {
+179
View File
@@ -0,0 +1,179 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:livekit_client/src/e2ee/events.dart';
import 'package:livekit_client/src/extensions.dart';
import '../events.dart';
import '../core/room.dart';
import '../managers/event.dart';
import 'key_provider.dart';
class E2EEManager {
Room? _room;
final Map<String, FrameCryptor> _frameCryptors = {};
final List<FrameCryptor> _senderFrameCryptors = [];
final BaseKeyProvider _keyProvider;
final Algorithm _algorithm = Algorithm.kAesGcm;
bool _enabled = true;
EventsListener<RoomEvent>? _listener;
E2EEManager(this._keyProvider);
Future<void> setup(Room room) async {
if (_room != room) {
await _cleanUp();
_room = room;
_listener = _room!.createListener();
_listener!
..on<LocalTrackPublishedEvent>((event) async {
var trackId = event.publication.sid;
var participantId = event.participant.sid;
var frameCryptor = await _addRtpSender(
event.publication.track!.sender!,
participantId,
trackId,
event.publication.track!.kind.name.toLowerCase());
if (kIsWeb && event.publication.track!.codec != null) {
await frameCryptor.updateCodec(event.publication.track!.codec!);
}
frameCryptor.onFrameCryptorStateChanged = (trackId, state) {
if (kDebugMode) {
print(
'Sender::onFrameCryptorStateChanged: $state, trackId: $trackId');
}
var participant = event.participant;
[event.participant.events, participant.room.events]
.emit(TrackE2EEStateEvent(
participant: participant,
publication: event.publication,
state: _e2eeStateFromFrameCryptoState(state),
));
};
_senderFrameCryptors.add(frameCryptor);
})
..on<LocalTrackUnpublishedEvent>((event) async {
var trackId = event.publication.sid;
var frameCryptor = _frameCryptors.remove(trackId);
_senderFrameCryptors.remove(frameCryptor);
await frameCryptor?.dispose();
})
..on<TrackSubscribedEvent>((event) async {
var trackId = event.publication.sid;
var participantId = event.participant.sid;
var frameCryptor = await _addRtpReceiver(event.track.receiver!,
participantId, trackId, event.track.kind.name.toLowerCase());
if (kIsWeb) {
var codec = event.publication.mimeType.split('/')[1];
await frameCryptor.updateCodec(codec.toLowerCase());
}
frameCryptor.onFrameCryptorStateChanged = (trackId, state) {
if (kDebugMode) {
print(
'Receiver::onFrameCryptorStateChanged: $state, trackId: $trackId');
}
var participant = event.participant;
[event.participant.events, participant.room.events]
.emit(TrackE2EEStateEvent(
participant: participant,
publication: event.publication,
state: _e2eeStateFromFrameCryptoState(state),
));
};
})
..on<TrackUnsubscribedEvent>((event) async {
var trackId = event.publication.sid;
var frameCryptor = _frameCryptors.remove(trackId);
await frameCryptor?.dispose();
});
}
}
Future<void> ratchetKey() async {
for (var frameCryptor in _senderFrameCryptors) {
var newKey = await _keyProvider.ratchetKey(frameCryptor.participantId, 0);
if (kDebugMode) {
print('newKey: $newKey');
}
}
}
Future<void> _cleanUp() async {
await _listener?.cancelAll();
await _listener?.dispose();
_listener = null;
for (var frameCryptor in _frameCryptors.values) {
await frameCryptor.dispose();
}
_frameCryptors.clear();
}
Future<FrameCryptor> _addRtpSender(RTCRtpSender sender, String participantId,
String trackId, String kind) async {
var pid = '$kind-sender-$participantId-$trackId';
var frameCryptor = await FrameCryptorFactory.instance
.createFrameCryptorForRtpSender(
participantId: pid,
sender: sender,
algorithm: _algorithm,
keyProvider: _keyProvider.keyProvider);
_frameCryptors[trackId] = frameCryptor;
await frameCryptor.setEnabled(_enabled);
if (_keyProvider.options.sharedKey) {
await _keyProvider.keyProvider
.setKey(participantId: pid, index: 0, key: _keyProvider.sharedKey!);
await frameCryptor.setKeyIndex(0);
}
return frameCryptor;
}
Future<FrameCryptor> _addRtpReceiver(RTCRtpReceiver receiver,
String participantId, String trackId, String kind) async {
var pid = '$kind-receiver-$participantId-$trackId';
var frameCryptor = await FrameCryptorFactory.instance
.createFrameCryptorForRtpReceiver(
participantId: pid,
receiver: receiver,
algorithm: _algorithm,
keyProvider: _keyProvider.keyProvider);
_frameCryptors[trackId] = frameCryptor;
await frameCryptor.setEnabled(_enabled);
if (_keyProvider.options.sharedKey) {
await _keyProvider.keyProvider
.setKey(participantId: pid, index: 0, key: _keyProvider.sharedKey!);
await frameCryptor.setKeyIndex(0);
}
return frameCryptor;
}
Future<void> setEnabled(bool enabled) async {
_enabled = enabled;
for (var frameCryptor in _frameCryptors.entries) {
await frameCryptor.value.setEnabled(enabled);
if (_keyProvider.options.sharedKey) {
await _keyProvider.keyProvider.setKey(
participantId: frameCryptor.key,
index: 0,
key: _keyProvider.sharedKey!);
await frameCryptor.value.setKeyIndex(0);
}
}
}
E2EEState _e2eeStateFromFrameCryptoState(FrameCryptorState state) {
switch (state) {
case FrameCryptorState.FrameCryptorStateNew:
return E2EEState.kNew;
case FrameCryptorState.FrameCryptorStateOk:
return E2EEState.kOk;
case FrameCryptorState.FrameCryptorStateMissingKey:
return E2EEState.kMissingKey;
case FrameCryptorState.FrameCryptorStateEncryptionFailed:
return E2EEState.kEncryptionFailed;
case FrameCryptorState.FrameCryptorStateDecryptionFailed:
return E2EEState.kDecryptionFailed;
case FrameCryptorState.FrameCryptorStateInternalError:
return E2EEState.kInternalError;
case FrameCryptorState.FrameCryptorStateKeyRatcheted:
return E2EEState.kKeyRatcheted;
}
}
}
+28
View File
@@ -0,0 +1,28 @@
import '../../livekit_client.dart';
enum E2EEState {
kNew,
kOk,
kKeyRatcheted,
kMissingKey,
kEncryptionFailed,
kDecryptionFailed,
kInternalError,
}
/// The [E2EEState] on the track.
/// Emitted by [E2EEManager].
class TrackE2EEStateEvent with RoomEvent, ParticipantEvent {
final Participant participant;
final TrackPublication publication;
final E2EEState state;
const TrackE2EEStateEvent({
required this.participant,
required this.publication,
required this.state,
});
@override
String toString() => '${runtimeType}'
'(participant: ${participant}, publication: ${publication}, state: ${state})';
}
+87
View File
@@ -0,0 +1,87 @@
import 'dart:typed_data';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
const defaultRatchetSalt = 'LKFrameEncryptionKey';
const defaultMagicBytes = 'LK-ROCKS';
const defaultRatchetWindowSize = 16;
class KeyInfo {
final String participantId;
final int keyIndex;
final Uint8List key;
KeyInfo({
required this.participantId,
required this.keyIndex,
required this.key,
});
}
abstract class KeyProvider {
Future<void> setKey(String key, {String? participantId, int keyIndex = 0});
Future<Uint8List> ratchetKey(String participantId, int index);
rtc.KeyProvider get keyProvider;
}
class BaseKeyProvider implements KeyProvider {
final Map<String, Map<int, Uint8List>> _keys = {};
Uint8List? _sharedKey;
final rtc.KeyProviderOptions options;
final rtc.KeyProvider _keyProvider;
@override
rtc.KeyProvider get keyProvider => _keyProvider;
Uint8List? get sharedKey => _sharedKey;
BaseKeyProvider(this._keyProvider, this.options);
static Future<BaseKeyProvider> create({
bool sharedKey = true,
String? ratchetSalt,
String? uncryptedMagicBytes,
int? ratchetWindowSize,
}) async {
rtc.KeyProviderOptions options = rtc.KeyProviderOptions(
sharedKey: sharedKey,
ratchetSalt:
Uint8List.fromList((ratchetSalt ?? defaultRatchetSalt).codeUnits),
ratchetWindowSize: ratchetWindowSize ?? defaultRatchetWindowSize,
uncryptedMagicBytes: Uint8List.fromList(
(uncryptedMagicBytes ?? defaultMagicBytes).codeUnits),
);
final keyProvider = await rtc.FrameCryptorFactory.instance
.createDefaultKeyProvider(options);
return BaseKeyProvider(keyProvider, options);
}
@override
Future<Uint8List> ratchetKey(String participantId, int index) =>
_keyProvider.ratchetKey(participantId: participantId, index: index);
@override
Future<void> setKey(String key,
{String? participantId, int keyIndex = 0}) async {
if (options.sharedKey) {
_sharedKey = Uint8List.fromList(key.codeUnits);
return;
}
final keyInfo = KeyInfo(
participantId: participantId ?? '',
keyIndex: keyIndex,
key: Uint8List.fromList(key.codeUnits),
);
return _setKey(keyInfo);
}
Future<void> _setKey(KeyInfo keyInfo) async {
if (!_keys.containsKey(keyInfo.participantId)) {
_keys[keyInfo.participantId] = {};
}
_keys[keyInfo.participantId]![keyInfo.keyIndex] = keyInfo.key;
await _keyProvider.setKey(
participantId: keyInfo.participantId,
index: keyInfo.keyIndex,
key: keyInfo.key,
);
}
}
+13
View File
@@ -0,0 +1,13 @@
import 'key_provider.dart';
enum EncryptionType {
kNone,
kGcm,
kCustom,
}
class E2EEOptions {
final BaseKeyProvider keyProvider;
final EncryptionType encryptionType = EncryptionType.kGcm;
const E2EEOptions({required this.keyProvider});
}
+8
View File
@@ -57,3 +57,11 @@ class DataPublishException extends LiveKitException {
class TimeoutException extends LiveKitException {
TimeoutException([String msg = 'Timeout']) : super._(msg);
}
/// An exception for End to End Encryption.
class LiveKitE2EEException extends LiveKitException {
LiveKitE2EEException([String msg = 'E2EE error']) : super._(msg);
@override
String toString() => 'E2EE Exception: [$runtimeType] $message';
}
+9 -3
View File
@@ -2,12 +2,10 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:livekit_client/livekit_client.dart';
import 'events.dart';
import 'managers/event.dart';
import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
import 'types/other.dart';
extension DataPacketKindExt on lk_models.DataPacket_Kind {
Reliability toSDKType() => {
@@ -172,6 +170,14 @@ extension WidgetsBindingCompatible on WidgetsBinding {
static WidgetsBinding? get instance => WidgetsBinding.instance;
}
extension EncryptionTypeExt on lk_models.Encryption_Type {
EncryptionType toLkType() => {
lk_models.Encryption_Type.NONE: EncryptionType.kNone,
lk_models.Encryption_Type.GCM: EncryptionType.kGcm,
lk_models.Encryption_Type.CUSTOM: EncryptionType.kCustom,
}[this]!;
}
extension DisconnectReasonExt on lk_models.DisconnectReason {
DisconnectReason toSDKType() => {
lk_models.DisconnectReason.UNKNOWN_REASON: DisconnectReason.unknown,
+9
View File
@@ -1,5 +1,6 @@
import 'constants.dart';
import 'core/room.dart';
import 'e2ee/options.dart';
import 'publication/remote.dart';
import 'track/local/audio.dart';
import 'track/local/video.dart';
@@ -88,6 +89,9 @@ class RoomOptions {
/// Defaults to true.
final bool stopLocalTrackOnUnpublish;
/// Options for end-to-end encryption.
final E2EEOptions? e2eeOptions;
const RoomOptions({
this.defaultCameraCaptureOptions = const CameraCaptureOptions(),
this.defaultScreenShareCaptureOptions = const ScreenShareCaptureOptions(),
@@ -98,6 +102,7 @@ class RoomOptions {
this.adaptiveStream = false,
this.dynacast = false,
this.stopLocalTrackOnUnpublish = true,
this.e2eeOptions,
});
RoomOptions copyWith({
@@ -134,6 +139,9 @@ class RoomOptions {
/// Options used when publishing video.
class VideoPublishOptions {
/// The video codec to use.
final String videoCodec;
/// If provided, this will be used instead of the SDK's suggested encodings.
/// Usually you don't need to provide this.
/// Defaults to null.
@@ -149,6 +157,7 @@ class VideoPublishOptions {
final List<VideoParameters> screenShareSimulcastLayers;
const VideoPublishOptions({
this.videoCodec = 'H264',
this.videoEncoding,
this.simulcast = true,
this.videoSimulcastLayers = const [],
+35
View File
@@ -12,6 +12,7 @@ import '../options.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
import '../publication/local.dart';
import '../support/platform.dart';
import '../track/local/audio.dart';
import '../track/local/local.dart';
import '../track/local/video.dart';
@@ -182,6 +183,40 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
init: transceiverInit,
);
if (lkBrowser() != BrowserType.firefox) {
var videoCodec = publishOptions.videoCodec.toLowerCase();
var caps = await rtc.getRtpSenderCapabilities('video');
List<rtc.RTCRtpCodecCapability> matched = [];
List<rtc.RTCRtpCodecCapability> partialMatched = [];
List<rtc.RTCRtpCodecCapability> unmatched = [];
for (var c in caps.codecs!) {
var codec = c.mimeType.toLowerCase();
if (codec == 'audio/opus') {
matched.add(c);
continue;
}
var matchesVideoCodec = codec == 'video/$videoCodec';
if (!matchesVideoCodec) {
unmatched.add(c);
continue;
}
if (publishOptions.videoCodec == 'h264') {
if (c.sdpFmtpLine != null &&
c.sdpFmtpLine!.contains('profile-level-id=42e01f')) {
matched.add(c);
} else {
partialMatched.add(c);
}
continue;
}
matched.add(c);
}
matched.addAll([...partialMatched, ...unmatched]);
await track.transceiver?.setCodecPreferences(matched);
track.codec = videoCodec;
}
// prefer to maintainResolution for screen share
if (track.source == TrackSource.screenShareVideo) {
var sender = track.transceiver!.sender;
+11 -3
View File
@@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import '../core/room.dart';
import '../e2ee/options.dart';
import '../events.dart';
import '../extensions.dart';
import '../logger.dart';
@@ -9,12 +10,9 @@ import '../managers/event.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../publication/track_publication.dart';
import '../support/disposable.dart';
import '../track/local/local.dart';
import '../track/track.dart';
import '../types/other.dart';
import '../types/participant_permissions.dart';
import 'local.dart';
import 'remote.dart';
/// Represents a Participant in the room, notifies changes via delegates as
/// well as ChangeNotifier/providers.
@@ -94,6 +92,16 @@ abstract class Participant<T extends TrackPublication>
// Must be implemented by child class.
List<T> get audioTracks;
EncryptionType get firstTrackEncryptionType {
if (hasAudio) {
return audioTracks.first.encryptionType;
} else if (hasVideo) {
return videoTracks.first.encryptionType;
} else {
return EncryptionType.kNone;
}
}
@internal
bool get hasInfo => _participantInfo != null;
+6 -7
View File
@@ -1,17 +1,11 @@
import 'package:livekit_client/livekit_client.dart';
import 'package:meta/meta.dart';
import '../core/signal_client.dart';
import '../events.dart';
import '../extensions.dart';
import '../internal/events.dart';
import '../logger.dart';
import '../participant/participant.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../support/disposable.dart';
import '../track/local/local.dart';
import '../track/track.dart';
import '../types/other.dart';
import '../types/video_dimensions.dart';
/// Represents a track that's published to the server. This class contains
/// metadata associated with tracks.
@@ -49,6 +43,11 @@ abstract class TrackPublication<T extends Track> extends Disposable {
bool get subscribed => track != null;
EncryptionType get encryptionType {
if (latestInfo == null) return EncryptionType.kNone;
return latestInfo!.encryption.toLkType();
}
@internal
lk_models.TrackInfo? latestInfo;
+2 -1
View File
@@ -1,5 +1,4 @@
import 'dart:io';
import 'platform/io.dart' if (dart.library.html) 'platform/web.dart';
// Returns the current platform which works for both web and devices.
@@ -7,6 +6,8 @@ PlatformType lkPlatform() => lkPlatformImplementation();
bool lkPlatformIs(PlatformType type) => lkPlatform() == type;
bool lkPlatformSupportsE2EE() => lkE2EESupportedImplementation();
bool lkPlatformIsTest() => Platform.environment.containsKey('FLUTTER_TEST');
BrowserType lkBrowser() => lkBrowserImplementation();
+10
View File
@@ -12,6 +12,16 @@ PlatformType lkPlatformImplementation() {
throw UnsupportedError('Unknown Platform');
}
bool lkE2EESupportedImplementation() {
return [
PlatformType.windows,
PlatformType.linux,
PlatformType.macOS,
PlatformType.iOS,
PlatformType.android,
].contains(lkPlatformImplementation());
}
BrowserType lkBrowserImplementation() {
return BrowserType.unknown;
}
+14
View File
@@ -1,9 +1,23 @@
import '../platform.dart';
import 'dart:js' as js;
import 'package:platform_detect/platform_detect.dart';
PlatformType lkPlatformImplementation() => PlatformType.web;
bool lkE2EESupportedImplementation() {
return isInsertableStreamSupported() || isScriptTransformSupported();
}
bool isScriptTransformSupported() {
return js.context['RTCRtpScriptTransform'] != null;
}
bool isInsertableStreamSupported() {
return js.context['RTCRtpSender'] != null &&
js.context['RTCRtpSender']['prototype']['createEncodedStreams'] != null;
}
BrowserType lkBrowserImplementation() {
if (browser.isChrome) return BrowserType.chrome;
if (browser.isFirefox) return BrowserType.firefox;
+2
View File
@@ -50,6 +50,8 @@ abstract class LocalTrack extends Track {
bool _published = false;
bool get isPublished => _published;
String? codec;
LocalTrack(
String name,
lk_models.TrackType kind,
+7
View File
@@ -92,11 +92,13 @@ class RTCConfiguration {
final int? iceCandidatePoolSize;
final List<RTCIceServer>? iceServers;
final RTCIceTransportPolicy? iceTransportPolicy;
final bool? encodedInsertableStreams;
const RTCConfiguration({
this.iceCandidatePoolSize,
this.iceServers,
this.iceTransportPolicy,
this.encodedInsertableStreams,
});
Map<String, dynamic> toMap() {
@@ -108,6 +110,8 @@ class RTCConfiguration {
return <String, dynamic>{
// only supports unified plan
'sdpSemantics': 'unified-plan',
if (encodedInsertableStreams != null)
'encodedInsertableStreams': encodedInsertableStreams,
if (iceServersMap.isNotEmpty) 'iceServers': iceServersMap,
if (iceCandidatePoolSize != null)
'iceCandidatePoolSize': iceCandidatePoolSize,
@@ -121,11 +125,14 @@ class RTCConfiguration {
int? iceCandidatePoolSize,
List<RTCIceServer>? iceServers,
RTCIceTransportPolicy? iceTransportPolicy,
bool? encodedInsertableStreams,
}) =>
RTCConfiguration(
iceCandidatePoolSize: iceCandidatePoolSize ?? this.iceCandidatePoolSize,
iceServers: iceServers ?? this.iceServers,
iceTransportPolicy: iceTransportPolicy ?? this.iceTransportPolicy,
encodedInsertableStreams:
encodedInsertableStreams ?? this.encodedInsertableStreams,
);
}
+2 -2
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'livekit_client'
s.version = '1.1.14'
s.version = '1.2.2'
s.summary = 'Open source platform for real-time audio and video.'
s.description = 'Open source platform for real-time audio and video.'
s.homepage = 'https://livekit.io/'
@@ -16,5 +16,5 @@ Pod::Spec.new do |s|
s.static_framework = true
s.dependency 'FlutterMacOS'
s.dependency 'WebRTC-SDK', '104.5112.12'
s.dependency 'WebRTC-SDK', '104.5112.16'
end
+14 -5
View File
@@ -129,6 +129,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
cryptography:
dependency: "direct main"
description:
name: cryptography
sha256: e0e37f79665cd5c86e8897f9abe1accfe813c0cc5299dab22256e22fddc1fef8
url: "https://pub.dev"
source: hosted
version: "2.0.5"
dart_style:
dependency: transitive
description:
@@ -227,11 +235,12 @@ packages:
flutter_webrtc:
dependency: "direct main"
description:
name: flutter_webrtc
sha256: f8904369f3ad57945a797481bf079c281f4944ad4635e9d412529141d81c9c76
url: "https://pub.dev"
source: hosted
version: "0.9.25"
path: "."
ref: "feat/frame-encryption"
resolved-ref: abb6aba36434fa3a30ce5c56b6c521acb83b1139
url: "https://github.com/flutter-webrtc/flutter-webrtc.git"
source: git
version: "0.9.20"
flutter_window_close:
dependency: "direct main"
description:
+3 -1
View File
@@ -16,6 +16,7 @@ dependencies:
async: ^2.9.0
collection: ^1.16.0
connectivity_plus: ^3.0.2
cryptography: ^2.0.5
fixnum: ^1.0.1
meta: ^1.8.0
http: ^0.13.5
@@ -23,11 +24,12 @@ dependencies:
uuid: ^3.0.6
synchronized: ^3.0.0+3
protobuf: ^2.1.0
flutter_webrtc: 0.9.26
flutter_webrtc: 0.9.27
flutter_window_close: ^0.2.2
device_info_plus: ^8.0.0
webrtc_interface: 1.0.13
dart_webrtc: 1.0.16
js: ^0.6.4
platform_detect: ^2.0.7
dev_dependencies:
+3
View File
@@ -0,0 +1,3 @@
# Upgrade Notes
When the flutter-webrtc version is upgraded ([web/e2ee*.dart](https://github.com/flutter-webrtc/flutter-webrtc/tree/main/web) changes), please directly overwrite these files from the corresponding flutter-webrtc version
+94
View File
@@ -0,0 +1,94 @@
import 'dart:async';
import 'dart:typed_data';
import 'dart:js_util' as jsutil;
import 'dart:html' as html;
import 'package:js/js.dart';
@JS('Promise')
class Promise<T> {
external factory Promise._();
}
@JS('Algorithm')
class Algorithm {
external String get name;
}
@JS('crypto.subtle.encrypt')
external Promise<ByteBuffer> encrypt(
dynamic algorithm,
html.CryptoKey key,
ByteBuffer data,
);
@JS('crypto.subtle.decrypt')
external Promise<ByteBuffer> decrypt(
dynamic algorithm,
html.CryptoKey key,
ByteBuffer data,
);
@JS()
@anonymous
class AesGcmParams {
external factory AesGcmParams({
required String name,
required ByteBuffer iv,
ByteBuffer? additionalData,
int tagLength = 128,
});
}
ByteBuffer jsArrayBufferFrom(List<int> data) {
// Avoid copying if possible
if (data is Uint8List &&
data.offsetInBytes == 0 &&
data.lengthInBytes == data.buffer.lengthInBytes) {
return data.buffer;
}
// Copy
return Uint8List.fromList(data).buffer;
}
@JS('crypto.subtle.importKey')
external Promise<html.CryptoKey> importKey(
String format,
ByteBuffer keyData,
dynamic algorithm,
bool extractable,
List<String> keyUsages,
);
@JS('crypto.subtle.exportKey')
external Promise<ByteBuffer> exportKey(
String format,
html.CryptoKey key,
);
@JS('crypto.subtle.deriveKey')
external Promise<html.CryptoKey> deriveKey(
dynamic algorithm,
html.CryptoKey baseKey,
dynamic derivedKeyAlgorithm,
bool extractable,
List<String> keyUsages);
@JS('crypto.subtle.deriveBits')
external Promise<ByteBuffer> deriveBits(
dynamic algorithm,
html.CryptoKey baseKey,
int length,
);
Future<html.CryptoKey> impportKeyFromRawData(List<int> secretKeyData,
{required String webCryptoAlgorithm,
required List<String> keyUsages}) async {
return jsutil.promiseToFuture<html.CryptoKey>(importKey(
'raw',
jsArrayBufferFrom(secretKeyData),
jsutil.jsify({'name': webCryptoAlgorithm}),
false,
keyUsages,
));
}
+643
View File
@@ -0,0 +1,643 @@
import 'dart:html';
import 'dart:js';
import 'dart:js_util' as jsutil;
import 'dart:math';
import 'dart:typed_data';
import 'dart:collection';
import 'dart:async';
import 'package:flutter_webrtc/src/web/rtc_transform_stream.dart';
import 'crypto.dart' as crypto;
import 'e2ee.utils.dart';
class KeyOptions {
KeyOptions({
required this.sharedKey,
required this.ratchetSalt,
required this.ratchetWindowSize,
this.uncryptedMagicBytes,
});
bool sharedKey;
Uint8List ratchetSalt;
int ratchetWindowSize;
Uint8List? uncryptedMagicBytes;
@override
String toString() {
return 'KeyOptions{sharedKey: $sharedKey, ratchetWindowSize: $ratchetWindowSize}';
}
}
const IV_LENGTH = 12;
const kNaluTypeMask = 0x1f;
/// Coded slice of a non-IDR picture
const SLICE_NON_IDR = 1;
/// Coded slice data partition A
const SLICE_PARTITION_A = 2;
/// Coded slice data partition B
const SLICE_PARTITION_B = 3;
/// Coded slice data partition C
const SLICE_PARTITION_C = 4;
/// Coded slice of an IDR picture
const SLICE_IDR = 5;
/// Supplemental enhancement information
const SEI = 6;
/// Sequence parameter set
const SPS = 7;
/// Picture parameter set
const PPS = 8;
/// Access unit delimiter
const AUD = 9;
/// End of sequence
const END_SEQ = 10;
/// End of stream
const END_STREAM = 11;
/// Filler data
const FILLER_DATA = 12;
/// Sequence parameter set extension
const SPS_EXT = 13;
/// Prefix NAL unit
const PREFIX_NALU = 14;
/// Subset sequence parameter set
const SUBSET_SPS = 15;
/// Depth parameter set
const DPS = 16;
// 17, 18 reserved
/// Coded slice of an auxiliary coded picture without partitioning
const SLICE_AUX = 19;
/// Coded slice extension
const SLICE_EXT = 20;
/// Coded slice extension for a depth view component or a 3D-AVC texture view component
const SLICE_LAYER_EXT = 21;
// 22, 23 reserved
List<int> findNALUIndices(Uint8List stream) {
var result = <int>[];
var start = 0, pos = 0, searchLength = stream.length - 2;
while (pos < searchLength) {
// skip until end of current NALU
while (pos < searchLength &&
!(stream[pos] == 0 && stream[pos + 1] == 0 && stream[pos + 2] == 1)) {
pos++;
}
if (pos >= searchLength) pos = stream.length;
// remove trailing zeros from current NALU
var end = pos;
while (end > start && stream[end - 1] == 0) {
end--;
}
// save current NALU
if (start == 0) {
if (end != start) throw Exception('byte stream contains leading data');
} else {
result.add(start);
}
// begin new NALU
start = pos = pos + 3;
}
return result;
}
int parseNALUType(int startByte) {
return startByte & kNaluTypeMask;
}
enum CryptorError {
kNew,
kOk,
kDecryptError,
kEncryptError,
kUnsupportedCodec,
kMissingKey,
kKeyRatcheted,
kInternalError,
kDisposed,
}
const KEYRING_SIZE = 16;
class KeySet {
KeySet(this.material, this.encryptionKey);
CryptoKey material;
CryptoKey encryptionKey;
}
class FrameCryptor {
FrameCryptor(
{required this.worker,
required this.participantId,
required this.trackId,
required this.keyOptions});
Map<int, int> sendCounts = {};
String? participantId;
String? trackId;
String? codec;
final KeyOptions keyOptions;
late String kind;
bool enabled = false;
CryptorError lastError = CryptorError.kNew;
final DedicatedWorkerGlobalScope worker;
int currentKeyIndex = 0;
Completer? _ratchetCompleter;
List<KeySet?> cryptoKeyRing = List.filled(KEYRING_SIZE, null);
Future<void> ratchetKey(int? keyIndex) async {
if (_ratchetCompleter == null) {
_ratchetCompleter = Completer<void>();
var currentMaterial = getKeySet(keyIndex)?.material;
if (currentMaterial == null) {
_ratchetCompleter!.complete();
_ratchetCompleter = null;
return;
}
ratchetMaterial(currentMaterial).then((newMaterial) {
deriveKeys(newMaterial, keyOptions.ratchetSalt).then((newKeySet) {
setKeySetFromMaterial(newKeySet, keyIndex ?? currentKeyIndex)
.then((_) {
_ratchetCompleter!.complete();
_ratchetCompleter = null;
});
});
});
}
return _ratchetCompleter!.future;
}
Future<CryptoKey> ratchetMaterial(CryptoKey currentMaterial) async {
var newMaterial = await jsutil.promiseToFuture(crypto.importKey(
'raw',
crypto.jsArrayBufferFrom(
await ratchet(currentMaterial, keyOptions.ratchetSalt)),
(currentMaterial.algorithm as crypto.Algorithm).name,
false,
['deriveBits', 'deriveKey'],
));
return newMaterial;
}
KeySet? getKeySet(int? keyIndex) {
return cryptoKeyRing[keyIndex ?? currentKeyIndex];
}
void setParticipantId(String participantId) {
if (lastError != CryptorError.kOk) {
print(
'setParticipantId: lastError != CryptorError.kOk, reset state to kNew');
lastError = CryptorError.kNew;
}
this.participantId = participantId;
}
void setKeyIndex(int keyIndex) {
if (lastError != CryptorError.kOk) {
print('setKeyIndex: lastError != CryptorError.kOk, reset state to kNew');
lastError = CryptorError.kNew;
}
currentKeyIndex = keyIndex;
}
void setEnabled(bool enabled) {
if (lastError != CryptorError.kOk) {
print(
'setEnabled[$enabled]: lastError != CryptorError.kOk, reset state to kNew');
lastError = CryptorError.kNew;
}
this.enabled = enabled;
}
Future<void> setKey(int keyIndex, Uint8List key) async {
if (lastError != CryptorError.kOk) {
print('setKey: lastError != CryptorError.kOk, reset state to kNew');
lastError = CryptorError.kNew;
}
var keyMaterial = await crypto.impportKeyFromRawData(key,
webCryptoAlgorithm: 'PBKDF2', keyUsages: ['deriveBits', 'deriveKey']);
var keySet = await deriveKeys(
keyMaterial,
keyOptions.ratchetSalt,
);
await setKeySetFromMaterial(keySet, keyIndex);
}
Future<void> setKeySetFromMaterial(KeySet keySet, int keyIndex) async {
print('setting new key');
if (keyIndex >= 0) {
currentKeyIndex = keyIndex % cryptoKeyRing.length;
}
cryptoKeyRing[currentKeyIndex] = keySet;
}
/// Derives a set of keys from the master key.
/// See https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.1
Future<KeySet> deriveKeys(CryptoKey material, Uint8List salt) async {
var algorithmOptions =
getAlgoOptions((material.algorithm as crypto.Algorithm).name, salt);
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#HKDF
// https://developer.mozilla.org/en-US/docs/Web/API/HkdfParams
var encryptionKey =
await jsutil.promiseToFuture<CryptoKey>(crypto.deriveKey(
jsutil.jsify(algorithmOptions),
material,
jsutil.jsify({'name': 'AES-GCM', 'length': 128}),
false,
['encrypt', 'decrypt'],
));
return KeySet(material, encryptionKey);
}
/// Ratchets a key. See
/// https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
Future<Uint8List> ratchet(CryptoKey material, Uint8List salt) async {
var algorithmOptions = getAlgoOptions('PBKDF2', salt);
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveBits
var newKey = await jsutil.promiseToFuture<ByteBuffer>(
crypto.deriveBits(jsutil.jsify(algorithmOptions), material, 256));
return newKey.asUint8List();
}
void updateCodec(String codec) {
if (lastError != CryptorError.kOk) {
print(
'updateCodec[$codec]: lastError != CryptorError.kOk, reset state to kNew');
lastError = CryptorError.kNew;
}
this.codec = codec;
}
Uint8List makeIv(
{required int synchronizationSource, required int timestamp}) {
var iv = ByteData(IV_LENGTH);
// having to keep our own send count (similar to a picture id) is not ideal.
if (sendCounts[synchronizationSource] == null) {
// Initialize with a random offset, similar to the RTP sequence number.
sendCounts[synchronizationSource] = Random.secure().nextInt(0xffff);
}
var sendCount = sendCounts[synchronizationSource] ?? 0;
iv.setUint32(0, synchronizationSource);
iv.setUint32(4, timestamp);
iv.setUint32(8, timestamp - (sendCount % 0xffff));
sendCounts[synchronizationSource] = sendCount + 1;
return iv.buffer.asUint8List();
}
void postMessage(Object message) {
worker.postMessage(message);
}
Future<void> setupTransform({
required String operation,
required ReadableStream readable,
required WritableStream writable,
required String trackId,
required String kind,
String? codec,
}) async {
print('setupTransform $operation');
this.kind = kind;
if (codec != null) {
print('setting codec on cryptor to $codec');
this.codec = codec;
}
var transformer = TransformStream(jsutil.jsify({
'transform':
allowInterop(operation == 'encode' ? encodeFunction : decodeFunction)
}));
try {
readable.pipeThrough(transformer).pipeTo(writable);
} catch (e) {
print('e ${e.toString()}');
if (lastError != CryptorError.kInternalError) {
lastError = CryptorError.kInternalError;
postMessage({
'type': 'cryptorState',
'participantId': participantId,
'state': 'internalError',
'error': 'Internal error: ${e.toString()}'
});
}
}
this.trackId = trackId;
}
int getUnencryptedBytes(RTCEncodedFrame frame, String? codec) {
if (codec != null && codec.toLowerCase() == 'h264') {
var data = frame.data.asUint8List();
var naluIndices = findNALUIndices(data);
for (var index in naluIndices) {
var type = parseNALUType(data[index]);
switch (type) {
case SLICE_IDR:
case SLICE_NON_IDR:
// skipping
//print('unEncryptedBytes NALU of type $type, offset ${index + 2}');
return index + 2;
default:
//print('skipping NALU of type $type');
break;
}
}
throw Exception('Could not find NALU');
}
switch (frame.type) {
case 'key':
return 10;
case 'delta':
return 3;
case 'audio':
return 1; // frame.type is not set on audio, so this is set manually
default:
return 0;
}
}
Future<void> encodeFunction(
RTCEncodedFrame frame,
TransformStreamDefaultController controller,
) async {
var buffer = frame.data.asUint8List();
if (!enabled ||
// skip for encryption for empty dtx frames
buffer.isEmpty) {
controller.enqueue(frame);
return;
}
var secretKey = getKeySet(currentKeyIndex)?.encryptionKey;
var keyIndex = currentKeyIndex;
if (secretKey == null) {
if (lastError != CryptorError.kMissingKey) {
lastError = CryptorError.kMissingKey;
postMessage({
'type': 'cryptorState',
'participantId': participantId,
'trackId': trackId,
'kind': kind,
'state': 'missingKey',
'error': 'Missing key for track $trackId',
});
}
return;
}
try {
var headerLength =
kind == 'video' ? getUnencryptedBytes(frame, codec) : 1;
var metaData = frame.getMetadata();
var iv = makeIv(
synchronizationSource: metaData.synchronizationSource,
timestamp: frame.timestamp);
var frameTrailer = ByteData(2);
frameTrailer.setInt8(0, IV_LENGTH);
frameTrailer.setInt8(1, keyIndex);
var cipherText = await jsutil.promiseToFuture<ByteBuffer>(crypto.encrypt(
crypto.AesGcmParams(
name: 'AES-GCM',
iv: crypto.jsArrayBufferFrom(iv),
additionalData:
crypto.jsArrayBufferFrom(buffer.sublist(0, headerLength)),
),
secretKey,
crypto.jsArrayBufferFrom(buffer.sublist(headerLength, buffer.length)),
));
//print(
// 'buffer: ${buffer.length}, cipherText: ${cipherText.asUint8List().length}');
var finalBuffer = BytesBuilder();
finalBuffer.add(Uint8List.fromList(buffer.sublist(0, headerLength)));
finalBuffer.add(cipherText.asUint8List());
finalBuffer.add(iv);
finalBuffer.add(frameTrailer.buffer.asUint8List());
frame.data = crypto.jsArrayBufferFrom(finalBuffer.toBytes());
controller.enqueue(frame);
if (lastError != CryptorError.kOk) {
lastError = CryptorError.kOk;
postMessage({
'type': 'cryptorState',
'participantId': participantId,
'trackId': trackId,
'kind': kind,
'state': 'ok',
'error': 'encryption ok'
});
}
//print(
// 'encrypto kind $kind,codec $codec headerLength: $headerLength, timestamp: ${frame.timestamp}, ssrc: ${metaData.synchronizationSource}, data length: ${buffer.length}, encrypted length: ${finalBuffer.toBytes().length}, key ${secretKey.toString()} , iv $iv');
} catch (e) {
//print('encrypt: e ${e.toString()}');
if (lastError != CryptorError.kEncryptError) {
lastError = CryptorError.kEncryptError;
postMessage({
'type': 'cryptorState',
'participantId': participantId,
'trackId': trackId,
'kind': kind,
'state': 'encryptError',
'error': e.toString()
});
}
}
}
Future<void> decodeFunction(
RTCEncodedFrame frame,
TransformStreamDefaultController controller,
) async {
var ratchetCount = 0;
var buffer = frame.data.asUint8List();
ByteBuffer? decrypted;
KeySet? initialKeySet;
int initialKeyIndex = currentKeyIndex;
if (!enabled ||
// skip for encryption for empty dtx frames
buffer.isEmpty) {
controller.enqueue(frame);
return;
}
if (keyOptions.uncryptedMagicBytes != null) {
var magicBytes = keyOptions.uncryptedMagicBytes!;
if (buffer.length >= magicBytes.length + 1) {
var magicBytesBuffer = buffer.sublist(
buffer.length - (magicBytes.length + 1), magicBytes.length);
if (magicBytesBuffer.toString() == magicBytes.toString()) {
var finalBuffer = BytesBuilder();
finalBuffer.add(Uint8List.fromList(
buffer.sublist(0, buffer.length - (magicBytes.length + 1))));
frame.data = crypto.jsArrayBufferFrom(finalBuffer.toBytes());
controller.enqueue(frame);
return;
}
}
}
try {
var headerLength =
kind == 'video' ? getUnencryptedBytes(frame, codec) : 1;
var metaData = frame.getMetadata();
var frameTrailer = buffer.sublist(buffer.length - 2);
var ivLength = frameTrailer[0];
var keyIndex = frameTrailer[1];
var iv = buffer.sublist(buffer.length - ivLength - 2, buffer.length - 2);
var initialKeySet = getKeySet(keyIndex);
initialKeyIndex = keyIndex;
if (initialKeySet == null) {
if (lastError != CryptorError.kMissingKey) {
lastError = CryptorError.kMissingKey;
postMessage({
'type': 'cryptorState',
'participantId': participantId,
'trackId': trackId,
'kind': kind,
'state': 'missingKey',
'error': 'Missing key for track $trackId'
});
}
controller.enqueue(frame);
return;
}
bool endDecLoop = false;
var currentkeySet = initialKeySet;
while (!endDecLoop) {
try {
decrypted = await jsutil.promiseToFuture<ByteBuffer>(crypto.decrypt(
crypto.AesGcmParams(
name: 'AES-GCM',
iv: crypto.jsArrayBufferFrom(iv),
additionalData:
crypto.jsArrayBufferFrom(buffer.sublist(0, headerLength)),
),
currentkeySet.encryptionKey,
crypto.jsArrayBufferFrom(
buffer.sublist(headerLength, buffer.length - ivLength - 2)),
));
if (decrypted != null && currentkeySet != initialKeySet) {
await setKeySetFromMaterial(currentkeySet, initialKeyIndex);
}
endDecLoop = true;
if (lastError != CryptorError.kOk &&
lastError != CryptorError.kKeyRatcheted &&
ratchetCount > 0) {
print(
'KeyRatcheted: ssrc ${metaData.synchronizationSource} timestamp ${frame.timestamp} ratchetCount $ratchetCount participantId: $participantId');
print(
'ratchetKey: lastError != CryptorError.kKeyRatcheted, reset state to kKeyRatcheted');
lastError = CryptorError.kKeyRatcheted;
postMessage({
'type': 'cryptorState',
'participantId': participantId,
'trackId': trackId,
'kind': kind,
'state': 'keyRatcheted',
'error': 'Key ratcheted ok'
});
}
} catch (e) {
lastError = CryptorError.kInternalError;
endDecLoop = ratchetCount >= keyOptions.ratchetWindowSize ||
keyOptions.ratchetWindowSize <= 0;
if (endDecLoop) {
rethrow;
}
var newMaterial = await ratchetMaterial(currentkeySet.material);
currentkeySet = await deriveKeys(newMaterial, keyOptions.ratchetSalt);
ratchetCount++;
}
}
//print(
// 'buffer: ${buffer.length}, decrypted: ${decrypted.asUint8List().length}');
var finalBuffer = BytesBuilder();
finalBuffer.add(Uint8List.fromList(buffer.sublist(0, headerLength)));
finalBuffer.add(decrypted!.asUint8List());
frame.data = crypto.jsArrayBufferFrom(finalBuffer.toBytes());
controller.enqueue(frame);
if (lastError != CryptorError.kOk) {
lastError = CryptorError.kOk;
postMessage({
'type': 'cryptorState',
'participantId': participantId,
'trackId': trackId,
'kind': kind,
'state': 'ok',
'error': 'decryption ok'
});
}
//print(
// 'decrypto kind $kind,codec $codec headerLength: $headerLength, timestamp: ${frame.timestamp}, ssrc: ${metaData.synchronizationSource}, data length: ${buffer.length}, decrypted length: ${finalBuffer.toBytes().length}, key ${secretKey.toString()}, keyindex $keyIndex iv $iv');
} catch (e) {
if (lastError != CryptorError.kDecryptError) {
lastError = CryptorError.kDecryptError;
postMessage({
'type': 'cryptorState',
'participantId': participantId,
'trackId': trackId,
'kind': kind,
'state': 'decryptError',
'error': e.toString()
});
}
/// Since the key it is first send and only afterwards actually used for encrypting, there were
/// situations when the decrypting failed due to the fact that the received frame was not encrypted
/// yet and ratcheting, of course, did not solve the problem. So if we fail RATCHET_WINDOW_SIZE times,
/// we come back to the initial key.
if (initialKeySet != null) {
await setKeySetFromMaterial(initialKeySet, initialKeyIndex);
}
}
}
}
+66
View File
@@ -0,0 +1,66 @@
import 'dart:html';
import 'dart:js' as js;
import 'dart:js_util';
import 'dart:typed_data';
import 'crypto.dart' as crypto;
bool isE2EESupported() {
return isInsertableStreamSupported() || isScriptTransformSupported();
}
bool isScriptTransformSupported() {
return js.context['RTCRtpScriptTransform'] != null;
}
bool isInsertableStreamSupported() {
return js.context['RTCRtpSender'] != null &&
js.context['RTCRtpSender']['prototype']['createEncodedStreams'] != null;
}
Future<CryptoKey> importKey(
Uint8List keyBytes, String algorithm, String usage) {
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
return promiseToFuture<CryptoKey>(crypto.importKey(
'raw',
crypto.jsArrayBufferFrom(keyBytes),
js.JsObject.jsify({'name': algorithm}),
false,
usage == 'derive' ? ['deriveBits', 'deriveKey'] : ['encrypt', 'decrypt'],
));
}
Future<CryptoKey> createKeyMaterialFromString(
Uint8List keyBytes, String algorithm, String usage) {
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
return promiseToFuture<CryptoKey>(crypto.importKey(
'raw',
crypto.jsArrayBufferFrom(keyBytes),
js.JsObject.jsify({'name': 'PBKDF2'}),
false,
['deriveBits', 'deriveKey'],
));
}
dynamic getAlgoOptions(String algorithmName, Uint8List salt) {
switch (algorithmName) {
case 'HKDF':
return {
'name': 'HKDF',
'salt': crypto.jsArrayBufferFrom(salt),
'hash': 'SHA-256',
'info': crypto.jsArrayBufferFrom(Uint8List(128)),
};
case 'PBKDF2':
{
return {
'name': 'PBKDF2',
'salt': crypto.jsArrayBufferFrom(salt),
'hash': 'SHA-256',
'iterations': 100000,
};
}
default:
throw Exception('algorithm $algorithmName is currently unsupported');
}
}
+281
View File
@@ -0,0 +1,281 @@
import 'dart:convert';
import 'dart:html' as html;
import 'dart:js_util' as js_util;
import 'dart:typed_data';
import 'package:js/js.dart';
import 'e2ee.cryptor.dart';
import 'package:flutter_webrtc/src/web/rtc_transform_stream.dart';
import 'package:collection/collection.dart';
import 'crypto.dart' as crypto;
@JS()
abstract class TransformMessage {
external String get msgType;
external String get kind;
}
@anonymous
@JS()
class EnableTransformMessage {
external factory EnableTransformMessage({
ReadableStream readable,
WritableStream writable,
String msgType,
String kind,
String participantId,
String trackId,
String codec,
});
external ReadableStream get readable;
external WritableStream get writable;
external String get msgType; // 'encode' or 'decode'
external String get participantId;
external String get trackId;
external String get kind;
external String get codec;
}
@anonymous
@JS()
class RemoveTransformMessage {
external factory RemoveTransformMessage(
{String msgType, String participantId, String trackId});
external String get msgType; // 'removeTransform'
external String get participantId;
external String get trackId;
}
@JS('self')
external html.DedicatedWorkerGlobalScope get self;
extension PropsRTCTransformEventHandler on html.DedicatedWorkerGlobalScope {
set onrtctransform(Function(dynamic) callback) =>
js_util.setProperty<Function>(this, 'onrtctransform', callback);
}
var participantCryptors = <FrameCryptor>[];
var publisherKeys = <String, html.CryptoKey>{};
bool isEncryptionEnabled = false;
KeyOptions keyProviderOptions = KeyOptions(
sharedKey: true,
ratchetSalt: Uint8List.fromList('ratchetSalt'.codeUnits),
ratchetWindowSize: 16);
void main() async {
print('E2EE Worker created');
if (js_util.getProperty(self, 'RTCTransformEvent') != null) {
print('setup transform event handler');
self.onrtctransform = allowInterop((event) {
print('got transform event');
var transformer = (event as RTCTransformEvent).transformer;
transformer.handled = true;
var options = transformer.options;
var kind = options.kind;
var participantId = options.participantId;
var trackId = options.trackId;
var codec = options.codec;
var msgType = options.msgType;
var cryptor =
participantCryptors.firstWhereOrNull((c) => c.trackId == trackId);
if (cryptor == null) {
cryptor = FrameCryptor(
worker: self,
participantId: participantId,
trackId: trackId,
keyOptions: keyProviderOptions,
);
participantCryptors.add(cryptor);
}
cryptor.setupTransform(
operation: msgType,
readable: transformer.readable,
writable: transformer.writable,
trackId: trackId,
kind: kind,
codec: codec);
});
}
self.onMessage.listen((e) {
var msg = e.data;
var msgType = msg['msgType'];
switch (msgType) {
case 'init':
var options = msg['keyOptions'];
keyProviderOptions = KeyOptions(
sharedKey: options['sharedKey'],
ratchetSalt: Uint8List.fromList(
base64Decode(options['ratchetSalt'] as String)),
ratchetWindowSize: options['ratchetWindowSize'],
uncryptedMagicBytes: options['ratchetSalt'] != null
? Uint8List.fromList(
base64Decode(options['uncryptedMagicBytes'] as String))
: null);
print('worker: init with keyOptions ${keyProviderOptions.toString()}');
break;
case 'enable':
{
var enabled = msg['enabled'] as bool;
var participantId = msg['participantId'] as String;
print('worker: set enable $enabled for participantId $participantId');
var cryptors = participantCryptors
.where((c) => c.participantId == participantId)
.toList();
for (var cryptor in cryptors) {
cryptor.setEnabled(enabled);
}
self.postMessage({
'type': 'cryptorEnabled',
'participantId': participantId,
'enable': enabled,
});
}
break;
case 'decode':
case 'encode':
{
var kind = msg['kind'];
var exist = msg['exist'] as bool;
var participantId = msg['participantId'] as String;
var trackId = msg['trackId'];
var readable = msg['readableStream'] as ReadableStream;
var writable = msg['writableStream'] as WritableStream;
print(
'worker: got $msgType, kind $kind, trackId $trackId, participantId $participantId, ${readable.runtimeType} ${writable.runtimeType}}');
var cryptor =
participantCryptors.firstWhereOrNull((c) => c.trackId == trackId);
if (cryptor == null) {
cryptor = FrameCryptor(
worker: self,
participantId: participantId,
trackId: trackId,
keyOptions: keyProviderOptions);
participantCryptors.add(cryptor);
}
if (!exist) {
cryptor.setupTransform(
operation: msgType,
readable: readable,
writable: writable,
trackId: trackId,
kind: kind);
}
cryptor.setParticipantId(participantId);
self.postMessage({
'type': 'cryptorSetup',
'participantId': participantId,
'trackId': trackId,
'exist': exist,
'operation': msgType,
});
cryptor.lastError = CryptorError.kNew;
}
break;
case 'removeTransform':
{
var trackId = msg['trackId'] as String;
print('worker: removing trackId $trackId');
participantCryptors.removeWhere((c) => c.trackId == trackId);
}
break;
case 'setKey':
{
var key = Uint8List.fromList(base64Decode(msg['key'] as String));
var keyIndex = msg['keyIndex'];
//print('worker: got setKey ${msg['key']}, key $key');
var participantId = msg['participantId'] as String;
print('worker: setup key for participant $participantId');
if (keyProviderOptions.sharedKey) {
for (var c in participantCryptors) {
c.setKey(keyIndex, key);
}
return;
}
var cryptors = participantCryptors
.where((c) => c.participantId == participantId)
.toList();
for (var c in cryptors) {
c.setKey(keyIndex, key);
}
}
break;
case 'ratchetKey':
{
var keyIndex = msg['keyIndex'];
var participantId = msg['participantId'] as String;
print(
'worker: ratchetKey for participant $participantId, keyIndex $keyIndex');
var cryptors = participantCryptors
.where((c) => c.participantId == participantId)
.toList();
for (var c in cryptors) {
var keySet = c.getKeySet(keyIndex);
c.ratchetKey(keyIndex).then((_) async {
var newKey = await c.ratchet(
keySet!.material, keyProviderOptions.ratchetSalt);
self.postMessage({
'type': 'ratchetKey',
'participantId': participantId,
'trackId': c.trackId,
'key': base64Encode(newKey),
});
});
}
}
break;
case 'setKeyIndex':
{
var keyIndex = msg['index'];
var participantId = msg['participantId'] as String;
print('worker: setup key index for participant $participantId');
var cryptors = participantCryptors
.where((c) => c.participantId == participantId)
.toList();
for (var c in cryptors) {
c.setKeyIndex(keyIndex);
}
}
break;
case 'updateCodec':
{
var codec = msg['codec'] as String;
var trackId = msg['trackId'] as String;
print('worker: update codec for trackId $trackId, codec $codec');
var cryptor =
participantCryptors.firstWhereOrNull((c) => c.trackId == trackId);
cryptor?.updateCodec(codec);
}
break;
case 'dispose':
{
var trackId = msg['trackId'] as String;
print('worker: dispose trackId $trackId');
var cryptor =
participantCryptors.firstWhereOrNull((c) => c.trackId == trackId);
if (cryptor != null) {
cryptor.lastError = CryptorError.kDisposed;
self.postMessage({
'type': 'cryptorDispose',
'participantId': cryptor.participantId,
'trackId': trackId,
});
}
}
break;
default:
print('worker: unknown message kind $msg');
}
});
}