* 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 <[email protected]>

* 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 <[email protected]>
This commit is contained in:
CloudWebRTC
2023-04-27 17:54:32 +08:00
committed by GitHub
co-authored by Théo Monnom
parent 3407221fc4
commit 26947e96d6
42 changed files with 12523 additions and 39 deletions
+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');
}
});
}