Skip to content

Commit 2e25499

Browse files
merge
2 parents 7335a11 + 38badf0 commit 2e25499

23 files changed

+360
-184
lines changed

.github/workflows/publish.yaml

+5-5
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ jobs:
1313
- name: Checkout
1414
uses: actions/checkout@v1
1515
- name: Publish
16-
uses: sakebook/actions-flutter-pub-publisher@v1.3.1
16+
uses: k-paxian/dart-package-publisher@v1.5.1
1717
with:
18-
credential: ${{ secrets.CREDENTIAL_JSON }}
19-
flutter_package: true
20-
skip_test: true
21-
dry_run: false
18+
credentialJson: ${{ secrets.CREDENTIAL_JSON }}
19+
flutter: true
20+
skipTests: true
21+
force: true

CHANGELOG.md

+65
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,71 @@
11
# Changelog
22

33
--------------------------------------------
4+
[1.4.8] - 2024-07-12
5+
6+
* fix: missing streamCompleter complete for getUserMedia.
7+
* fix: RTCPeerConnectionWeb.getRemoteStreams.
8+
9+
[1.4.7] - 2024-07-12
10+
11+
* fix: MediaStreamTrack.getSettings.
12+
13+
[1.4.6+hotfix.2] - 2024-06-07
14+
15+
[1.4.6+hotfix.1] - 2024-06-07
16+
17+
* Wider version dependencies for js/http.
18+
19+
[1.4.6] - 2024-06-05
20+
21+
* chore: bump version for js and http.
22+
* fix: decrypting audio when e2ee.
23+
* fix: translate audio constraints for web.
24+
* fix: missing fault tolerance, better worker reports and a increased timeout for worker tasks.
25+
* fix type cast exception in getConstraints()
26+
27+
[1.4.5] - 2024-05-13
28+
29+
* fix: negotiationNeeded listener.
30+
* fix: fix type cast exception in getConstraints().
31+
32+
[1.4.4] - 2024-04-24
33+
34+
* fix: datachannel message parse for Firefox.
35+
* fix: tryCatch editing mediaConstraints #34
36+
37+
[1.4.3] - 2024-04-18
38+
39+
* fix: do not fail if removing constraint fails
40+
41+
[1.4.2] - 2024-04-15
42+
43+
* fix.
44+
45+
[1.4.1] - 2024-04-12
46+
47+
* remove RTCConfiguration convert.
48+
49+
[1.4.0] - 2024-04-09
50+
51+
* Fixed bug for RTCConfiguration convert.
52+
53+
[1.3.3] - 2024-04-09
54+
55+
* Fix DC data parse.
56+
57+
[1.3.2] - 2024-04-09
58+
59+
* Fix error when constructing RTCDataChannelInit.
60+
61+
[1.3.1] - 2024-04-08
62+
63+
* Add keyRingSize/discardFrameWhenCryptorNotReady to KeyProviderOptions.
64+
65+
[1.3.0] - 2024-04-08
66+
67+
* update to package:web by @jezell in #29.
68+
469
[1.2.1] - 2024-02-05
570

671
* Downgrade some dependencies make more compatible.

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ dart compile js ./lib/src/e2ee.worker/e2ee.worker.dart -o web/e2ee.worker.dart.j
1515
## How to develop
1616

1717
* `git clone https://github.com/flutter-webrtc/dart-webrtc && cd dart-webrtc`
18-
* `pub get`
19-
* `pub global activate webdev`
18+
* `dart pub get`
19+
* `dart pub global activate webdev`
2020
* `webdev serve --auto=refresh`

lib/src/e2ee.worker/e2ee.cryptor.dart

+85-60
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import 'dart:js_util' as jsutil;
55
import 'dart:math';
66
import 'dart:typed_data';
77

8-
import 'package:dart_webrtc/src/rtc_transform_stream.dart';
98
import 'package:web/web.dart' as web;
109

10+
import 'package:dart_webrtc/src/rtc_transform_stream.dart';
1111
import 'crypto.dart' as crypto;
1212
import 'e2ee.keyhandler.dart';
1313
import 'e2ee.logger.dart';
@@ -301,6 +301,9 @@ class FrameCryptor {
301301
if (!enabled ||
302302
// skip for encryption for empty dtx frames
303303
buffer.isEmpty) {
304+
if (keyOptions.discardFrameWhenCryptorNotReady) {
305+
return;
306+
}
304307
controller.enqueue(frame);
305308
return;
306309
}
@@ -405,6 +408,8 @@ class FrameCryptor {
405408
// skip for encryption for empty dtx frames
406409
buffer.isEmpty) {
407410
sifGuard.recordUserFrame();
411+
if (keyOptions.discardFrameWhenCryptorNotReady) return;
412+
logger.fine('enqueing empty frame');
408413
controller.enqueue(frame);
409414
return;
410415
}
@@ -415,7 +420,7 @@ class FrameCryptor {
415420
var magicBytesBuffer = buffer.sublist(
416421
buffer.length - magicBytes.length - 1, buffer.length - 1);
417422
logger.finer(
418-
'magicBytesBuffer $magicBytesBuffer, magicBytes $magicBytes, ');
423+
'magicBytesBuffer $magicBytesBuffer, magicBytes $magicBytes');
419424
if (magicBytesBuffer.toString() == magicBytes.toString()) {
420425
sifGuard.recordSif();
421426
if (sifGuard.isSifAllowed()) {
@@ -425,6 +430,7 @@ class FrameCryptor {
425430
finalBuffer.add(Uint8List.fromList(
426431
buffer.sublist(0, buffer.length - (magicBytes.length + 1))));
427432
frame.data = crypto.jsArrayBufferFrom(finalBuffer.toBytes());
433+
logger.fine('enqueing silent frame');
428434
controller.enqueue(frame);
429435
} else {
430436
logger.finer('SIF limit reached, dropping frame');
@@ -449,6 +455,12 @@ class FrameCryptor {
449455
initialKeySet = keyHandler.getKeySet(keyIndex);
450456
initialKeyIndex = keyIndex;
451457

458+
/// missingKey flow:
459+
/// tries to decrypt once, fails, tries to ratchet once and decrypt again,
460+
/// fails (does not save ratcheted key), bumps _decryptionFailureCount,
461+
/// if higher than failuretolerance hasValidKey is set to false, on next
462+
/// frame it fires a missingkey
463+
/// to throw missingkeys faster lower your failureTolerance
452464
if (initialKeySet == null || !keyHandler.hasValidKey) {
453465
if (lastError != CryptorError.kMissingKey) {
454466
lastError = CryptorError.kMissingKey;
@@ -462,14 +474,14 @@ class FrameCryptor {
462474
'error': 'Missing key for track $trackId'
463475
});
464476
}
465-
controller.enqueue(frame);
477+
// controller.enqueue(frame);
466478
return;
467479
}
468-
var endDecLoop = false;
469480
var currentkeySet = initialKeySet;
470-
while (!endDecLoop) {
471-
try {
472-
decrypted = await jsutil.promiseToFuture<ByteBuffer>(crypto.decrypt(
481+
482+
Future<void> decryptFrameInternal() async {
483+
decrypted = await jsutil.promiseToFuture<ByteBuffer>(
484+
crypto.decrypt(
473485
crypto.AesGcmParams(
474486
name: 'AES-GCM',
475487
iv: crypto.jsArrayBufferFrom(iv),
@@ -478,56 +490,78 @@ class FrameCryptor {
478490
),
479491
currentkeySet.encryptionKey,
480492
crypto.jsArrayBufferFrom(
481-
buffer.sublist(headerLength, buffer.length - ivLength - 2)),
482-
));
483-
484-
if (currentkeySet != initialKeySet) {
485-
logger.fine(
486-
'ratchetKey: decryption ok, reset state to kKeyRatcheted');
487-
await keyHandler.setKeySetFromMaterial(
488-
currentkeySet, initialKeyIndex);
489-
}
493+
buffer.sublist(headerLength, buffer.length - ivLength - 2),
494+
),
495+
),
496+
);
497+
if (decrypted == null) {
498+
throw Exception('[decryptFrameInternal] could not decrypt');
499+
}
490500

491-
endDecLoop = true;
501+
if (currentkeySet != initialKeySet) {
502+
logger.fine('ratchetKey: decryption ok, newState: kKeyRatcheted');
503+
await keyHandler.setKeySetFromMaterial(
504+
currentkeySet, initialKeyIndex);
505+
}
492506

493-
if (lastError != CryptorError.kOk &&
494-
lastError != CryptorError.kKeyRatcheted &&
495-
ratchetCount > 0) {
496-
logger.finer(
497-
'KeyRatcheted: ssrc ${metaData.synchronizationSource} timestamp ${frame.timestamp} ratchetCount $ratchetCount participantId: $participantIdentity');
498-
logger.finer(
499-
'ratchetKey: lastError != CryptorError.kKeyRatcheted, reset state to kKeyRatcheted');
500-
501-
lastError = CryptorError.kKeyRatcheted;
502-
postMessage({
503-
'type': 'cryptorState',
504-
'msgType': 'event',
505-
'participantId': participantIdentity,
506-
'trackId': trackId,
507-
'kind': kind,
508-
'state': 'keyRatcheted',
509-
'error': 'Key ratcheted ok'
510-
});
511-
}
512-
} catch (e) {
513-
lastError = CryptorError.kInternalError;
514-
endDecLoop = ratchetCount >= keyOptions.ratchetWindowSize ||
515-
keyOptions.ratchetWindowSize <= 0;
516-
if (endDecLoop) {
517-
rethrow;
518-
}
519-
var newKeyBuffer = crypto.jsArrayBufferFrom(await keyHandler.ratchet(
520-
currentkeySet.material, keyOptions.ratchetSalt));
521-
var newMaterial = await keyHandler.ratchetMaterial(
522-
currentkeySet.material, newKeyBuffer);
523-
currentkeySet =
524-
await keyHandler.deriveKeys(newMaterial, keyOptions.ratchetSalt);
525-
ratchetCount++;
507+
if (lastError != CryptorError.kOk &&
508+
lastError != CryptorError.kKeyRatcheted &&
509+
ratchetCount > 0) {
510+
logger.finer(
511+
'KeyRatcheted: ssrc ${metaData.synchronizationSource} timestamp ${frame.timestamp} ratchetCount $ratchetCount participantId: $participantIdentity');
512+
logger.finer(
513+
'ratchetKey: lastError != CryptorError.kKeyRatcheted, reset state to kKeyRatcheted');
514+
515+
lastError = CryptorError.kKeyRatcheted;
516+
postMessage({
517+
'type': 'cryptorState',
518+
'msgType': 'event',
519+
'participantId': participantIdentity,
520+
'trackId': trackId,
521+
'kind': kind,
522+
'state': 'keyRatcheted',
523+
'error': 'Key ratcheted ok'
524+
});
526525
}
527526
}
528527

528+
Future<void> ratchedKeyInternal() async {
529+
if (ratchetCount >= keyOptions.ratchetWindowSize ||
530+
keyOptions.ratchetWindowSize <= 0) {
531+
throw Exception('[ratchedKeyInternal] cannot ratchet anymore');
532+
}
533+
534+
var newKeyBuffer = crypto.jsArrayBufferFrom(await keyHandler.ratchet(
535+
currentkeySet.material, keyOptions.ratchetSalt));
536+
var newMaterial = await keyHandler.ratchetMaterial(
537+
currentkeySet.material, newKeyBuffer);
538+
currentkeySet =
539+
await keyHandler.deriveKeys(newMaterial, keyOptions.ratchetSalt);
540+
ratchetCount++;
541+
await decryptFrameInternal();
542+
}
543+
544+
try {
545+
/// gets frame -> tries to decrypt -> tries to ratchet (does this failureTolerance
546+
/// times, then says missing key)
547+
/// we only save the new key after ratcheting if we were able to decrypt something
548+
await decryptFrameInternal();
549+
} catch (e) {
550+
lastError = CryptorError.kInternalError;
551+
await ratchedKeyInternal();
552+
}
553+
554+
if (decrypted == null) {
555+
throw Exception(
556+
'[decodeFunction] decryption failed even after ratchting');
557+
}
558+
559+
// we can now be sure that decryption was a success
560+
keyHandler.decryptionSuccess();
561+
529562
logger.finer(
530-
'buffer: ${buffer.length}, decrypted: ${decrypted?.asUint8List().length ?? 0}');
563+
'buffer: ${buffer.length}, decrypted: ${decrypted!.asUint8List().length}');
564+
531565
var finalBuffer = BytesBuilder();
532566

533567
finalBuffer.add(Uint8List.fromList(buffer.sublist(0, headerLength)));
@@ -564,15 +598,6 @@ class FrameCryptor {
564598
});
565599
}
566600

567-
/// Since the key it is first send and only afterwards actually used for encrypting, there were
568-
/// situations when the decrypting failed due to the fact that the received frame was not encrypted
569-
/// yet and ratcheting, of course, did not solve the problem. So if we fail RATCHET_WINDOW_SIZE times,
570-
/// we come back to the initial key.
571-
if (initialKeySet != null) {
572-
logger.warning(
573-
'decryption failed, ratcheting back to initial key, keyIndex: $initialKeyIndex');
574-
await keyHandler.setKeySetFromMaterial(initialKeySet, initialKeyIndex);
575-
}
576601
keyHandler.decryptionFailure();
577602
}
578603
}

lib/src/e2ee.worker/e2ee.keyhandler.dart

+13-4
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,25 @@ import 'crypto.dart' as crypto;
88
import 'e2ee.logger.dart';
99
import 'e2ee.utils.dart';
1010

11+
const KEYRING_SIZE = 16;
12+
1113
class KeyOptions {
1214
KeyOptions({
1315
required this.sharedKey,
1416
required this.ratchetSalt,
1517
required this.ratchetWindowSize,
1618
this.uncryptedMagicBytes,
1719
this.failureTolerance = -1,
20+
this.keyRingSze = KEYRING_SIZE,
21+
this.discardFrameWhenCryptorNotReady = false,
1822
});
1923
bool sharedKey;
2024
Uint8List ratchetSalt;
2125
int ratchetWindowSize = 0;
2226
int failureTolerance;
2327
Uint8List? uncryptedMagicBytes;
28+
int keyRingSze;
29+
bool discardFrameWhenCryptorNotReady;
2430

2531
@override
2632
String toString() {
@@ -77,8 +83,6 @@ class KeyProvider {
7783
}
7884
}
7985

80-
const KEYRING_SIZE = 16;
81-
8286
class KeySet {
8387
KeySet(this.material, this.encryptionKey);
8488
web.CryptoKey material;
@@ -90,10 +94,15 @@ class ParticipantKeyHandler {
9094
required this.worker,
9195
required this.keyOptions,
9296
required this.participantIdentity,
93-
});
97+
}) {
98+
if (keyOptions.keyRingSze <= 0 || keyOptions.keyRingSze > 255) {
99+
throw Exception('Invalid key ring size');
100+
}
101+
cryptoKeyRing = List.filled(keyOptions.keyRingSze, null);
102+
}
94103
int currentKeyIndex = 0;
95104

96-
List<KeySet?> cryptoKeyRing = List.filled(KEYRING_SIZE, null);
105+
late List<KeySet?> cryptoKeyRing;
97106

98107
bool _hasValidKey = false;
99108

lib/src/e2ee.worker/e2ee.worker.dart

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import 'dart:js_util' as js_util;
44
import 'dart:typed_data';
55

66
import 'package:collection/collection.dart';
7-
import 'package:dart_webrtc/src/rtc_transform_stream.dart';
87
import 'package:logging/logging.dart';
98
import 'package:web/web.dart' as web;
109

10+
import 'package:dart_webrtc/src/rtc_transform_stream.dart';
1111
import 'e2ee.cryptor.dart';
1212
import 'e2ee.keyhandler.dart';
1313
import 'e2ee.logger.dart';
@@ -118,7 +118,10 @@ void main() async {
118118
uncryptedMagicBytes: options['uncryptedMagicBytes'] != null
119119
? Uint8List.fromList(
120120
base64Decode(options['uncryptedMagicBytes'] as String))
121-
: null);
121+
: null,
122+
keyRingSze: options['keyRingSize'] ?? KEYRING_SIZE,
123+
discardFrameWhenCryptorNotReady:
124+
options['discardFrameWhenCryptorNotReady'] ?? false);
122125
logger.config(
123126
'Init with keyProviderOptions:\n ${keyProviderOptions.toString()}');
124127

0 commit comments

Comments
 (0)