Skip to content

Commit 7274696

Browse files
committed
feat: data packet cryptor.
1 parent 569fd75 commit 7274696

File tree

7 files changed

+554
-3
lines changed

7 files changed

+554
-3
lines changed

lib/dart_webrtc.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ library dart_webrtc;
33
export 'package:webrtc_interface/webrtc_interface.dart'
44
hide MediaDevices, MediaRecorder, Navigator;
55

6+
export 'src/data_packet_cryptor_impl.dart';
67
export 'src/factory_impl.dart';
78
export 'src/media_devices.dart';
89
export 'src/media_recorder.dart';
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import 'dart:js_interop';
2+
import 'dart:typed_data';
3+
4+
import 'package:web/web.dart' as web;
5+
import 'package:webrtc_interface/webrtc_interface.dart';
6+
7+
import 'e2ee.worker/e2ee.logger.dart';
8+
import 'event.dart';
9+
import 'frame_cryptor_impl.dart' show KeyProviderImpl, WorkerResponse;
10+
import 'utils.dart';
11+
12+
class DataPacketCryptorImpl implements DataPacketCryptor {
13+
DataPacketCryptorImpl({
14+
required this.keyProvider,
15+
required this.algorithm,
16+
});
17+
18+
final KeyProviderImpl keyProvider;
19+
final Algorithm algorithm;
20+
web.Worker get worker => keyProvider.worker;
21+
final String _dataCyrptorId = randomString(24);
22+
EventsEmitter<WorkerResponse> get events => keyProvider.events;
23+
24+
@override
25+
Future<EncryptedPacket> encrypt({
26+
required String participantId,
27+
required int keyIndex,
28+
required Uint8List data,
29+
}) async {
30+
var msgId = randomString(12);
31+
worker.postMessage(
32+
{
33+
'msgType': 'dataCyrptorEncrypt',
34+
'msgId': msgId,
35+
'keyProviderId': keyProvider.id,
36+
'dataCyrptorId': _dataCyrptorId,
37+
'participantId': participantId,
38+
'keyIndex': keyIndex,
39+
'data': data,
40+
'algorithm': algorithm.name,
41+
}.jsify(),
42+
);
43+
44+
var res = await events.waitFor<WorkerResponse>(
45+
filter: (event) {
46+
logger.fine('waiting for encrypt on msg: $msgId');
47+
return event.msgId == msgId;
48+
},
49+
duration: Duration(seconds: 5),
50+
onTimeout: () => throw Exception('waiting for encrypt on msg timed out'),
51+
);
52+
53+
return EncryptedPacket(
54+
data: res.data['data'] as Uint8List,
55+
keyIndex: res.data['keyIndex'] as int,
56+
iv: res.data['iv'] as Uint8List,
57+
);
58+
}
59+
60+
@override
61+
Future<Uint8List> decrypt({
62+
required String participantId,
63+
required EncryptedPacket encryptedPacket,
64+
}) async {
65+
var msgId = randomString(12);
66+
worker.postMessage(
67+
{
68+
'msgType': 'dataCyrptorDecrypt',
69+
'msgId': msgId,
70+
'keyProviderId': keyProvider.id,
71+
'dataCyrptorId': _dataCyrptorId,
72+
'participantId': participantId,
73+
'keyIndex': encryptedPacket.keyIndex,
74+
'data': encryptedPacket.data,
75+
'iv': encryptedPacket.iv,
76+
'algorithm': algorithm.name,
77+
}.jsify(),
78+
);
79+
80+
var res = await events.waitFor<WorkerResponse>(
81+
filter: (event) {
82+
logger.fine('waiting for decrypt on msg: $msgId');
83+
return event.msgId == msgId;
84+
},
85+
duration: Duration(seconds: 5),
86+
onTimeout: () => throw Exception('waiting for decrypt on msg timed out'),
87+
);
88+
89+
return res.data['data'] as Uint8List;
90+
}
91+
92+
@override
93+
Future<void> dispose() async {
94+
var msgId = randomString(12);
95+
worker.postMessage(
96+
{
97+
'msgType': 'dataCyrptorDispose',
98+
'msgId': msgId,
99+
'dataCyrptorId': _dataCyrptorId
100+
}.jsify(),
101+
);
102+
103+
await events.waitFor<WorkerResponse>(
104+
filter: (event) {
105+
logger.fine('waiting for dispose on msg: $msgId');
106+
return event.msgId == msgId;
107+
},
108+
duration: Duration(seconds: 5),
109+
onTimeout: () => throw Exception('waiting for dispose on msg timed out'),
110+
);
111+
}
112+
}
113+
114+
class DataPacketCryptorFactoryImpl implements DataPacketCryptorFactory {
115+
DataPacketCryptorFactoryImpl._internal();
116+
117+
static final DataPacketCryptorFactoryImpl instance =
118+
DataPacketCryptorFactoryImpl._internal();
119+
@override
120+
Future<DataPacketCryptor> createDataPacketCryptor(
121+
{required Algorithm algorithm, required KeyProvider keyProvider}) async {
122+
return Future.value(DataPacketCryptorImpl(
123+
algorithm: algorithm, keyProvider: keyProvider as KeyProviderImpl));
124+
}
125+
}
126+
127+
DataPacketCryptorFactory get dataPacketCryptorFactory =>
128+
DataPacketCryptorFactoryImpl.instance;
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import 'dart:async';
2+
import 'dart:js_interop';
3+
import 'dart:js_interop_unsafe';
4+
import 'dart:math';
5+
import 'dart:typed_data';
6+
7+
// ignore: deprecated_member_use
8+
import 'package:dart_webrtc/src/e2ee.worker/e2ee.frame_cryptor.dart'
9+
show IV_LENGTH;
10+
import 'package:js/js.dart';
11+
import 'package:web/web.dart' as web;
12+
import 'e2ee.keyhandler.dart';
13+
import 'e2ee.logger.dart';
14+
15+
class EncryptedPacket {
16+
EncryptedPacket({
17+
required this.data,
18+
required this.keyIndex,
19+
required this.iv,
20+
});
21+
22+
Uint8List data;
23+
int keyIndex;
24+
Uint8List iv;
25+
}
26+
27+
class E2EEDataPacketCryptor {
28+
E2EEDataPacketCryptor({
29+
required this.worker,
30+
required this.participantIdentity,
31+
required this.dataCryptorId,
32+
required this.keyHandler,
33+
});
34+
int sendCount_ = -1;
35+
String? participantIdentity;
36+
String? dataCryptorId;
37+
ParticipantKeyHandler keyHandler;
38+
KeyOptions get keyOptions => keyHandler.keyOptions;
39+
int currentKeyIndex = 0;
40+
final web.DedicatedWorkerGlobalScope worker;
41+
42+
void setParticipant(String identity, ParticipantKeyHandler keys) {
43+
participantIdentity = identity;
44+
keyHandler = keys;
45+
}
46+
47+
void unsetParticipant() {
48+
participantIdentity = null;
49+
}
50+
51+
void setKeyIndex(int keyIndex) {
52+
logger.config('setKeyIndex for $participantIdentity, newIndex: $keyIndex');
53+
currentKeyIndex = keyIndex;
54+
}
55+
56+
Uint8List makeIv({required int timestamp}) {
57+
var iv = ByteData(IV_LENGTH);
58+
59+
// having to keep our own send count (similar to a picture id) is not ideal.
60+
if (sendCount_ == -1) {
61+
// Initialize with a random offset, similar to the RTP sequence number.
62+
sendCount_ = Random.secure().nextInt(0xffff);
63+
}
64+
65+
var sendCount = sendCount_ ?? 0;
66+
final randomBytes =
67+
Random.secure().nextInt(max(0, 0xffffffff)).toUnsigned(32);
68+
69+
iv.setUint32(0, randomBytes);
70+
iv.setUint32(4, timestamp);
71+
iv.setUint32(8, timestamp - (sendCount % 0xffff));
72+
73+
sendCount = sendCount + 1;
74+
75+
return iv.buffer.asUint8List();
76+
}
77+
78+
void postMessage(Object message) {
79+
worker.postMessage(message.jsify());
80+
}
81+
82+
Future<EncryptedPacket?> encrypt(
83+
ParticipantKeyHandler keys,
84+
Uint8List data,
85+
) async {
86+
logger.fine('encodeFunction: buffer ${data.length}');
87+
88+
var secretKey = keyHandler.getKeySet(currentKeyIndex)?.encryptionKey;
89+
var keyIndex = currentKeyIndex;
90+
91+
if (secretKey == null) {
92+
logger.warning(
93+
'encodeFunction: no secretKey for index $keyIndex, cannot encrypt');
94+
return null;
95+
}
96+
97+
var iv = makeIv(timestamp: DateTime.timestamp().millisecondsSinceEpoch);
98+
99+
var frameTrailer = ByteData(2);
100+
frameTrailer.setInt8(0, IV_LENGTH);
101+
frameTrailer.setInt8(1, keyIndex);
102+
103+
try {
104+
var cipherText = await worker.crypto.subtle
105+
.encrypt(
106+
{
107+
'name': 'AES-GCM',
108+
'iv': iv,
109+
}.jsify() as web.AlgorithmIdentifier,
110+
secretKey,
111+
data.toJS,
112+
)
113+
.toDart as JSArrayBuffer;
114+
115+
logger.finer(
116+
'encodeFunction: encrypted buffer: ${data.length}, cipherText: ${cipherText.toDart.asUint8List().length}');
117+
118+
return EncryptedPacket(
119+
data: cipherText.toDart.asUint8List(),
120+
keyIndex: keyIndex,
121+
iv: iv,
122+
);
123+
} catch (e) {
124+
logger.warning('encodeFunction encrypt: e ${e.toString()}');
125+
rethrow;
126+
}
127+
}
128+
129+
Future<Uint8List?> decrypt(
130+
ParticipantKeyHandler keys,
131+
EncryptedPacket encryptedPacket,
132+
) async {
133+
var ratchetCount = 0;
134+
135+
logger.fine(
136+
'decodeFunction: data packet lenght ${encryptedPacket.data.length}');
137+
138+
ByteBuffer? decrypted;
139+
KeySet? initialKeySet;
140+
var initialKeyIndex = currentKeyIndex;
141+
142+
try {
143+
var ivLength = encryptedPacket.iv.length;
144+
var keyIndex = encryptedPacket.keyIndex;
145+
var iv = encryptedPacket.iv;
146+
var payload = encryptedPacket.data;
147+
initialKeySet = keyHandler.getKeySet(initialKeyIndex);
148+
149+
logger.finer(
150+
'decodeFunction: start decrypting data packet length ${payload.length}, ivLength $ivLength, keyIndex $keyIndex, iv $iv');
151+
152+
/// missingKey flow:
153+
/// tries to decrypt once, fails, tries to ratchet once and decrypt again,
154+
/// fails (does not save ratcheted key), bumps _decryptionFailureCount,
155+
/// if higher than failuretolerance hasValidKey is set to false, on next
156+
/// frame it fires a missingkey
157+
/// to throw missingkeys faster lower your failureTolerance
158+
if (initialKeySet == null || !keyHandler.hasValidKey) {
159+
return null;
160+
}
161+
var currentkeySet = initialKeySet;
162+
163+
Future<void> decryptFrameInternal() async {
164+
decrypted = ((await worker.crypto.subtle
165+
.decrypt(
166+
{
167+
'name': 'AES-GCM',
168+
'iv': iv,
169+
}.jsify() as web.AlgorithmIdentifier,
170+
currentkeySet.encryptionKey,
171+
payload.toJS,
172+
)
173+
.toDart) as JSArrayBuffer)
174+
.toDart;
175+
logger.finer(
176+
'decodeFunction::decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}');
177+
178+
if (decrypted == null) {
179+
throw Exception('[decryptFrameInternal] could not decrypt');
180+
}
181+
logger.finer(
182+
'decodeFunction::decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}');
183+
if (currentkeySet != initialKeySet) {
184+
logger.fine(
185+
'decodeFunction::decryptFrameInternal: ratchetKey: decryption ok, newState: kKeyRatcheted');
186+
await keyHandler.setKeySetFromMaterial(
187+
currentkeySet, initialKeyIndex);
188+
}
189+
}
190+
191+
Future<void> ratchedKeyInternal() async {
192+
if (ratchetCount >= keyOptions.ratchetWindowSize ||
193+
keyOptions.ratchetWindowSize <= 0) {
194+
throw Exception('[ratchedKeyInternal] cannot ratchet anymore');
195+
}
196+
197+
var newKeyBuffer = await keyHandler.ratchet(
198+
currentkeySet.material, keyOptions.ratchetSalt);
199+
var newMaterial = await keyHandler.ratchetMaterial(
200+
currentkeySet.material, newKeyBuffer.buffer);
201+
currentkeySet =
202+
await keyHandler.deriveKeys(newMaterial, keyOptions.ratchetSalt);
203+
ratchetCount++;
204+
await decryptFrameInternal();
205+
}
206+
207+
try {
208+
/// gets frame -> tries to decrypt -> tries to ratchet (does this failureTolerance
209+
/// times, then says missing key)
210+
/// we only save the new key after ratcheting if we were able to decrypt something
211+
await decryptFrameInternal();
212+
} catch (e) {
213+
logger.finer('decodeFunction: kInternalError catch $e');
214+
await ratchedKeyInternal();
215+
}
216+
217+
if (decrypted == null) {
218+
throw Exception(
219+
'[decodeFunction] decryption failed even after ratchting');
220+
}
221+
222+
// we can now be sure that decryption was a success
223+
keyHandler.decryptionSuccess();
224+
225+
logger.finer(
226+
'decodeFunction: decryption success, buffer length ${payload.length}, decrypted: ${decrypted!.asUint8List().length}');
227+
228+
return decrypted!.asUint8List();
229+
} catch (e) {
230+
keyHandler.decryptionFailure();
231+
rethrow;
232+
}
233+
}
234+
}

0 commit comments

Comments
 (0)