Skip to content

Commit 605a656

Browse files
committed
e2ee improve.
1 parent 7982c34 commit 605a656

File tree

10 files changed

+1057
-357
lines changed

10 files changed

+1057
-357
lines changed

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

Lines changed: 113 additions & 181 deletions
Large diffs are not rendered by default.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import 'dart:async';
2+
import 'dart:html';
3+
import 'dart:js_util' as jsutil;
4+
import 'dart:typed_data';
5+
6+
import 'crypto.dart' as crypto;
7+
import 'e2ee.logger.dart';
8+
import 'e2ee.utils.dart';
9+
10+
class KeyOptions {
11+
KeyOptions({
12+
required this.sharedKey,
13+
required this.ratchetSalt,
14+
required this.ratchetWindowSize,
15+
this.uncryptedMagicBytes,
16+
this.failureTolerance = -1,
17+
});
18+
bool sharedKey;
19+
Uint8List ratchetSalt;
20+
int ratchetWindowSize = 0;
21+
int failureTolerance;
22+
Uint8List? uncryptedMagicBytes;
23+
24+
@override
25+
String toString() {
26+
return 'KeyOptions{sharedKey: $sharedKey, ratchetWindowSize: $ratchetWindowSize, failureTolerance: $failureTolerance, uncryptedMagicBytes: $uncryptedMagicBytes, ratchetSalt: $ratchetSalt}';
27+
}
28+
}
29+
30+
const KEYRING_SIZE = 16;
31+
32+
class KeySet {
33+
KeySet(this.material, this.encryptionKey);
34+
CryptoKey material;
35+
CryptoKey encryptionKey;
36+
}
37+
38+
class ParticipantKeyHandler {
39+
ParticipantKeyHandler({
40+
required this.worker,
41+
required this.keyOptions,
42+
required this.participantIdentity,
43+
});
44+
int currentKeyIndex = 0;
45+
46+
List<KeySet?> cryptoKeyRing = List.filled(KEYRING_SIZE, null);
47+
48+
bool _hasValidKey = false;
49+
50+
bool get hasValidKey => _hasValidKey;
51+
52+
final KeyOptions keyOptions;
53+
54+
final DedicatedWorkerGlobalScope worker;
55+
56+
final String participantIdentity;
57+
58+
int _decryptionFailureCount = 0;
59+
60+
void decryptionFailure() {
61+
if (keyOptions.failureTolerance < 0) {
62+
return;
63+
}
64+
_decryptionFailureCount += 1;
65+
66+
if (_decryptionFailureCount > keyOptions.failureTolerance) {
67+
logger.warning('key for $participantIdentity is being marked as invalid');
68+
_hasValidKey = false;
69+
}
70+
}
71+
72+
void decryptionSuccess() {
73+
resetKeyStatus();
74+
}
75+
76+
/// Call this after user initiated ratchet or a new key has been set in order
77+
/// to make sure to mark potentially invalid keys as valid again
78+
void resetKeyStatus() {
79+
_decryptionFailureCount = 0;
80+
_hasValidKey = true;
81+
}
82+
83+
Future<Uint8List?> exportKey(int? keyIndex) async {
84+
var currentMaterial = getKeySet(keyIndex)?.material;
85+
if (currentMaterial == null) {
86+
return null;
87+
}
88+
try {
89+
var key = await jsutil.promiseToFuture<ByteBuffer>(
90+
crypto.exportKey('raw', currentMaterial));
91+
return key.asUint8List();
92+
} catch (e) {
93+
logger.warning('exportKey: $e');
94+
return null;
95+
}
96+
}
97+
98+
Future<Uint8List?> ratchetKey(int? keyIndex) async {
99+
var currentMaterial = getKeySet(keyIndex)?.material;
100+
if (currentMaterial == null) {
101+
return null;
102+
}
103+
var newKey = await ratchet(currentMaterial, keyOptions.ratchetSalt);
104+
var newMaterial = await ratchetMaterial(
105+
currentMaterial, crypto.jsArrayBufferFrom(newKey));
106+
var newKeySet = await deriveKeys(newMaterial, keyOptions.ratchetSalt);
107+
await setKeySetFromMaterial(newKeySet, keyIndex ?? currentKeyIndex);
108+
return newKey;
109+
}
110+
111+
Future<CryptoKey> ratchetMaterial(
112+
CryptoKey currentMaterial, ByteBuffer newKeyBuffer) async {
113+
var newMaterial = await jsutil.promiseToFuture(crypto.importKey(
114+
'raw',
115+
newKeyBuffer,
116+
(currentMaterial.algorithm as crypto.Algorithm).name,
117+
false,
118+
['deriveBits', 'deriveKey'],
119+
));
120+
return newMaterial;
121+
}
122+
123+
KeySet? getKeySet(int? keyIndex) {
124+
return cryptoKeyRing[keyIndex ?? currentKeyIndex];
125+
}
126+
127+
Future<void> setKey(Uint8List key, {int keyIndex = 0}) async {
128+
var keyMaterial = await crypto.impportKeyFromRawData(key,
129+
webCryptoAlgorithm: 'PBKDF2', keyUsages: ['deriveBits', 'deriveKey']);
130+
var keySet = await deriveKeys(
131+
keyMaterial,
132+
keyOptions.ratchetSalt,
133+
);
134+
await setKeySetFromMaterial(keySet, keyIndex);
135+
resetKeyStatus();
136+
}
137+
138+
Future<void> setKeySetFromMaterial(KeySet keySet, int keyIndex) async {
139+
logger.config('setKeySetFromMaterial: set new key, index: $keyIndex');
140+
if (keyIndex >= 0) {
141+
currentKeyIndex = keyIndex % cryptoKeyRing.length;
142+
}
143+
cryptoKeyRing[currentKeyIndex] = keySet;
144+
}
145+
146+
/// Derives a set of keys from the master key.
147+
/// See https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.1
148+
Future<KeySet> deriveKeys(CryptoKey material, Uint8List salt) async {
149+
var algorithmOptions =
150+
getAlgoOptions((material.algorithm as crypto.Algorithm).name, salt);
151+
152+
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#HKDF
153+
// https://developer.mozilla.org/en-US/docs/Web/API/HkdfParams
154+
var encryptionKey =
155+
await jsutil.promiseToFuture<CryptoKey>(crypto.deriveKey(
156+
jsutil.jsify(algorithmOptions),
157+
material,
158+
jsutil.jsify({'name': 'AES-GCM', 'length': 128}),
159+
false,
160+
['encrypt', 'decrypt'],
161+
));
162+
163+
return KeySet(material, encryptionKey);
164+
}
165+
166+
/// Ratchets a key. See
167+
/// https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
168+
169+
Future<Uint8List> ratchet(CryptoKey material, Uint8List salt) async {
170+
var algorithmOptions = getAlgoOptions('PBKDF2', salt);
171+
172+
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveBits
173+
var newKey = await jsutil.promiseToFuture<ByteBuffer>(
174+
crypto.deriveBits(jsutil.jsify(algorithmOptions), material, 256));
175+
return newKey.asUint8List();
176+
}
177+
}

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2024 LiveKit, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'package:logging/logging.dart';
16+
17+
enum LoggerLevel {
18+
kALL,
19+
kFINEST,
20+
kFINER,
21+
kFINE,
22+
kCONFIG,
23+
kINFO,
24+
kWARNING,
25+
kSEVERE,
26+
kSHOUT,
27+
kOFF
28+
}
29+
30+
final logger = Logger('E2EE.Worker');
31+
32+
/// disable logging
33+
void disableLogging() {
34+
logger.level = Level.OFF;
35+
}
36+
37+
/// set the logging level
38+
void setLoggingLevel(LoggerLevel level) {
39+
switch (level) {
40+
case LoggerLevel.kALL:
41+
logger.level = Level.ALL;
42+
break;
43+
case LoggerLevel.kFINEST:
44+
logger.level = Level.FINEST;
45+
break;
46+
case LoggerLevel.kFINER:
47+
logger.level = Level.FINER;
48+
break;
49+
case LoggerLevel.kFINE:
50+
logger.level = Level.FINE;
51+
break;
52+
case LoggerLevel.kCONFIG:
53+
logger.level = Level.CONFIG;
54+
break;
55+
case LoggerLevel.kINFO:
56+
logger.level = Level.INFO;
57+
break;
58+
case LoggerLevel.kWARNING:
59+
logger.level = Level.WARNING;
60+
break;
61+
case LoggerLevel.kSEVERE:
62+
logger.level = Level.SEVERE;
63+
break;
64+
case LoggerLevel.kSHOUT:
65+
logger.level = Level.SHOUT;
66+
break;
67+
case LoggerLevel.kOFF:
68+
logger.level = Level.OFF;
69+
break;
70+
}
71+
}
72+
73+
/// get the current logging level
74+
Level getLoggingLevel() {
75+
return logger.level;
76+
}
77+
78+
/// set a custom logging handler
79+
void setLoggingHandler(Function(LogRecord) handler) {
80+
logger.onRecord.listen(handler);
81+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const MAX_SIF_COUNT = 100;
2+
const MAX_SIF_DURATION = 2000;
3+
4+
class SifGuard {
5+
int consecutiveSifCount = 0;
6+
7+
int? sifSequenceStartedAt;
8+
9+
int lastSifReceivedAt = 0;
10+
11+
int userFramesSinceSif = 0;
12+
13+
void recordSif() {
14+
consecutiveSifCount += 1;
15+
sifSequenceStartedAt ??= DateTime.now().millisecondsSinceEpoch;
16+
lastSifReceivedAt = DateTime.now().millisecondsSinceEpoch;
17+
}
18+
19+
void recordUserFrame() {
20+
if (sifSequenceStartedAt == null) {
21+
return;
22+
} else {
23+
userFramesSinceSif += 1;
24+
}
25+
if (
26+
// reset if we received more user frames than SIFs
27+
userFramesSinceSif > consecutiveSifCount ||
28+
// also reset if we got a new user frame and the latest SIF frame hasn't been updated in a while
29+
DateTime.now().millisecondsSinceEpoch - lastSifReceivedAt >
30+
MAX_SIF_DURATION) {
31+
reset();
32+
}
33+
}
34+
35+
bool isSifAllowed() {
36+
return consecutiveSifCount < MAX_SIF_COUNT &&
37+
(sifSequenceStartedAt == null ||
38+
DateTime.now().millisecondsSinceEpoch - sifSequenceStartedAt! <
39+
MAX_SIF_DURATION);
40+
}
41+
42+
void reset() {
43+
userFramesSinceSif = 0;
44+
consecutiveSifCount = 0;
45+
sifSequenceStartedAt = null;
46+
}
47+
}

0 commit comments

Comments
 (0)