Skip to content

fix E2EE for FireFox. #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 65 additions & 66 deletions lib/src/e2ee.worker/e2ee.cryptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,11 @@ enum CryptorError {

class FrameInfo {
FrameInfo({
required this.mimeType,
required this.ssrc,
required this.timestamp,
required this.buffer,
required this.frameType,
});
String? mimeType;
String frameType;
int ssrc;
int timestamp;
Expand All @@ -144,7 +142,6 @@ class FrameCryptor {
String? participantIdentity;
String? trackId;
String? codec;
String? mineType;
ParticipantKeyHandler keyHandler;
KeyOptions get keyOptions => keyHandler.keyOptions;
late String kind;
Expand Down Expand Up @@ -282,8 +279,7 @@ class FrameCryptor {
data = obj.data.toDart.asUint8List();
if (obj.hasProperty('type'.toJS).toDart) {
frameType = obj.type;
logger.finer(
'frameType: $frameType, mineTye ${obj.getMetadata().mimeType}');
logger.finer('frameType: $frameType');
}
}

Expand Down Expand Up @@ -318,33 +314,40 @@ class FrameCryptor {
}

FrameInfo readFrameInfo(JSObject frameObj) {
Uint8List buffer;
var buffer = Uint8List(0);
var synchronizationSource = 0;
var timestamp = 0;
var frameType = '';
if (frameObj is web.RTCEncodedVideoFrame) {
buffer = frameObj.data.toDart.asUint8List();
if (frameObj.hasProperty('type'.toJS).toDart) {
frameType = frameObj.type;
logger.finer(
'frameType: $frameType, mineTye ${frameObj.getMetadata().mimeType}');
logger.finer('frameType: $frameType');
}
synchronizationSource = frameObj.getMetadata().synchronizationSource;
timestamp = frameObj.getMetadata().rtpTimestamp.toInt();
mineType ??= frameObj.getMetadata().mimeType;
if (frameObj.getMetadata().hasProperty('rtpTimestamp'.toJS).toDart) {
timestamp = frameObj.getMetadata().rtpTimestamp.toInt();
} else if (frameObj.hasProperty('timestamp'.toJS).toDart) {
timestamp =
(frameObj.getProperty('timestamp'.toJS) as JSNumber).toDartInt;
}
} else if (frameObj is web.RTCEncodedAudioFrame) {
buffer = frameObj.data.toDart.asUint8List();
synchronizationSource = frameObj.getMetadata().synchronizationSource;
timestamp = frameObj.getMetadata().rtpTimestamp.toInt();
mineType ??= frameObj.getMetadata().mimeType;

if (frameObj.getMetadata().hasProperty('rtpTimestamp'.toJS).toDart) {
timestamp = frameObj.getMetadata().rtpTimestamp.toInt();
} else if (frameObj.hasProperty('timestamp'.toJS).toDart) {
timestamp =
(frameObj.getProperty('timestamp'.toJS) as JSNumber).toDartInt;
}
frameType = 'audio';
} else {
throw Exception(
'encodeFunction: frame is not a RTCEncodedVideoFrame or RTCEncodedAudioFrame');
}

return FrameInfo(
mimeType: mineType,
ssrc: synchronizationSource,
timestamp: timestamp,
buffer: buffer,
Expand All @@ -366,43 +369,44 @@ class FrameCryptor {
JSObject frameObj,
web.TransformStreamDefaultController controller,
) async {
var srcFrame = readFrameInfo(frameObj);

mineType ??= srcFrame.mimeType;

logger.finer(
'encodeFunction: frame ${srcFrame.buffer.length}, synchronizationSource ${srcFrame.ssrc} mineType $mineType frameType ${srcFrame.frameType}');

if (!enabled ||
// skip for encryption for empty dtx frames
srcFrame.buffer.isEmpty) {
if (keyOptions.discardFrameWhenCryptorNotReady) {
try {
if (!enabled ||
// skip for encryption for empty dtx frames
((frameObj is web.RTCEncodedVideoFrame &&
frameObj.data.toDart.lengthInBytes == 0) ||
(frameObj is web.RTCEncodedAudioFrame &&
frameObj.data.toDart.lengthInBytes == 0))) {
if (keyOptions.discardFrameWhenCryptorNotReady) {
return;
}
controller.enqueue(frameObj);
return;
}
controller.enqueue(frameObj);
return;
}

var secretKey = keyHandler.getKeySet(currentKeyIndex)?.encryptionKey;
var keyIndex = currentKeyIndex;
var srcFrame = readFrameInfo(frameObj);

if (secretKey == null) {
if (lastError != CryptorError.kMissingKey) {
lastError = CryptorError.kMissingKey;
postMessage({
'type': 'cryptorState',
'msgType': 'event',
'participantId': participantIdentity,
'trackId': trackId,
'kind': kind,
'state': 'missingKey',
'error': 'Missing key for track $trackId',
});
logger.fine(
'encodeFunction: buffer ${srcFrame.buffer.length}, synchronizationSource ${srcFrame.ssrc} frameType ${srcFrame.frameType}');

var secretKey = keyHandler.getKeySet(currentKeyIndex)?.encryptionKey;
var keyIndex = currentKeyIndex;

if (secretKey == null) {
if (lastError != CryptorError.kMissingKey) {
lastError = CryptorError.kMissingKey;
postMessage({
'type': 'cryptorState',
'msgType': 'event',
'participantId': participantIdentity,
'trackId': trackId,
'kind': kind,
'state': 'missingKey',
'error': 'Missing key for track $trackId',
});
}
return;
}
return;
}

try {
var headerLength =
kind == 'video' ? getUnencryptedBytes(frameObj, codec) : 1;

Expand All @@ -426,7 +430,7 @@ class FrameCryptor {
.toDart as JSArrayBuffer;

logger.finer(
'encodeFunction, buffer: ${srcFrame.buffer.length}, cipherText: ${cipherText.toDart.asUint8List().length}');
'encodeFunction: encrypted buffer: ${srcFrame.buffer.length}, cipherText: ${cipherText.toDart.asUint8List().length}');
var finalBuffer = BytesBuilder();

finalBuffer
Expand All @@ -436,8 +440,6 @@ class FrameCryptor {
finalBuffer.add(frameTrailer.buffer.asUint8List());

enqueueFrame(frameObj, controller, finalBuffer);
logger.finer(
'encodeFunction: ssrc ${srcFrame.ssrc} buffer length ${srcFrame.buffer.length}, encrypted: ${finalBuffer.length}, headerLength $headerLength cipherText ${cipherText.toDart.asUint8List().length} iv ${iv.length} frameTrailer ${frameTrailer.buffer.asUint8List().length}');

if (lastError != CryptorError.kOk) {
lastError = CryptorError.kOk;
Expand All @@ -453,7 +455,7 @@ class FrameCryptor {
}

logger.finer(
'encrypto kind $kind,codec $codec headerLength: $headerLength, timestamp: ${srcFrame.timestamp}, ssrc: ${srcFrame.ssrc}, data length: ${srcFrame.buffer.length}, encrypted length: ${finalBuffer.toBytes().length}, iv $iv');
'encodeFunction[CryptorError.kOk]: frame enqueued kind $kind,codec $codec headerLength: $headerLength, timestamp: ${srcFrame.timestamp}, ssrc: ${srcFrame.ssrc}, data length: ${srcFrame.buffer.length}, encrypted length: ${finalBuffer.toBytes().length}, iv $iv');
} catch (e) {
logger.warning('encodeFunction encrypt: e ${e.toString()}');
if (lastError != CryptorError.kEncryptError) {
Expand All @@ -476,11 +478,9 @@ class FrameCryptor {
web.TransformStreamDefaultController controller,
) async {
var srcFrame = readFrameInfo(frameObj);
mineType ??= srcFrame.mimeType;
var ratchetCount = 0;

logger.finer(
'decodeFunction: frame ${srcFrame.buffer.length} mineType $mineType');
logger.fine('decodeFunction: frame lenght ${srcFrame.buffer.length}');

ByteBuffer? decrypted;
KeySet? initialKeySet;
Expand Down Expand Up @@ -510,17 +510,18 @@ class FrameCryptor {
if (sifGuard.isSifAllowed()) {
var frameType =
srcFrame.buffer.sublist(srcFrame.buffer.length - 1)[0];
logger.finer('skip uncrypted frame, type $frameType');
logger
.finer('ecodeFunction: skip uncrypted frame, type $frameType');
var finalBuffer = BytesBuilder();
finalBuffer.add(Uint8List.fromList(srcFrame.buffer
.sublist(0, srcFrame.buffer.length - (magicBytes.length + 1))));
enqueueFrame(frameObj, controller, finalBuffer);
logger.fine('enqueing silent frame');
logger.fine('ecodeFunction: enqueing silent frame');
controller.enqueue(frameObj);
} else {
logger.finer('SIF limit reached, dropping frame');
logger.finer('ecodeFunction: SIF limit reached, dropping frame');
}
logger.finer('enqueing silent frame');
logger.finer('ecodeFunction: enqueing silent frame');
controller.enqueue(frameObj);
return;
} else {
Expand All @@ -545,9 +546,6 @@ class FrameCryptor {
logger.finer(
'decodeFunction: start decrypting frame headerLength $headerLength ${srcFrame.buffer.length} frameTrailer $frameTrailer, ivLength $ivLength, keyIndex $keyIndex, iv $iv');

logger.finer(
'decodeFunction: ssrc ${srcFrame.ssrc} buffer length ${srcFrame.buffer.length}, encrypted: ${srcFrame.buffer.length}, headerLength $headerLength cipherText ${srcFrame.buffer.sublist(headerLength, srcFrame.buffer.length - ivLength - 2).length} iv ${iv.length} frameTrailer ${frameTrailer.buffer.asUint8List().length}');

/// missingKey flow:
/// tries to decrypt once, fails, tries to ratchet once and decrypt again,
/// fails (does not save ratcheted key), bumps _decryptionFailureCount,
Expand Down Expand Up @@ -589,15 +587,16 @@ class FrameCryptor {
.toDart) as JSArrayBuffer)
.toDart;
logger.finer(
'decodeFunction: decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}');
'decodeFunction::decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}');

if (decrypted == null) {
throw Exception('[decryptFrameInternal] could not decrypt');
}
logger.finer(
'decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}');
'decodeFunction::decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}');
if (currentkeySet != initialKeySet) {
logger.fine('ratchetKey: decryption ok, newState: kKeyRatcheted');
logger.fine(
'decodeFunction::decryptFrameInternal: ratchetKey: decryption ok, newState: kKeyRatcheted');
await keyHandler.setKeySetFromMaterial(
currentkeySet, initialKeyIndex);
}
Expand All @@ -606,9 +605,9 @@ class FrameCryptor {
lastError != CryptorError.kKeyRatcheted &&
ratchetCount > 0) {
logger.finer(
'KeyRatcheted: ssrc ${srcFrame.ssrc} timestamp ${srcFrame.timestamp} ratchetCount $ratchetCount participantId: $participantIdentity');
'decodeFunction::decryptFrameInternal: KeyRatcheted: ssrc ${srcFrame.ssrc} timestamp ${srcFrame.timestamp} ratchetCount $ratchetCount participantId: $participantIdentity');
logger.finer(
'ratchetKey: lastError != CryptorError.kKeyRatcheted, reset state to kKeyRatcheted');
'decodeFunction::decryptFrameInternal: ratchetKey: lastError != CryptorError.kKeyRatcheted, reset state to kKeyRatcheted');

lastError = CryptorError.kKeyRatcheted;
postMessage({
Expand Down Expand Up @@ -659,7 +658,7 @@ class FrameCryptor {
keyHandler.decryptionSuccess();

logger.finer(
'decodeFunction: buffer length ${srcFrame.buffer.length}, decrypted: ${decrypted!.asUint8List().length}');
'decodeFunction: decryption success, buffer length ${srcFrame.buffer.length}, decrypted: ${decrypted!.asUint8List().length}');

var finalBuffer = BytesBuilder();

Expand All @@ -681,8 +680,8 @@ class FrameCryptor {
});
}

logger.finer(
'decrypto kind $kind, codec $mineType headerLength: $headerLength, timestamp: ${srcFrame.timestamp}, ssrc: ${srcFrame.ssrc}, data length: ${srcFrame.buffer.length}, decrypted length: ${finalBuffer.toBytes().length}, keyindex $keyIndex iv $iv');
logger.fine(
'decodeFunction[CryptorError.kOk]: decryption success kind $kind, headerLength: $headerLength, timestamp: ${srcFrame.timestamp}, ssrc: ${srcFrame.ssrc}, data length: ${srcFrame.buffer.length}, decrypted length: ${finalBuffer.toBytes().length}, keyindex $keyIndex iv $iv');
} catch (e) {
if (lastError != CryptorError.kDecryptError) {
lastError = CryptorError.kDecryptError;
Expand Down
4 changes: 2 additions & 2 deletions lib/src/e2ee.worker/e2ee.worker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ void main() async {
var kind = options.getProperty('kind'.toJS) as JSString;
var participantId = options.getProperty('participantId'.toJS) as JSString;
var trackId = options.getProperty('trackId'.toJS) as JSString;
var codec = options.getProperty('codec'.toJS) as JSString;
var codec = options.getProperty('codec'.toJS) as JSString?;
var msgType = options.getProperty('msgType'.toJS) as JSString;
var keyProviderId = options.getProperty('keyProviderId'.toJS) as JSString;

Expand All @@ -88,7 +88,7 @@ void main() async {
writable: transformer.writable,
trackId: trackId.toDart,
kind: kind.toDart,
codec: codec.toDart);
codec: codec?.toDart);
}.toJS;
}

Expand Down