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