refactor: Upgrade to vodozemac cryptoutils

This commit is contained in:
Christian Kußowski 2025-09-30 10:07:19 +02:00
parent 03c5e354d8
commit fe94df97db
No known key found for this signature in database
GPG Key ID: E067ECD60F1A0652
17 changed files with 481 additions and 946 deletions

View File

@ -1,2 +1,2 @@
flutter_version=3.27.4 flutter_version=3.35.4
dart_version=3.6.2 dart_version=3.9.2

View File

@ -6,16 +6,13 @@ Matrix (matrix.org) SDK written in dart.
For E2EE, vodozemac must be provided. For E2EE, vodozemac must be provided.
Additionally, OpenSSL (libcrypto) must be provided on native platforms for E2EE. For flutter apps you can easily import it with the [flutter_vodozemac](https://pub.dev/packages/flutter_vodozemac) package.
For flutter apps you can easily import it with the [flutter_vodozemac](https://pub.dev/packages/flutter_vodozemac) and the [flutter_openssl_crypto](https://pub.dev/packages/flutter_openssl_crypto) packages.
```sh ```sh
flutter pub add matrix flutter pub add matrix
# Optional: For end to end encryption: # Optional: For end to end encryption:
flutter pub add flutter_vodozemac flutter pub add flutter_vodozemac
flutter pub add flutter_openssl_crypto
``` ```
## Get started ## Get started

View File

@ -6,12 +6,6 @@ For Flutter you can use [flutter_vodozemac](https://pub.dev/packages/flutter_vod
flutter pub add flutter_vodozemac flutter pub add flutter_vodozemac
``` ```
You also need [flutter_openssl_crypto](https://pub.dev/packages/flutter_openssl_crypto).
```sh
flutter pub add flutter_openssl_crypto
```
Now before you create your `Client`, init vodozemac: Now before you create your `Client`, init vodozemac:
```dart ```dart

View File

@ -12,7 +12,6 @@ In your `pubspec.yaml` file add the following dependencies:
# (Optional) For end to end encryption, please head on the # (Optional) For end to end encryption, please head on the
# encryption guide and add these dependencies: # encryption guide and add these dependencies:
flutter_vodozemac: <latest-version> flutter_vodozemac: <latest-version>
flutter_openssl_crypto: <latest-version>
``` ```
## Step 2: Create the client ## Step 2: Create the client

View File

@ -23,7 +23,7 @@ import 'dart:typed_data';
import 'package:base58check/base58.dart'; import 'package:base58check/base58.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart'; import 'package:vodozemac/vodozemac.dart';
import 'package:matrix/encryption/encryption.dart'; import 'package:matrix/encryption/encryption.dart';
import 'package:matrix/encryption/utils/base64_unpadded.dart'; import 'package:matrix/encryption/utils/base64_unpadded.dart';
@ -74,16 +74,18 @@ class SSSS {
static DerivedKeys deriveKeys(Uint8List key, String name) { static DerivedKeys deriveKeys(Uint8List key, String name) {
final zerosalt = Uint8List(8); final zerosalt = Uint8List(8);
final prk = Hmac(sha256, zerosalt).convert(key); final prk = CryptoUtils.hmac(key: zerosalt, input: key);
final b = Uint8List(1); final b = Uint8List(1);
b[0] = 1; b[0] = 1;
final aesKey = Hmac(sha256, prk.bytes).convert(utf8.encode(name) + b); final aesKey = CryptoUtils.hmac(key: prk, input: utf8.encode(name) + b);
b[0] = 2; b[0] = 2;
final hmacKey = final hmacKey = CryptoUtils.hmac(
Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b); key: prk,
input: aesKey + utf8.encode(name) + b,
);
return DerivedKeys( return DerivedKeys(
aesKey: Uint8List.fromList(aesKey.bytes), aesKey: Uint8List.fromList(aesKey),
hmacKey: Uint8List.fromList(hmacKey.bytes), hmacKey: Uint8List.fromList(hmacKey),
); );
} }
@ -105,14 +107,15 @@ class SSSS {
final keys = deriveKeys(key, name); final keys = deriveKeys(key, name);
final plain = Uint8List.fromList(utf8.encode(data)); final plain = Uint8List.fromList(utf8.encode(data));
final ciphertext = await uc.aesCtr.encrypt(plain, keys.aesKey, iv); final ciphertext =
CryptoUtils.aesCtr(input: plain, key: keys.aesKey, iv: iv);
final hmac = Hmac(sha256, keys.hmacKey).convert(ciphertext); final hmac = CryptoUtils.hmac(key: keys.hmacKey, input: ciphertext);
return EncryptedContent( return EncryptedContent(
iv: base64.encode(iv), iv: base64.encode(iv),
ciphertext: base64.encode(ciphertext), ciphertext: base64.encode(ciphertext),
mac: base64.encode(hmac.bytes), mac: base64.encode(hmac),
); );
} }
@ -124,13 +127,16 @@ class SSSS {
final keys = deriveKeys(key, name); final keys = deriveKeys(key, name);
final cipher = base64decodeUnpadded(data.ciphertext); final cipher = base64decodeUnpadded(data.ciphertext);
final hmac = base64 final hmac = base64
.encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes) .encode(CryptoUtils.hmac(key: keys.hmacKey, input: cipher))
.replaceAll(RegExp(r'=+$'), ''); .replaceAll(RegExp(r'=+$'), '');
if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) { if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) {
throw Exception('Bad MAC'); throw Exception('Bad MAC');
} }
final decipher = await uc.aesCtr final decipher = CryptoUtils.aesCtr(
.encrypt(cipher, keys.aesKey, base64decodeUnpadded(data.iv)); input: cipher,
key: keys.aesKey,
iv: base64decodeUnpadded(data.iv),
);
return String.fromCharCodes(decipher); return String.fromCharCodes(decipher);
} }
@ -184,12 +190,10 @@ class SSSS {
if (info.salt == null) { if (info.salt == null) {
throw InvalidPassphraseException('Passphrase info without salt'); throw InvalidPassphraseException('Passphrase info without salt');
} }
return await uc.pbkdf2( return CryptoUtils.pbkdf2(
Uint8List.fromList(utf8.encode(passphrase)), passphrase: Uint8List.fromList(utf8.encode(passphrase)),
Uint8List.fromList(utf8.encode(info.salt!)), salt: Uint8List.fromList(utf8.encode(info.salt!)),
uc.sha512, iterations: info.iterations!,
info.iterations!,
info.bits ?? 256,
); );
} }
@ -742,7 +746,7 @@ class OpenSSSS {
info: keyData.passphrase!, info: keyData.passphrase!,
), ),
), ),
).timeout(Duration(seconds: 10)); ).timeout(Duration(minutes: 2));
} else if (recoveryKey != null) { } else if (recoveryKey != null) {
privateKey = SSSS.decodeRecoveryKey(recoveryKey); privateKey = SSSS.decodeRecoveryKey(recoveryKey);
} else { } else {

View File

@ -21,7 +21,6 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:canonical_json/canonical_json.dart'; import 'package:canonical_json/canonical_json.dart';
import 'package:crypto/crypto.dart' as crypto;
import 'package:typed_data/typed_data.dart'; import 'package:typed_data/typed_data.dart';
import 'package:vodozemac/vodozemac.dart' as vod; import 'package:vodozemac/vodozemac.dart' as vod;
@ -1559,8 +1558,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
Future<String> _makeCommitment(String pubKey, String canonicalJson) async { Future<String> _makeCommitment(String pubKey, String canonicalJson) async {
if (hash == 'sha256') { if (hash == 'sha256') {
final bytes = utf8.encoder.convert(pubKey + canonicalJson); final bytes = utf8.encoder.convert(pubKey + canonicalJson);
final digest = crypto.sha256.convert(bytes); final digest = vod.CryptoUtils.sha256(input: bytes);
return encodeBase64Unpadded(digest.bytes); return encodeBase64Unpadded(digest);
} }
throw Exception('Unknown hash method'); throw Exception('Unknown hash method');
} }

View File

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export 'native.dart' if (dart.library.js_interop) 'js.dart';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';

View File

@ -19,6 +19,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:vodozemac/vodozemac.dart';
import 'package:matrix/encryption/utils/base64_unpadded.dart'; import 'package:matrix/encryption/utils/base64_unpadded.dart';
import 'package:matrix/src/utils/crypto/crypto.dart'; import 'package:matrix/src/utils/crypto/crypto.dart';
@ -38,8 +40,8 @@ class EncryptedFile {
Future<EncryptedFile> encryptFile(Uint8List input) async { Future<EncryptedFile> encryptFile(Uint8List input) async {
final key = secureRandomBytes(32); final key = secureRandomBytes(32);
final iv = secureRandomBytes(16); final iv = secureRandomBytes(16);
final data = await aesCtr.encrypt(input, key, iv); final data = CryptoUtils.aesCtr(input: input, key: key, iv: iv);
final hash = await sha256(data); final hash = CryptoUtils.sha256(input: data);
return EncryptedFile( return EncryptedFile(
data: data, data: data,
k: base64Url.encode(key).replaceAll('=', ''), k: base64Url.encode(key).replaceAll('=', ''),
@ -51,12 +53,12 @@ Future<EncryptedFile> encryptFile(Uint8List input) async {
/// you would likely want to use [NativeImplementations] and /// you would likely want to use [NativeImplementations] and
/// [Client.nativeImplementations] instead /// [Client.nativeImplementations] instead
Future<Uint8List?> decryptFileImplementation(EncryptedFile input) async { Future<Uint8List?> decryptFileImplementation(EncryptedFile input) async {
if (base64.encode(await sha256(input.data)) != if (base64.encode(CryptoUtils.sha256(input: input.data)) !=
base64.normalize(input.sha256)) { base64.normalize(input.sha256)) {
return null; return null;
} }
final key = base64decodeUnpadded(base64.normalize(input.k)); final key = base64decodeUnpadded(base64.normalize(input.k));
final iv = base64decodeUnpadded(base64.normalize(input.iv)); final iv = base64decodeUnpadded(base64.normalize(input.iv));
return await aesCtr.encrypt(input.data, key, iv); return CryptoUtils.aesCtr(input: input.data, key: key, iv: iv);
} }

View File

@ -1,164 +0,0 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import 'dart:ffi';
import 'dart:io';
final libcrypto = () {
if (Platform.isIOS) {
return DynamicLibrary.process();
} else if (Platform.isAndroid) {
return DynamicLibrary.open('libcrypto.so');
} else if (Platform.isWindows) {
return DynamicLibrary.open('libcrypto.dll');
} else if (Platform.isMacOS) {
try {
return DynamicLibrary.open('libcrypto.3.dylib');
} catch (_) {
return DynamicLibrary.open('libcrypto.1.1.dylib');
}
} else {
try {
return DynamicLibrary.open('libcrypto.so.3');
} catch (_) {
return DynamicLibrary.open('libcrypto.so.1.1');
}
}
}();
final PKCS5_PBKDF2_HMAC = libcrypto.lookupFunction<
IntPtr Function(
Pointer<Uint8> pass,
IntPtr passlen,
Pointer<Uint8> salt,
IntPtr saltlen,
IntPtr iter,
Pointer<NativeType> digest,
IntPtr keylen,
Pointer<Uint8> out,
),
int Function(
Pointer<Uint8> pass,
int passlen,
Pointer<Uint8> salt,
int saltlen,
int iter,
Pointer<NativeType> digest,
int keylen,
Pointer<Uint8> out,
)>('PKCS5_PBKDF2_HMAC');
final EVP_sha1 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
Pointer<NativeType> Function()>('EVP_sha1');
final EVP_sha256 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
Pointer<NativeType> Function()>('EVP_sha256');
final EVP_sha512 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
Pointer<NativeType> Function()>('EVP_sha512');
final EVP_aes_128_ctr = libcrypto.lookupFunction<Pointer<NativeType> Function(),
Pointer<NativeType> Function()>('EVP_aes_128_ctr');
final EVP_aes_256_ctr = libcrypto.lookupFunction<Pointer<NativeType> Function(),
Pointer<NativeType> Function()>('EVP_aes_256_ctr');
final EVP_CIPHER_CTX_new = libcrypto.lookupFunction<
Pointer<NativeType> Function(),
Pointer<NativeType> Function()>('EVP_CIPHER_CTX_new');
final EVP_EncryptInit_ex = libcrypto.lookupFunction<
Pointer<NativeType> Function(
Pointer<NativeType> ctx,
Pointer<NativeType> alg,
Pointer<NativeType> some,
Pointer<Uint8> key,
Pointer<Uint8> iv,
),
Pointer<NativeType> Function(
Pointer<NativeType> ctx,
Pointer<NativeType> alg,
Pointer<NativeType> some,
Pointer<Uint8> key,
Pointer<Uint8> iv,
)>('EVP_EncryptInit_ex');
final EVP_EncryptUpdate = libcrypto.lookupFunction<
Pointer<NativeType> Function(
Pointer<NativeType> ctx,
Pointer<Uint8> output,
Pointer<IntPtr> outputLen,
Pointer<Uint8> input,
IntPtr inputLen,
),
Pointer<NativeType> Function(
Pointer<NativeType> ctx,
Pointer<Uint8> output,
Pointer<IntPtr> outputLen,
Pointer<Uint8> input,
int inputLen,
)>('EVP_EncryptUpdate');
final EVP_EncryptFinal_ex = libcrypto.lookupFunction<
Pointer<NativeType> Function(
Pointer<NativeType> ctx,
Pointer<Uint8> data,
Pointer<IntPtr> len,
),
Pointer<NativeType> Function(
Pointer<NativeType> ctx,
Pointer<Uint8> data,
Pointer<IntPtr> len,
)>('EVP_EncryptFinal_ex');
final EVP_CIPHER_CTX_free = libcrypto.lookupFunction<
Pointer<NativeType> Function(Pointer<NativeType> ctx),
Pointer<NativeType> Function(
Pointer<NativeType> ctx,
)>('EVP_CIPHER_CTX_free');
final EVP_Digest = libcrypto.lookupFunction<
IntPtr Function(
Pointer<Uint8> data,
IntPtr len,
Pointer<Uint8> hash,
Pointer<IntPtr> hsize,
Pointer<NativeType> alg,
Pointer<NativeType> engine,
),
int Function(
Pointer<Uint8> data,
int len,
Pointer<Uint8> hash,
Pointer<IntPtr> hsize,
Pointer<NativeType> alg,
Pointer<NativeType> engine,
)>('EVP_Digest');
final EVP_MD_size = () {
// EVP_MD_size was renamed to EVP_MD_get_size in Openssl3.0.
// There is an alias macro, but those don't exist in libraries.
// Try loading the new name first, then fall back to the old one if not found.
try {
return libcrypto.lookupFunction<IntPtr Function(Pointer<NativeType> ctx),
int Function(Pointer<NativeType> ctx)>('EVP_MD_get_size');
} catch (e) {
return libcrypto.lookupFunction<IntPtr Function(Pointer<NativeType> ctx),
int Function(Pointer<NativeType> ctx)>('EVP_MD_size');
}
}();

View File

@ -1,77 +0,0 @@
// Copyright (c) 2020 Famedly GmbH
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'dart:typed_data';
import 'package:matrix/src/utils/crypto/subtle.dart' as subtle;
import 'package:matrix/src/utils/crypto/subtle.dart';
abstract class Hash {
Hash._(this.name);
String name;
Future<Uint8List> call(Uint8List input) async =>
Uint8List.view(await digest(name, input));
}
final Hash sha1 = _Sha1();
final Hash sha256 = _Sha256();
final Hash sha512 = _Sha512();
class _Sha1 extends Hash {
_Sha1() : super._('SHA-1');
}
class _Sha256 extends Hash {
_Sha256() : super._('SHA-256');
}
class _Sha512 extends Hash {
_Sha512() : super._('SHA-512');
}
abstract class Cipher {
Cipher._(this.name);
String name;
Object params(Uint8List iv);
Future<Uint8List> encrypt(
Uint8List input,
Uint8List key,
Uint8List iv,
) async {
final subtleKey = await importKey('raw', key, name, false, ['encrypt']);
return (await subtle.encrypt(params(iv), subtleKey, input)).asUint8List();
}
}
final Cipher aesCtr = _AesCtr();
class _AesCtr extends Cipher {
_AesCtr() : super._('AES-CTR');
@override
Object params(Uint8List iv) =>
AesCtrParams(name: name, counter: iv, length: 64);
}
Future<Uint8List> pbkdf2(
Uint8List passphrase,
Uint8List salt,
Hash hash,
int iterations,
int bits,
) async {
final raw =
await importKey('raw', passphrase, 'PBKDF2', false, ['deriveBits']);
final res = await deriveBits(
Pbkdf2Params(
name: 'PBKDF2',
hash: hash.name,
salt: salt,
iterations: iterations,
),
raw,
bits,
);
return Uint8List.view(res);
}

View File

@ -1,120 +0,0 @@
// ignore_for_file: deprecated_member_use
// ignoring the elementAt deprecation because this would make the SDK
// incompatible with older flutter versions than 3.19.0 or dart 3.3.0
import 'dart:async';
import 'dart:ffi';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'package:matrix/src/utils/crypto/ffi.dart';
abstract class Hash {
Hash._(this.ptr);
Pointer<NativeType> ptr;
FutureOr<Uint8List> call(Uint8List data) {
final outSize = EVP_MD_size(ptr);
final mem = malloc.call<Uint8>(outSize + data.length);
final dataMem = mem.elementAt(outSize);
try {
dataMem.asTypedList(data.length).setAll(0, data);
EVP_Digest(dataMem, data.length, mem, nullptr, ptr, nullptr);
return Uint8List.fromList(mem.asTypedList(outSize));
} finally {
malloc.free(mem);
}
}
}
final Hash sha1 = _Sha1();
final Hash sha256 = _Sha256();
final Hash sha512 = _Sha512();
class _Sha1 extends Hash {
_Sha1() : super._(EVP_sha1());
}
class _Sha256 extends Hash {
_Sha256() : super._(EVP_sha256());
}
class _Sha512 extends Hash {
_Sha512() : super._(EVP_sha512());
}
abstract class Cipher {
Cipher._();
Pointer<NativeType> getAlg(int keysize);
FutureOr<Uint8List> encrypt(Uint8List input, Uint8List key, Uint8List iv) {
final alg = getAlg(key.length * 8);
final mem = malloc
.call<Uint8>(sizeOf<IntPtr>() + key.length + iv.length + input.length);
final lenMem = mem.cast<IntPtr>();
final keyMem = mem.elementAt(sizeOf<IntPtr>());
final ivMem = keyMem.elementAt(key.length);
final dataMem = ivMem.elementAt(iv.length);
try {
keyMem.asTypedList(key.length).setAll(0, key);
ivMem.asTypedList(iv.length).setAll(0, iv);
dataMem.asTypedList(input.length).setAll(0, input);
final ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx, alg, nullptr, keyMem, ivMem);
EVP_EncryptUpdate(ctx, dataMem, lenMem, dataMem, input.length);
EVP_EncryptFinal_ex(ctx, dataMem.elementAt(lenMem.value), lenMem);
EVP_CIPHER_CTX_free(ctx);
return Uint8List.fromList(dataMem.asTypedList(input.length));
} finally {
malloc.free(mem);
}
}
}
final Cipher aesCtr = _AesCtr();
class _AesCtr extends Cipher {
_AesCtr() : super._();
@override
Pointer<NativeType> getAlg(int keysize) {
switch (keysize) {
case 128:
return EVP_aes_128_ctr();
case 256:
return EVP_aes_256_ctr();
default:
throw ArgumentError('invalid key size');
}
}
}
FutureOr<Uint8List> pbkdf2(
Uint8List passphrase,
Uint8List salt,
Hash hash,
int iterations,
int bits,
) {
final outLen = bits ~/ 8;
final mem = malloc.call<Uint8>(passphrase.length + salt.length + outLen);
final saltMem = mem.elementAt(passphrase.length);
final outMem = saltMem.elementAt(salt.length);
try {
mem.asTypedList(passphrase.length).setAll(0, passphrase);
saltMem.asTypedList(salt.length).setAll(0, salt);
PKCS5_PBKDF2_HMAC(
mem,
passphrase.length,
saltMem,
salt.length,
iterations,
hash.ptr,
outLen,
outMem,
);
return Uint8List.fromList(outMem.asTypedList(outLen));
} finally {
malloc.free(mem);
}
}

View File

@ -1,119 +0,0 @@
// Copyright (c) 2020 Famedly GmbH
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'dart:async';
import 'dart:js_util';
import 'dart:typed_data';
import 'package:js/js.dart';
@JS()
@anonymous
class Pbkdf2Params {
external factory Pbkdf2Params({
String name,
String hash,
Uint8List salt,
int iterations,
});
String? name;
String? hash;
Uint8List? salt;
int? iterations;
}
@JS()
@anonymous
class AesCtrParams {
external factory AesCtrParams({
String name,
Uint8List counter,
int length,
});
String? name;
Uint8List? counter;
int? length;
}
@JS('crypto.subtle.encrypt')
external dynamic _encrypt(dynamic algorithm, dynamic key, Uint8List data);
Future<ByteBuffer> encrypt(dynamic algorithm, dynamic key, Uint8List data) {
return promiseToFuture(_encrypt(algorithm, key, data));
}
@JS('crypto.subtle.decrypt')
external dynamic _decrypt(dynamic algorithm, dynamic key, Uint8List data);
Future<ByteBuffer> decrypt(dynamic algorithm, dynamic key, Uint8List data) {
return promiseToFuture(_decrypt(algorithm, key, data));
}
@JS('crypto.subtle.importKey')
external dynamic _importKey(
String format,
dynamic keyData,
dynamic algorithm,
bool extractable,
List<String> keyUsages,
);
Future<dynamic> importKey(
String format,
dynamic keyData,
dynamic algorithm,
bool extractable,
List<String> keyUsages,
) {
return promiseToFuture(
_importKey(format, keyData, algorithm, extractable, keyUsages),
);
}
@JS('crypto.subtle.exportKey')
external dynamic _exportKey(String algorithm, dynamic key);
Future<dynamic> exportKey(String algorithm, dynamic key) {
return promiseToFuture(_exportKey(algorithm, key));
}
@JS('crypto.subtle.deriveKey')
external dynamic _deriveKey(
dynamic algorithm,
dynamic baseKey,
dynamic derivedKeyAlgorithm,
bool extractable,
List<String> keyUsages,
);
Future<ByteBuffer> deriveKey(
dynamic algorithm,
dynamic baseKey,
dynamic derivedKeyAlgorithm,
bool extractable,
List<String> keyUsages,
) {
return promiseToFuture(
_deriveKey(
algorithm,
baseKey,
derivedKeyAlgorithm,
extractable,
keyUsages,
),
);
}
@JS('crypto.subtle.deriveBits')
external dynamic _deriveBits(dynamic algorithm, dynamic baseKey, int length);
Future<ByteBuffer> deriveBits(dynamic algorithm, dynamic baseKey, int length) {
return promiseToFuture(_deriveBits(algorithm, baseKey, length));
}
@JS('crypto.subtle.digest')
external dynamic _digest(String algorithm, Uint8List data);
Future<ByteBuffer> digest(String algorithm, Uint8List data) {
return promiseToFuture(_digest(algorithm, data));
}

View File

@ -155,7 +155,10 @@ class NativeImplementationsIsolate extends NativeImplementations {
bool retryInDummy = true, bool retryInDummy = true,
}) { }) {
return runInBackground<Uint8List?, EncryptedFile>( return runInBackground<Uint8List?, EncryptedFile>(
NativeImplementations.dummy.decryptFile, (EncryptedFile args) async {
await vodozemacInit?.call();
return NativeImplementations.dummy.decryptFile(args);
},
file, file,
); );
} }
@ -180,7 +183,10 @@ class NativeImplementationsIsolate extends NativeImplementations {
bool retryInDummy = true, bool retryInDummy = true,
}) { }) {
return runInBackground<Uint8List, KeyFromPassphraseArgs>( return runInBackground<Uint8List, KeyFromPassphraseArgs>(
NativeImplementations.dummy.keyFromPassphrase, (KeyFromPassphraseArgs args) async {
await vodozemacInit?.call();
return NativeImplementations.dummy.keyFromPassphrase(args);
},
args, args,
); );
} }

View File

@ -14,14 +14,10 @@ dependencies:
blurhash_dart: ^1.1.0 blurhash_dart: ^1.1.0
canonical_json: ^1.1.0 canonical_json: ^1.1.0
collection: ^1.15.0 collection: ^1.15.0
crypto: ^3.0.0
enhanced_enum: ^0.2.4
ffi: ^2.0.0
html: ^0.15.0 html: ^0.15.0
html_unescape: ^2.0.0 html_unescape: ^2.0.0
http: ">=0.13.0 <2.0.0" http: ">=0.13.0 <2.0.0"
image: ^4.0.15 image: ^4.0.15
js: ^0.6.3
js_interop: ^0.0.1 js_interop: ^0.0.1
markdown: ^7.1.1 markdown: ^7.1.1
mime: ">=1.0.0 <3.0.0" mime: ">=1.0.0 <3.0.0"
@ -32,7 +28,7 @@ dependencies:
sqflite_common: ^2.4.5 sqflite_common: ^2.4.5
sqlite3: ^2.1.0 sqlite3: ^2.1.0
typed_data: ^1.3.2 typed_data: ^1.3.2
vodozemac: ^0.2.0 vodozemac: ^0.3.0
web: ^1.1.1 web: ^1.1.1
webrtc_interface: ^1.2.0 webrtc_interface: ^1.2.0
@ -43,4 +39,4 @@ dev_dependencies:
import_sorter: ^4.6.0 import_sorter: ^4.6.0
lints: ^5.0.0 lints: ^5.0.0
sqflite_common_ffi: ^2.3.4+4 # sqflite_common_ffi aggressively requires newer dart versions sqflite_common_ffi: ^2.3.4+4 # sqflite_common_ffi aggressively requires newer dart versions
test: ^1.25.13 test: ^1.25.13

View File

@ -1,7 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
rm -rf rust rm -rf rust
git clone https://github.com/famedly/dart-vodozemac.git version=$(yq ".dependencies.vodozemac" < pubspec.yaml)
version=$(expr "$version" : '\^*\(.*\)')
git clone https://github.com/famedly/dart-vodozemac.git -b ${version}
mv ./dart-vodozemac/rust ./ mv ./dart-vodozemac/rust ./
rm -rf dart-vodozemac rm -rf dart-vodozemac
cd ./rust cd ./rust

View File

@ -48,462 +48,476 @@ class MockSSSS extends SSSS {
} }
void main() { void main() {
group('SSSS', tags: 'olm', () { group(
Logs().level = Level.error; 'SSSS',
tags: 'olm',
() {
Logs().level = Level.error;
late Client client; late Client client;
setUpAll(() async { setUpAll(() async {
await vod.init( await vod.init(
wasmPath: './pkg/', wasmPath: './pkg/',
libraryPath: './rust/target/debug/', libraryPath: './rust/target/debug/',
); );
client = await getClient(); client = await getClient();
});
test('basic things', () async {
expect(
client.encryption!.ssss.defaultKeyId,
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3',
);
});
test('encrypt / decrypt', () async {
final key = Uint8List.fromList(secureRandomBytes(32));
final enc = await SSSS.encryptAes('secret foxies', key, 'name');
final dec = await SSSS.decryptAes(enc, key, 'name');
expect(dec, 'secret foxies');
});
test('store', () async {
final handle = client.encryption!.ssss.open();
var failed = false;
try {
await handle.unlock(passphrase: 'invalid');
} catch (_) {
failed = true;
}
expect(failed, true);
expect(handle.isUnlocked, false);
failed = false;
try {
await handle.unlock(recoveryKey: 'invalid');
} catch (_) {
failed = true;
}
expect(failed, true);
expect(handle.isUnlocked, false);
await handle.unlock(passphrase: ssssPassphrase);
await handle.unlock(recoveryKey: ssssKey);
expect(handle.isUnlocked, true);
FakeMatrixApi.calledEndpoints.clear();
// OpenSSSS store waits for accountdata to be updated before returning
// but we can't update that before the below endpoint is not hit.
await handle.ssss
.store('best animal', 'foxies', handle.keyId, handle.privateKey!);
final content = FakeMatrixApi
.calledEndpoints[
'/client/v3/user/%40test%3AfakeServer.notExisting/account_data/best%20animal']!
.first;
client.accountData['best animal'] = BasicEvent.fromJson({
'type': 'best animal',
'content': json.decode(content),
}); });
expect(await handle.getStored('best animal'), 'foxies');
});
test('encode / decode recovery key', () async { test('basic things', () async {
final key = Uint8List.fromList(secureRandomBytes(32)); expect(
final encoded = SSSS.encodeRecoveryKey(key); client.encryption!.ssss.defaultKeyId,
var decoded = SSSS.decodeRecoveryKey(encoded); '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3',
expect(key, decoded); );
});
decoded = SSSS.decodeRecoveryKey('$encoded \n\t'); test('encrypt / decrypt', () async {
expect(key, decoded); final key = Uint8List.fromList(secureRandomBytes(32));
final handle = client.encryption!.ssss.open(); final enc = await SSSS.encryptAes('secret foxies', key, 'name');
await handle.unlock(recoveryKey: ssssKey); final dec = await SSSS.decryptAes(enc, key, 'name');
expect(handle.recoveryKey, ssssKey); expect(dec, 'secret foxies');
}); });
test('cache', () async { test('store', () async {
await client.encryption!.ssss.clearCache(); final handle = client.encryption!.ssss.open();
final handle = var failed = false;
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning); try {
await handle.unlock(recoveryKey: ssssKey, postUnlock: false); await handle.unlock(passphrase: 'invalid');
expect( } catch (_) {
(await client.encryption!.ssss failed = true;
.getCached(EventTypes.CrossSigningSelfSigning)) != }
null, expect(failed, true);
false, expect(handle.isUnlocked, false);
); failed = false;
expect( try {
(await client.encryption!.ssss await handle.unlock(recoveryKey: 'invalid');
.getCached(EventTypes.CrossSigningUserSigning)) != } catch (_) {
null, failed = true;
false, }
); expect(failed, true);
await handle.getStored(EventTypes.CrossSigningSelfSigning); expect(handle.isUnlocked, false);
expect( await handle.unlock(passphrase: ssssPassphrase);
(await client.encryption!.ssss await handle.unlock(recoveryKey: ssssKey);
.getCached(EventTypes.CrossSigningSelfSigning)) != expect(handle.isUnlocked, true);
null, FakeMatrixApi.calledEndpoints.clear();
true,
);
await handle.maybeCacheAll();
expect(
(await client.encryption!.ssss
.getCached(EventTypes.CrossSigningUserSigning)) !=
null,
true,
);
expect(
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
null,
true,
);
});
test('postUnlock', () async { // OpenSSSS store waits for accountdata to be updated before returning
await client.encryption!.ssss.clearCache(); // but we can't update that before the below endpoint is not hit.
client.userDeviceKeys[client.userID!]!.masterKey! await handle.ssss
.setDirectVerified(false); .store('best animal', 'foxies', handle.keyId, handle.privateKey!);
final handle =
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
await handle.unlock(recoveryKey: ssssKey);
expect(
(await client.encryption!.ssss
.getCached(EventTypes.CrossSigningSelfSigning)) !=
null,
true,
);
expect(
(await client.encryption!.ssss
.getCached(EventTypes.CrossSigningUserSigning)) !=
null,
true,
);
expect(
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
null,
true,
);
expect(
client.userDeviceKeys[client.userID!]!.masterKey!.directVerified,
true,
);
});
test('make share requests', () async { final content = FakeMatrixApi
final key = .calledEndpoints[
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!; '/client/v3/user/%40test%3AfakeServer.notExisting/account_data/best%20animal']!
key.setDirectVerified(true); .first;
FakeMatrixApi.calledEndpoints.clear(); client.accountData['best animal'] = BasicEvent.fromJson({
await client.encryption!.ssss.request('some.type', [key]); 'type': 'best animal',
expect( 'content': json.decode(content),
FakeMatrixApi.calledEndpoints.keys.any( });
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'), expect(await handle.getStored('best animal'), 'foxies');
), });
true,
);
});
test('answer to share requests', () async { test('encode / decode recovery key', () async {
var event = ToDeviceEvent( final key = Uint8List.fromList(secureRandomBytes(32));
sender: client.userID!, final encoded = SSSS.encodeRecoveryKey(key);
type: 'm.secret.request', var decoded = SSSS.decodeRecoveryKey(encoded);
content: { expect(key, decoded);
'action': 'request',
'requesting_device_id': 'OTHERDEVICE',
'name': EventTypes.CrossSigningSelfSigning,
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
),
true,
);
// now test some fail scenarios decoded = SSSS.decodeRecoveryKey('$encoded \n\t');
expect(key, decoded);
// not by us final handle = client.encryption!.ssss.open();
event = ToDeviceEvent( await handle.unlock(recoveryKey: ssssKey);
sender: '@someotheruser:example.org', expect(handle.recoveryKey, ssssKey);
type: 'm.secret.request', });
content: {
'action': 'request',
'requesting_device_id': 'OTHERDEVICE',
'name': EventTypes.CrossSigningSelfSigning,
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
),
false,
);
// secret not cached test('cache', () async {
event = ToDeviceEvent( await client.encryption!.ssss.clearCache();
sender: client.userID!, final handle =
type: 'm.secret.request', client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
content: { await handle.unlock(recoveryKey: ssssKey, postUnlock: false);
'action': 'request', expect(
'requesting_device_id': 'OTHERDEVICE', (await client.encryption!.ssss
'name': 'm.unknown.secret', .getCached(EventTypes.CrossSigningSelfSigning)) !=
'request_id': '1', null,
}, false,
); );
FakeMatrixApi.calledEndpoints.clear(); expect(
await client.encryption!.ssss.handleToDeviceEvent(event); (await client.encryption!.ssss
expect( .getCached(EventTypes.CrossSigningUserSigning)) !=
FakeMatrixApi.calledEndpoints.keys.any( null,
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'), false,
), );
false, await handle.getStored(EventTypes.CrossSigningSelfSigning);
); expect(
(await client.encryption!.ssss
.getCached(EventTypes.CrossSigningSelfSigning)) !=
null,
true,
);
await handle.maybeCacheAll();
expect(
(await client.encryption!.ssss
.getCached(EventTypes.CrossSigningUserSigning)) !=
null,
true,
);
expect(
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
null,
true,
);
});
// is a cancelation test('postUnlock', () async {
event = ToDeviceEvent( await client.encryption!.ssss.clearCache();
sender: client.userID!, client.userDeviceKeys[client.userID!]!.masterKey!
type: 'm.secret.request', .setDirectVerified(false);
content: { final handle =
'action': 'request_cancellation', client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
'requesting_device_id': 'OTHERDEVICE', await handle.unlock(recoveryKey: ssssKey);
'name': EventTypes.CrossSigningSelfSigning, expect(
'request_id': '1', (await client.encryption!.ssss
}, .getCached(EventTypes.CrossSigningSelfSigning)) !=
); null,
FakeMatrixApi.calledEndpoints.clear(); true,
await client.encryption!.ssss.handleToDeviceEvent(event); );
expect( expect(
FakeMatrixApi.calledEndpoints.keys.any( (await client.encryption!.ssss
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'), .getCached(EventTypes.CrossSigningUserSigning)) !=
), null,
false, true,
); );
expect(
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
null,
true,
);
expect(
client.userDeviceKeys[client.userID!]!.masterKey!.directVerified,
true,
);
});
// device not verified test('make share requests', () async {
final key = final key =
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!; client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
key.setDirectVerified(false); key.setDirectVerified(true);
client.userDeviceKeys[client.userID!]!.masterKey! FakeMatrixApi.calledEndpoints.clear();
.setDirectVerified(false); await client.encryption!.ssss.request('some.type', [key]);
event = ToDeviceEvent( expect(
sender: client.userID!, FakeMatrixApi.calledEndpoints.keys.any(
type: 'm.secret.request', (k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
content: { ),
'action': 'request', true,
'requesting_device_id': 'OTHERDEVICE', );
'name': EventTypes.CrossSigningSelfSigning, });
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
),
false,
);
key.setDirectVerified(true);
});
test('receive share requests', () async { test('answer to share requests', () async {
final key = var event = ToDeviceEvent(
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!; sender: client.userID!,
key.setDirectVerified(true); type: 'm.secret.request',
final handle = content: {
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning); 'action': 'request',
await handle.unlock(recoveryKey: ssssKey); 'requesting_device_id': 'OTHERDEVICE',
'name': EventTypes.CrossSigningSelfSigning,
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
),
true,
);
await client.encryption!.ssss.clearCache(); // now test some fail scenarios
client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request('best animal', [key]); // not by us
var event = ToDeviceEvent( event = ToDeviceEvent(
sender: client.userID!, sender: '@someotheruser:example.org',
type: 'm.secret.send', type: 'm.secret.request',
content: { content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first, 'action': 'request',
'secret': 'foxies!', 'requesting_device_id': 'OTHERDEVICE',
}, 'name': EventTypes.CrossSigningSelfSigning,
encryptedContent: { 'request_id': '1',
'sender_key': key.curve25519Key, },
}, );
); FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event); await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached('best animal'), 'foxies!'); expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
),
false,
);
// secret not cached
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.request',
content: {
'action': 'request',
'requesting_device_id': 'OTHERDEVICE',
'name': 'm.unknown.secret',
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
),
false,
);
// is a cancelation
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.request',
content: {
'action': 'request_cancellation',
'requesting_device_id': 'OTHERDEVICE',
'name': EventTypes.CrossSigningSelfSigning,
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
),
false,
);
// device not verified
final key =
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
key.setDirectVerified(false);
client.userDeviceKeys[client.userID!]!.masterKey!
.setDirectVerified(false);
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.request',
content: {
'action': 'request',
'requesting_device_id': 'OTHERDEVICE',
'name': EventTypes.CrossSigningSelfSigning,
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
),
false,
);
key.setDirectVerified(true);
});
test('receive share requests', () async {
final key =
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
key.setDirectVerified(true);
final handle =
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
await handle.unlock(recoveryKey: ssssKey);
// test the different validators
for (final type in [
EventTypes.CrossSigningSelfSigning,
EventTypes.CrossSigningUserSigning,
EventTypes.MegolmBackup,
]) {
final secret = await handle.getStored(type);
await client.encryption!.ssss.clearCache(); await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear(); client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request(type, [key]); await client.encryption!.ssss.request('best animal', [key]);
event = ToDeviceEvent( var event = ToDeviceEvent(
sender: client.userID!, sender: client.userID!,
type: 'm.secret.send', type: 'm.secret.send',
content: { content: {
'request_id': 'request_id':
client.encryption!.ssss.pendingShareRequests.keys.first, client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': secret, 'secret': 'foxies!',
}, },
encryptedContent: { encryptedContent: {
'sender_key': key.curve25519Key, 'sender_key': key.curve25519Key,
}, },
); );
await client.encryption!.ssss.handleToDeviceEvent(event); await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached(type), secret); expect(
} await client.encryption!.ssss.getCached('best animal'),
'foxies!',
);
// test different fail scenarios // test the different validators
for (final type in [
EventTypes.CrossSigningSelfSigning,
EventTypes.CrossSigningUserSigning,
EventTypes.MegolmBackup,
]) {
final secret = await handle.getStored(type);
await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request(type, [key]);
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id':
client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': secret,
},
encryptedContent: {
'sender_key': key.curve25519Key,
},
);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached(type), secret);
}
// not encrypted // test different fail scenarios
await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request('best animal', [key]);
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 'foxies!',
},
);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached('best animal'), null);
// unknown request id // not encrypted
await client.encryption!.ssss.clearCache(); await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear(); client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request('best animal', [key]); await client.encryption!.ssss.request('best animal', [key]);
event = ToDeviceEvent( event = ToDeviceEvent(
sender: client.userID!, sender: client.userID!,
type: 'm.secret.send', type: 'm.secret.send',
content: { content: {
'request_id': 'invalid', 'request_id':
'secret': 'foxies!', client.encryption!.ssss.pendingShareRequests.keys.first,
}, 'secret': 'foxies!',
encryptedContent: { },
'sender_key': key.curve25519Key, );
}, await client.encryption!.ssss.handleToDeviceEvent(event);
); expect(await client.encryption!.ssss.getCached('best animal'), null);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached('best animal'), null);
// not from a device we sent the request to // unknown request id
await client.encryption!.ssss.clearCache(); await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear(); client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request('best animal', [key]); await client.encryption!.ssss.request('best animal', [key]);
event = ToDeviceEvent( event = ToDeviceEvent(
sender: client.userID!, sender: client.userID!,
type: 'm.secret.send', type: 'm.secret.send',
content: { content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first, 'request_id': 'invalid',
'secret': 'foxies!', 'secret': 'foxies!',
}, },
encryptedContent: { encryptedContent: {
'sender_key': 'invalid', 'sender_key': key.curve25519Key,
}, },
); );
await client.encryption!.ssss.handleToDeviceEvent(event); await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached('best animal'), null); expect(await client.encryption!.ssss.getCached('best animal'), null);
// secret not a string // not from a device we sent the request to
await client.encryption!.ssss.clearCache(); await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear(); client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request('best animal', [key]); await client.encryption!.ssss.request('best animal', [key]);
event = ToDeviceEvent( event = ToDeviceEvent(
sender: client.userID!, sender: client.userID!,
type: 'm.secret.send', type: 'm.secret.send',
content: { content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first, 'request_id':
'secret': 42, client.encryption!.ssss.pendingShareRequests.keys.first,
}, 'secret': 'foxies!',
encryptedContent: { },
'sender_key': key.curve25519Key, encryptedContent: {
}, 'sender_key': 'invalid',
); },
await client.encryption!.ssss.handleToDeviceEvent(event); );
expect(await client.encryption!.ssss.getCached('best animal'), null); await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached('best animal'), null);
// validator doesn't check out // secret not a string
await client.encryption!.ssss.clearCache(); await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear(); client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request(EventTypes.MegolmBackup, [key]); await client.encryption!.ssss.request('best animal', [key]);
event = ToDeviceEvent( event = ToDeviceEvent(
sender: client.userID!, sender: client.userID!,
type: 'm.secret.send', type: 'm.secret.send',
content: { content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first, 'request_id':
'secret': 'foxies!', client.encryption!.ssss.pendingShareRequests.keys.first,
}, 'secret': 42,
encryptedContent: { },
'sender_key': key.curve25519Key, encryptedContent: {
}, 'sender_key': key.curve25519Key,
); },
await client.encryption!.ssss.handleToDeviceEvent(event); );
expect( await client.encryption!.ssss.handleToDeviceEvent(event);
await client.encryption!.ssss.getCached(EventTypes.MegolmBackup), expect(await client.encryption!.ssss.getCached('best animal'), null);
null,
);
});
test('request all', () async { // validator doesn't check out
final key = await client.encryption!.ssss.clearCache();
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!; client.encryption!.ssss.pendingShareRequests.clear();
key.setDirectVerified(true); await client.encryption!.ssss.request(EventTypes.MegolmBackup, [key]);
await client.encryption!.ssss.clearCache(); event = ToDeviceEvent(
client.encryption!.ssss.pendingShareRequests.clear(); sender: client.userID!,
await client.encryption!.ssss.maybeRequestAll([key]); type: 'm.secret.send',
expect(client.encryption!.ssss.pendingShareRequests.length, 3); content: {
}); 'request_id':
client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 'foxies!',
},
encryptedContent: {
'sender_key': key.curve25519Key,
},
);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
await client.encryption!.ssss.getCached(EventTypes.MegolmBackup),
null,
);
});
test('periodicallyRequestMissingCache', () async { test('request all', () async {
client.userDeviceKeys[client.userID!]!.masterKey!.setDirectVerified(true); final key =
client.encryption!.ssss = MockSSSS(client.encryption!); client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
(client.encryption!.ssss as MockSSSS).requestedSecrets = false; key.setDirectVerified(true);
await client.encryption!.ssss.periodicallyRequestMissingCache(); await client.encryption!.ssss.clearCache();
expect((client.encryption!.ssss as MockSSSS).requestedSecrets, true); client.encryption!.ssss.pendingShareRequests.clear();
// it should only retry once every 15 min await client.encryption!.ssss.maybeRequestAll([key]);
(client.encryption!.ssss as MockSSSS).requestedSecrets = false; expect(client.encryption!.ssss.pendingShareRequests.length, 3);
await client.encryption!.ssss.periodicallyRequestMissingCache(); });
expect((client.encryption!.ssss as MockSSSS).requestedSecrets, false);
});
test('createKey', () async { test('periodicallyRequestMissingCache', () async {
// with passphrase client.userDeviceKeys[client.userID!]!.masterKey!
var newKey = await client.encryption!.ssss.createKey('test'); .setDirectVerified(true);
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true); client.encryption!.ssss = MockSSSS(client.encryption!);
var testKey = client.encryption!.ssss.open(newKey.keyId); (client.encryption!.ssss as MockSSSS).requestedSecrets = false;
await testKey.unlock(passphrase: 'test'); await client.encryption!.ssss.periodicallyRequestMissingCache();
await testKey.setPrivateKey(newKey.privateKey!); expect((client.encryption!.ssss as MockSSSS).requestedSecrets, true);
// it should only retry once every 15 min
(client.encryption!.ssss as MockSSSS).requestedSecrets = false;
await client.encryption!.ssss.periodicallyRequestMissingCache();
expect((client.encryption!.ssss as MockSSSS).requestedSecrets, false);
});
// without passphrase test('createKey', () async {
newKey = await client.encryption!.ssss.createKey(); // with passphrase
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true); var newKey = await client.encryption!.ssss.createKey('test');
testKey = client.encryption!.ssss.open(newKey.keyId); expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
await testKey.setPrivateKey(newKey.privateKey!); var testKey = client.encryption!.ssss.open(newKey.keyId);
}); await testKey.unlock(passphrase: 'test');
await testKey.setPrivateKey(newKey.privateKey!);
test('dispose client', () async { // without passphrase
await client.dispose(closeDatabase: true); newKey = await client.encryption!.ssss.createKey();
}); expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
}); testKey = client.encryption!.ssss.open(newKey.keyId);
await testKey.setPrivateKey(newKey.privateKey!);
});
test('dispose client', () async {
await client.dispose(closeDatabase: true);
});
},
timeout: Timeout(const Duration(minutes: 2)),
);
} }

View File

@ -22,10 +22,14 @@ import 'package:http/http.dart' as http;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'fake_client.dart';
void main() { void main() {
/// All Tests related to device keys /// All Tests related to device keys
group('Matrix File', tags: 'olm', () { group('Matrix File', tags: 'olm', () {
setUpAll(() async {
await getClient(); // To trigger vodozemac init
});
Logs().level = Level.error; Logs().level = Level.error;
test('Decrypt', () async { test('Decrypt', () async {
final text = 'hello world'; final text = 'hello world';