Skip to content

Commit

Permalink
merge
Browse files Browse the repository at this point in the history
  • Loading branch information
timu-jesse-ezell committed Aug 8, 2024
2 parents 7335a11 + 38badf0 commit 2e25499
Show file tree
Hide file tree
Showing 23 changed files with 360 additions and 184 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v1
- name: Publish
uses: sakebook/actions-flutter-pub-publisher@v1.3.1
uses: k-paxian/dart-package-publisher@v1.5.1
with:
credential: ${{ secrets.CREDENTIAL_JSON }}
flutter_package: true
skip_test: true
dry_run: false
credentialJson: ${{ secrets.CREDENTIAL_JSON }}
flutter: true
skipTests: true
force: true
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,71 @@
# Changelog

--------------------------------------------
[1.4.8] - 2024-07-12

* fix: missing streamCompleter complete for getUserMedia.
* fix: RTCPeerConnectionWeb.getRemoteStreams.

[1.4.7] - 2024-07-12

* fix: MediaStreamTrack.getSettings.

[1.4.6+hotfix.2] - 2024-06-07

[1.4.6+hotfix.1] - 2024-06-07

* Wider version dependencies for js/http.

[1.4.6] - 2024-06-05

* chore: bump version for js and http.
* fix: decrypting audio when e2ee.
* fix: translate audio constraints for web.
* fix: missing fault tolerance, better worker reports and a increased timeout for worker tasks.
* fix type cast exception in getConstraints()

[1.4.5] - 2024-05-13

* fix: negotiationNeeded listener.
* fix: fix type cast exception in getConstraints().

[1.4.4] - 2024-04-24

* fix: datachannel message parse for Firefox.
* fix: tryCatch editing mediaConstraints #34

[1.4.3] - 2024-04-18

* fix: do not fail if removing constraint fails

[1.4.2] - 2024-04-15

* fix.

[1.4.1] - 2024-04-12

* remove RTCConfiguration convert.

[1.4.0] - 2024-04-09

* Fixed bug for RTCConfiguration convert.

[1.3.3] - 2024-04-09

* Fix DC data parse.

[1.3.2] - 2024-04-09

* Fix error when constructing RTCDataChannelInit.

[1.3.1] - 2024-04-08

* Add keyRingSize/discardFrameWhenCryptorNotReady to KeyProviderOptions.

[1.3.0] - 2024-04-08

* update to package:web by @jezell in #29.

[1.2.1] - 2024-02-05

* Downgrade some dependencies make more compatible.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ dart compile js ./lib/src/e2ee.worker/e2ee.worker.dart -o web/e2ee.worker.dart.j
## How to develop

* `git clone https://github.com/flutter-webrtc/dart-webrtc && cd dart-webrtc`
* `pub get`
* `pub global activate webdev`
* `dart pub get`
* `dart pub global activate webdev`
* `webdev serve --auto=refresh`
145 changes: 85 additions & 60 deletions lib/src/e2ee.worker/e2ee.cryptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import 'dart:js_util' as jsutil;
import 'dart:math';
import 'dart:typed_data';

import 'package:dart_webrtc/src/rtc_transform_stream.dart';
import 'package:web/web.dart' as web;

import 'package:dart_webrtc/src/rtc_transform_stream.dart';
import 'crypto.dart' as crypto;
import 'e2ee.keyhandler.dart';
import 'e2ee.logger.dart';
Expand Down Expand Up @@ -301,6 +301,9 @@ class FrameCryptor {
if (!enabled ||
// skip for encryption for empty dtx frames
buffer.isEmpty) {
if (keyOptions.discardFrameWhenCryptorNotReady) {
return;
}
controller.enqueue(frame);
return;
}
Expand Down Expand Up @@ -405,6 +408,8 @@ class FrameCryptor {
// skip for encryption for empty dtx frames
buffer.isEmpty) {
sifGuard.recordUserFrame();
if (keyOptions.discardFrameWhenCryptorNotReady) return;
logger.fine('enqueing empty frame');
controller.enqueue(frame);
return;
}
Expand All @@ -415,7 +420,7 @@ class FrameCryptor {
var magicBytesBuffer = buffer.sublist(
buffer.length - magicBytes.length - 1, buffer.length - 1);
logger.finer(
'magicBytesBuffer $magicBytesBuffer, magicBytes $magicBytes, ');
'magicBytesBuffer $magicBytesBuffer, magicBytes $magicBytes');
if (magicBytesBuffer.toString() == magicBytes.toString()) {
sifGuard.recordSif();
if (sifGuard.isSifAllowed()) {
Expand All @@ -425,6 +430,7 @@ class FrameCryptor {
finalBuffer.add(Uint8List.fromList(
buffer.sublist(0, buffer.length - (magicBytes.length + 1))));
frame.data = crypto.jsArrayBufferFrom(finalBuffer.toBytes());
logger.fine('enqueing silent frame');
controller.enqueue(frame);
} else {
logger.finer('SIF limit reached, dropping frame');
Expand All @@ -449,6 +455,12 @@ class FrameCryptor {
initialKeySet = keyHandler.getKeySet(keyIndex);
initialKeyIndex = keyIndex;

/// missingKey flow:
/// tries to decrypt once, fails, tries to ratchet once and decrypt again,
/// fails (does not save ratcheted key), bumps _decryptionFailureCount,
/// if higher than failuretolerance hasValidKey is set to false, on next
/// frame it fires a missingkey
/// to throw missingkeys faster lower your failureTolerance
if (initialKeySet == null || !keyHandler.hasValidKey) {
if (lastError != CryptorError.kMissingKey) {
lastError = CryptorError.kMissingKey;
Expand All @@ -462,14 +474,14 @@ class FrameCryptor {
'error': 'Missing key for track $trackId'
});
}
controller.enqueue(frame);
// controller.enqueue(frame);
return;
}
var endDecLoop = false;
var currentkeySet = initialKeySet;
while (!endDecLoop) {
try {
decrypted = await jsutil.promiseToFuture<ByteBuffer>(crypto.decrypt(

Future<void> decryptFrameInternal() async {
decrypted = await jsutil.promiseToFuture<ByteBuffer>(
crypto.decrypt(
crypto.AesGcmParams(
name: 'AES-GCM',
iv: crypto.jsArrayBufferFrom(iv),
Expand All @@ -478,56 +490,78 @@ class FrameCryptor {
),
currentkeySet.encryptionKey,
crypto.jsArrayBufferFrom(
buffer.sublist(headerLength, buffer.length - ivLength - 2)),
));

if (currentkeySet != initialKeySet) {
logger.fine(
'ratchetKey: decryption ok, reset state to kKeyRatcheted');
await keyHandler.setKeySetFromMaterial(
currentkeySet, initialKeyIndex);
}
buffer.sublist(headerLength, buffer.length - ivLength - 2),
),
),
);
if (decrypted == null) {
throw Exception('[decryptFrameInternal] could not decrypt');
}

endDecLoop = true;
if (currentkeySet != initialKeySet) {
logger.fine('ratchetKey: decryption ok, newState: kKeyRatcheted');
await keyHandler.setKeySetFromMaterial(
currentkeySet, initialKeyIndex);
}

if (lastError != CryptorError.kOk &&
lastError != CryptorError.kKeyRatcheted &&
ratchetCount > 0) {
logger.finer(
'KeyRatcheted: ssrc ${metaData.synchronizationSource} timestamp ${frame.timestamp} ratchetCount $ratchetCount participantId: $participantIdentity');
logger.finer(
'ratchetKey: lastError != CryptorError.kKeyRatcheted, reset state to kKeyRatcheted');

lastError = CryptorError.kKeyRatcheted;
postMessage({
'type': 'cryptorState',
'msgType': 'event',
'participantId': participantIdentity,
'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 newKeyBuffer = crypto.jsArrayBufferFrom(await keyHandler.ratchet(
currentkeySet.material, keyOptions.ratchetSalt));
var newMaterial = await keyHandler.ratchetMaterial(
currentkeySet.material, newKeyBuffer);
currentkeySet =
await keyHandler.deriveKeys(newMaterial, keyOptions.ratchetSalt);
ratchetCount++;
if (lastError != CryptorError.kOk &&
lastError != CryptorError.kKeyRatcheted &&
ratchetCount > 0) {
logger.finer(
'KeyRatcheted: ssrc ${metaData.synchronizationSource} timestamp ${frame.timestamp} ratchetCount $ratchetCount participantId: $participantIdentity');
logger.finer(
'ratchetKey: lastError != CryptorError.kKeyRatcheted, reset state to kKeyRatcheted');

lastError = CryptorError.kKeyRatcheted;
postMessage({
'type': 'cryptorState',
'msgType': 'event',
'participantId': participantIdentity,
'trackId': trackId,
'kind': kind,
'state': 'keyRatcheted',
'error': 'Key ratcheted ok'
});
}
}

Future<void> ratchedKeyInternal() async {
if (ratchetCount >= keyOptions.ratchetWindowSize ||
keyOptions.ratchetWindowSize <= 0) {
throw Exception('[ratchedKeyInternal] cannot ratchet anymore');
}

var newKeyBuffer = crypto.jsArrayBufferFrom(await keyHandler.ratchet(
currentkeySet.material, keyOptions.ratchetSalt));
var newMaterial = await keyHandler.ratchetMaterial(
currentkeySet.material, newKeyBuffer);
currentkeySet =
await keyHandler.deriveKeys(newMaterial, keyOptions.ratchetSalt);
ratchetCount++;
await decryptFrameInternal();
}

try {
/// gets frame -> tries to decrypt -> tries to ratchet (does this failureTolerance
/// times, then says missing key)
/// we only save the new key after ratcheting if we were able to decrypt something
await decryptFrameInternal();
} catch (e) {
lastError = CryptorError.kInternalError;
await ratchedKeyInternal();
}

if (decrypted == null) {
throw Exception(
'[decodeFunction] decryption failed even after ratchting');
}

// we can now be sure that decryption was a success
keyHandler.decryptionSuccess();

logger.finer(
'buffer: ${buffer.length}, decrypted: ${decrypted?.asUint8List().length ?? 0}');
'buffer: ${buffer.length}, decrypted: ${decrypted!.asUint8List().length}');

var finalBuffer = BytesBuilder();

finalBuffer.add(Uint8List.fromList(buffer.sublist(0, headerLength)));
Expand Down Expand Up @@ -564,15 +598,6 @@ class FrameCryptor {
});
}

/// 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) {
logger.warning(
'decryption failed, ratcheting back to initial key, keyIndex: $initialKeyIndex');
await keyHandler.setKeySetFromMaterial(initialKeySet, initialKeyIndex);
}
keyHandler.decryptionFailure();
}
}
Expand Down
17 changes: 13 additions & 4 deletions lib/src/e2ee.worker/e2ee.keyhandler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,25 @@ import 'crypto.dart' as crypto;
import 'e2ee.logger.dart';
import 'e2ee.utils.dart';

const KEYRING_SIZE = 16;

class KeyOptions {
KeyOptions({
required this.sharedKey,
required this.ratchetSalt,
required this.ratchetWindowSize,
this.uncryptedMagicBytes,
this.failureTolerance = -1,
this.keyRingSze = KEYRING_SIZE,
this.discardFrameWhenCryptorNotReady = false,
});
bool sharedKey;
Uint8List ratchetSalt;
int ratchetWindowSize = 0;
int failureTolerance;
Uint8List? uncryptedMagicBytes;
int keyRingSze;
bool discardFrameWhenCryptorNotReady;

@override
String toString() {
Expand Down Expand Up @@ -77,8 +83,6 @@ class KeyProvider {
}
}

const KEYRING_SIZE = 16;

class KeySet {
KeySet(this.material, this.encryptionKey);
web.CryptoKey material;
Expand All @@ -90,10 +94,15 @@ class ParticipantKeyHandler {
required this.worker,
required this.keyOptions,
required this.participantIdentity,
});
}) {
if (keyOptions.keyRingSze <= 0 || keyOptions.keyRingSze > 255) {
throw Exception('Invalid key ring size');
}
cryptoKeyRing = List.filled(keyOptions.keyRingSze, null);
}
int currentKeyIndex = 0;

List<KeySet?> cryptoKeyRing = List.filled(KEYRING_SIZE, null);
late List<KeySet?> cryptoKeyRing;

bool _hasValidKey = false;

Expand Down
7 changes: 5 additions & 2 deletions lib/src/e2ee.worker/e2ee.worker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import 'dart:js_util' as js_util;
import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:dart_webrtc/src/rtc_transform_stream.dart';
import 'package:logging/logging.dart';
import 'package:web/web.dart' as web;

import 'package:dart_webrtc/src/rtc_transform_stream.dart';
import 'e2ee.cryptor.dart';
import 'e2ee.keyhandler.dart';
import 'e2ee.logger.dart';
Expand Down Expand Up @@ -118,7 +118,10 @@ void main() async {
uncryptedMagicBytes: options['uncryptedMagicBytes'] != null
? Uint8List.fromList(
base64Decode(options['uncryptedMagicBytes'] as String))
: null);
: null,
keyRingSze: options['keyRingSize'] ?? KEYRING_SIZE,
discardFrameWhenCryptorNotReady:
options['discardFrameWhenCryptorNotReady'] ?? false);
logger.config(
'Init with keyProviderOptions:\n ${keyProviderOptions.toString()}');

Expand Down
Loading

0 comments on commit 2e25499

Please sign in to comment.