refactor: Upgrade to vodozemac cryptoutils
This commit is contained in:
parent
03c5e354d8
commit
fe94df97db
|
|
@ -1,2 +1,2 @@
|
|||
flutter_version=3.27.4
|
||||
dart_version=3.6.2
|
||||
flutter_version=3.35.4
|
||||
dart_version=3.9.2
|
||||
|
|
@ -6,16 +6,13 @@ Matrix (matrix.org) SDK written in dart.
|
|||
|
||||
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) and the [flutter_openssl_crypto](https://pub.dev/packages/flutter_openssl_crypto) packages.
|
||||
For flutter apps you can easily import it with the [flutter_vodozemac](https://pub.dev/packages/flutter_vodozemac) package.
|
||||
|
||||
```sh
|
||||
flutter pub add matrix
|
||||
|
||||
# Optional: For end to end encryption:
|
||||
flutter pub add flutter_vodozemac
|
||||
flutter pub add flutter_openssl_crypto
|
||||
```
|
||||
|
||||
## Get started
|
||||
|
|
|
|||
|
|
@ -6,12 +6,6 @@ For Flutter you can use [flutter_vodozemac](https://pub.dev/packages/flutter_vod
|
|||
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:
|
||||
|
||||
```dart
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ In your `pubspec.yaml` file add the following dependencies:
|
|||
# (Optional) For end to end encryption, please head on the
|
||||
# encryption guide and add these dependencies:
|
||||
flutter_vodozemac: <latest-version>
|
||||
flutter_openssl_crypto: <latest-version>
|
||||
```
|
||||
|
||||
## Step 2: Create the client
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:base58check/base58.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/utils/base64_unpadded.dart';
|
||||
|
|
@ -74,16 +74,18 @@ class SSSS {
|
|||
|
||||
static DerivedKeys deriveKeys(Uint8List key, String name) {
|
||||
final zerosalt = Uint8List(8);
|
||||
final prk = Hmac(sha256, zerosalt).convert(key);
|
||||
final prk = CryptoUtils.hmac(key: zerosalt, input: key);
|
||||
final b = Uint8List(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;
|
||||
final hmacKey =
|
||||
Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b);
|
||||
final hmacKey = CryptoUtils.hmac(
|
||||
key: prk,
|
||||
input: aesKey + utf8.encode(name) + b,
|
||||
);
|
||||
return DerivedKeys(
|
||||
aesKey: Uint8List.fromList(aesKey.bytes),
|
||||
hmacKey: Uint8List.fromList(hmacKey.bytes),
|
||||
aesKey: Uint8List.fromList(aesKey),
|
||||
hmacKey: Uint8List.fromList(hmacKey),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -105,14 +107,15 @@ class SSSS {
|
|||
final keys = deriveKeys(key, name);
|
||||
|
||||
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(
|
||||
iv: base64.encode(iv),
|
||||
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 cipher = base64decodeUnpadded(data.ciphertext);
|
||||
final hmac = base64
|
||||
.encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes)
|
||||
.encode(CryptoUtils.hmac(key: keys.hmacKey, input: cipher))
|
||||
.replaceAll(RegExp(r'=+$'), '');
|
||||
if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) {
|
||||
throw Exception('Bad MAC');
|
||||
}
|
||||
final decipher = await uc.aesCtr
|
||||
.encrypt(cipher, keys.aesKey, base64decodeUnpadded(data.iv));
|
||||
final decipher = CryptoUtils.aesCtr(
|
||||
input: cipher,
|
||||
key: keys.aesKey,
|
||||
iv: base64decodeUnpadded(data.iv),
|
||||
);
|
||||
return String.fromCharCodes(decipher);
|
||||
}
|
||||
|
||||
|
|
@ -184,12 +190,10 @@ class SSSS {
|
|||
if (info.salt == null) {
|
||||
throw InvalidPassphraseException('Passphrase info without salt');
|
||||
}
|
||||
return await uc.pbkdf2(
|
||||
Uint8List.fromList(utf8.encode(passphrase)),
|
||||
Uint8List.fromList(utf8.encode(info.salt!)),
|
||||
uc.sha512,
|
||||
info.iterations!,
|
||||
info.bits ?? 256,
|
||||
return CryptoUtils.pbkdf2(
|
||||
passphrase: Uint8List.fromList(utf8.encode(passphrase)),
|
||||
salt: Uint8List.fromList(utf8.encode(info.salt!)),
|
||||
iterations: info.iterations!,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -742,7 +746,7 @@ class OpenSSSS {
|
|||
info: keyData.passphrase!,
|
||||
),
|
||||
),
|
||||
).timeout(Duration(seconds: 10));
|
||||
).timeout(Duration(minutes: 2));
|
||||
} else if (recoveryKey != null) {
|
||||
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import 'dart:convert';
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:typed_data/typed_data.dart';
|
||||
import 'package:vodozemac/vodozemac.dart' as vod;
|
||||
|
||||
|
|
@ -1559,8 +1558,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
Future<String> _makeCommitment(String pubKey, String canonicalJson) async {
|
||||
if (hash == 'sha256') {
|
||||
final bytes = utf8.encoder.convert(pubKey + canonicalJson);
|
||||
final digest = crypto.sha256.convert(bytes);
|
||||
return encodeBase64Unpadded(digest.bytes);
|
||||
final digest = vod.CryptoUtils.sha256(input: bytes);
|
||||
return encodeBase64Unpadded(digest);
|
||||
}
|
||||
throw Exception('Unknown hash method');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@
|
|||
* 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:typed_data';
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:vodozemac/vodozemac.dart';
|
||||
|
||||
import 'package:matrix/encryption/utils/base64_unpadded.dart';
|
||||
import 'package:matrix/src/utils/crypto/crypto.dart';
|
||||
|
||||
|
|
@ -38,8 +40,8 @@ class EncryptedFile {
|
|||
Future<EncryptedFile> encryptFile(Uint8List input) async {
|
||||
final key = secureRandomBytes(32);
|
||||
final iv = secureRandomBytes(16);
|
||||
final data = await aesCtr.encrypt(input, key, iv);
|
||||
final hash = await sha256(data);
|
||||
final data = CryptoUtils.aesCtr(input: input, key: key, iv: iv);
|
||||
final hash = CryptoUtils.sha256(input: data);
|
||||
return EncryptedFile(
|
||||
data: data,
|
||||
k: base64Url.encode(key).replaceAll('=', ''),
|
||||
|
|
@ -51,12 +53,12 @@ Future<EncryptedFile> encryptFile(Uint8List input) async {
|
|||
/// you would likely want to use [NativeImplementations] and
|
||||
/// [Client.nativeImplementations] instead
|
||||
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)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final key = base64decodeUnpadded(base64.normalize(input.k));
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}();
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -155,7 +155,10 @@ class NativeImplementationsIsolate extends NativeImplementations {
|
|||
bool retryInDummy = true,
|
||||
}) {
|
||||
return runInBackground<Uint8List?, EncryptedFile>(
|
||||
NativeImplementations.dummy.decryptFile,
|
||||
(EncryptedFile args) async {
|
||||
await vodozemacInit?.call();
|
||||
return NativeImplementations.dummy.decryptFile(args);
|
||||
},
|
||||
file,
|
||||
);
|
||||
}
|
||||
|
|
@ -180,7 +183,10 @@ class NativeImplementationsIsolate extends NativeImplementations {
|
|||
bool retryInDummy = true,
|
||||
}) {
|
||||
return runInBackground<Uint8List, KeyFromPassphraseArgs>(
|
||||
NativeImplementations.dummy.keyFromPassphrase,
|
||||
(KeyFromPassphraseArgs args) async {
|
||||
await vodozemacInit?.call();
|
||||
return NativeImplementations.dummy.keyFromPassphrase(args);
|
||||
},
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,14 +14,10 @@ dependencies:
|
|||
blurhash_dart: ^1.1.0
|
||||
canonical_json: ^1.1.0
|
||||
collection: ^1.15.0
|
||||
crypto: ^3.0.0
|
||||
enhanced_enum: ^0.2.4
|
||||
ffi: ^2.0.0
|
||||
html: ^0.15.0
|
||||
html_unescape: ^2.0.0
|
||||
http: ">=0.13.0 <2.0.0"
|
||||
image: ^4.0.15
|
||||
js: ^0.6.3
|
||||
js_interop: ^0.0.1
|
||||
markdown: ^7.1.1
|
||||
mime: ">=1.0.0 <3.0.0"
|
||||
|
|
@ -32,7 +28,7 @@ dependencies:
|
|||
sqflite_common: ^2.4.5
|
||||
sqlite3: ^2.1.0
|
||||
typed_data: ^1.3.2
|
||||
vodozemac: ^0.2.0
|
||||
vodozemac: ^0.3.0
|
||||
web: ^1.1.1
|
||||
webrtc_interface: ^1.2.0
|
||||
|
||||
|
|
@ -43,4 +39,4 @@ dev_dependencies:
|
|||
import_sorter: ^4.6.0
|
||||
lints: ^5.0.0
|
||||
sqflite_common_ffi: ^2.3.4+4 # sqflite_common_ffi aggressively requires newer dart versions
|
||||
test: ^1.25.13
|
||||
test: ^1.25.13
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
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 ./
|
||||
rm -rf dart-vodozemac
|
||||
cd ./rust
|
||||
|
|
|
|||
|
|
@ -48,462 +48,476 @@ class MockSSSS extends SSSS {
|
|||
}
|
||||
|
||||
void main() {
|
||||
group('SSSS', tags: 'olm', () {
|
||||
Logs().level = Level.error;
|
||||
group(
|
||||
'SSSS',
|
||||
tags: 'olm',
|
||||
() {
|
||||
Logs().level = Level.error;
|
||||
|
||||
late Client client;
|
||||
late Client client;
|
||||
|
||||
setUpAll(() async {
|
||||
await vod.init(
|
||||
wasmPath: './pkg/',
|
||||
libraryPath: './rust/target/debug/',
|
||||
);
|
||||
setUpAll(() async {
|
||||
await vod.init(
|
||||
wasmPath: './pkg/',
|
||||
libraryPath: './rust/target/debug/',
|
||||
);
|
||||
|
||||
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),
|
||||
client = await getClient();
|
||||
});
|
||||
expect(await handle.getStored('best animal'), 'foxies');
|
||||
});
|
||||
|
||||
test('encode / decode recovery key', () async {
|
||||
final key = Uint8List.fromList(secureRandomBytes(32));
|
||||
final encoded = SSSS.encodeRecoveryKey(key);
|
||||
var decoded = SSSS.decodeRecoveryKey(encoded);
|
||||
expect(key, decoded);
|
||||
test('basic things', () async {
|
||||
expect(
|
||||
client.encryption!.ssss.defaultKeyId,
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3',
|
||||
);
|
||||
});
|
||||
|
||||
decoded = SSSS.decodeRecoveryKey('$encoded \n\t');
|
||||
expect(key, decoded);
|
||||
test('encrypt / decrypt', () async {
|
||||
final key = Uint8List.fromList(secureRandomBytes(32));
|
||||
|
||||
final handle = client.encryption!.ssss.open();
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
expect(handle.recoveryKey, ssssKey);
|
||||
});
|
||||
final enc = await SSSS.encryptAes('secret foxies', key, 'name');
|
||||
final dec = await SSSS.decryptAes(enc, key, 'name');
|
||||
expect(dec, 'secret foxies');
|
||||
});
|
||||
|
||||
test('cache', () async {
|
||||
await client.encryption!.ssss.clearCache();
|
||||
final handle =
|
||||
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: ssssKey, postUnlock: false);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
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,
|
||||
);
|
||||
});
|
||||
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();
|
||||
|
||||
test('postUnlock', () async {
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!
|
||||
.setDirectVerified(false);
|
||||
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,
|
||||
);
|
||||
});
|
||||
// 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!);
|
||||
|
||||
test('make share requests', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.request('some.type', [key]);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
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('answer to share requests', () async {
|
||||
var 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'),
|
||||
),
|
||||
true,
|
||||
);
|
||||
test('encode / decode recovery key', () async {
|
||||
final key = Uint8List.fromList(secureRandomBytes(32));
|
||||
final encoded = SSSS.encodeRecoveryKey(key);
|
||||
var decoded = SSSS.decodeRecoveryKey(encoded);
|
||||
expect(key, decoded);
|
||||
|
||||
// now test some fail scenarios
|
||||
decoded = SSSS.decodeRecoveryKey('$encoded \n\t');
|
||||
expect(key, decoded);
|
||||
|
||||
// not by us
|
||||
event = ToDeviceEvent(
|
||||
sender: '@someotheruser:example.org',
|
||||
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,
|
||||
);
|
||||
final handle = client.encryption!.ssss.open();
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
expect(handle.recoveryKey, ssssKey);
|
||||
});
|
||||
|
||||
// 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,
|
||||
);
|
||||
test('cache', () async {
|
||||
await client.encryption!.ssss.clearCache();
|
||||
final handle =
|
||||
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: ssssKey, postUnlock: false);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
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
|
||||
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,
|
||||
);
|
||||
test('postUnlock', () async {
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!
|
||||
.setDirectVerified(false);
|
||||
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,
|
||||
);
|
||||
});
|
||||
|
||||
// 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('make share requests', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.request('some.type', [key]);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
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('answer to share requests', () async {
|
||||
var 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'),
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
var event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
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('best animal'), 'foxies!');
|
||||
// now test some fail scenarios
|
||||
|
||||
// not by us
|
||||
event = ToDeviceEvent(
|
||||
sender: '@someotheruser:example.org',
|
||||
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
|
||||
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();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request(type, [key]);
|
||||
event = ToDeviceEvent(
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
var event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id':
|
||||
client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': secret,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
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
|
||||
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);
|
||||
// test different fail scenarios
|
||||
|
||||
// unknown request id
|
||||
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': 'invalid',
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
// not encrypted
|
||||
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);
|
||||
|
||||
// not from a device we sent the request to
|
||||
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!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': 'invalid',
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
// unknown request id
|
||||
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': 'invalid',
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
|
||||
// secret not a string
|
||||
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': 42,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
// not from a device we sent the request to
|
||||
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!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': 'invalid',
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
|
||||
// validator doesn't check out
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request(EventTypes.MegolmBackup, [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
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,
|
||||
);
|
||||
});
|
||||
// secret not a string
|
||||
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': 42,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
|
||||
test('request all', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.maybeRequestAll([key]);
|
||||
expect(client.encryption!.ssss.pendingShareRequests.length, 3);
|
||||
});
|
||||
// validator doesn't check out
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request(EventTypes.MegolmBackup, [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
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 {
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!.setDirectVerified(true);
|
||||
client.encryption!.ssss = MockSSSS(client.encryption!);
|
||||
(client.encryption!.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client.encryption!.ssss.periodicallyRequestMissingCache();
|
||||
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);
|
||||
});
|
||||
test('request all', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.maybeRequestAll([key]);
|
||||
expect(client.encryption!.ssss.pendingShareRequests.length, 3);
|
||||
});
|
||||
|
||||
test('createKey', () async {
|
||||
// with passphrase
|
||||
var newKey = await client.encryption!.ssss.createKey('test');
|
||||
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
|
||||
var testKey = client.encryption!.ssss.open(newKey.keyId);
|
||||
await testKey.unlock(passphrase: 'test');
|
||||
await testKey.setPrivateKey(newKey.privateKey!);
|
||||
test('periodicallyRequestMissingCache', () async {
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!
|
||||
.setDirectVerified(true);
|
||||
client.encryption!.ssss = MockSSSS(client.encryption!);
|
||||
(client.encryption!.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client.encryption!.ssss.periodicallyRequestMissingCache();
|
||||
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
|
||||
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('createKey', () async {
|
||||
// with passphrase
|
||||
var newKey = await client.encryption!.ssss.createKey('test');
|
||||
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
|
||||
var testKey = client.encryption!.ssss.open(newKey.keyId);
|
||||
await testKey.unlock(passphrase: 'test');
|
||||
await testKey.setPrivateKey(newKey.privateKey!);
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
// without passphrase
|
||||
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)),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,14 @@ import 'package:http/http.dart' as http;
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'fake_client.dart';
|
||||
|
||||
void main() {
|
||||
/// All Tests related to device keys
|
||||
group('Matrix File', tags: 'olm', () {
|
||||
setUpAll(() async {
|
||||
await getClient(); // To trigger vodozemac init
|
||||
});
|
||||
Logs().level = Level.error;
|
||||
test('Decrypt', () async {
|
||||
final text = 'hello world';
|
||||
|
|
|
|||
Loading…
Reference in New Issue