feat: Add bootstrapping
This commit is contained in:
parent
88888a43f1
commit
49f0679fbf
|
|
@ -22,3 +22,4 @@ export 'encryption/encryption.dart';
|
||||||
export 'encryption/key_manager.dart';
|
export 'encryption/key_manager.dart';
|
||||||
export 'encryption/ssss.dart';
|
export 'encryption/ssss.dart';
|
||||||
export 'encryption/utils/key_verification.dart';
|
export 'encryption/utils/key_verification.dart';
|
||||||
|
export 'encryption/utils/bootstrap.dart';
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ import 'package:olm/olm.dart' as olm;
|
||||||
import '../famedlysdk.dart';
|
import '../famedlysdk.dart';
|
||||||
import 'encryption.dart';
|
import 'encryption.dart';
|
||||||
|
|
||||||
const SELF_SIGNING_KEY = 'm.cross_signing.self_signing';
|
const SELF_SIGNING_KEY = EventTypes.CrossSigningSelfSigning;
|
||||||
const USER_SIGNING_KEY = 'm.cross_signing.user_signing';
|
const USER_SIGNING_KEY = EventTypes.CrossSigningUserSigning;
|
||||||
const MASTER_KEY = 'm.cross_signing.master';
|
const MASTER_KEY = 'm.cross_signing.master';
|
||||||
|
|
||||||
class CrossSigning {
|
class CrossSigning {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import 'key_manager.dart';
|
||||||
import 'key_verification_manager.dart';
|
import 'key_verification_manager.dart';
|
||||||
import 'olm_manager.dart';
|
import 'olm_manager.dart';
|
||||||
import 'ssss.dart';
|
import 'ssss.dart';
|
||||||
|
import 'utils/bootstrap.dart';
|
||||||
|
|
||||||
class Encryption {
|
class Encryption {
|
||||||
final Client client;
|
final Client client;
|
||||||
|
|
@ -69,6 +70,11 @@ class Encryption {
|
||||||
_backgroundTasks(); // start the background tasks
|
_backgroundTasks(); // start the background tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bootstrap bootstrap({void Function() onUpdate}) => Bootstrap(
|
||||||
|
encryption: this,
|
||||||
|
onUpdate: onUpdate,
|
||||||
|
);
|
||||||
|
|
||||||
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
|
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
|
||||||
runInRoot(() => olmManager.handleDeviceOneTimeKeysCount(countJson));
|
runInRoot(() => olmManager.handleDeviceOneTimeKeysCount(countJson));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import '../matrix_api/utils/logs.dart';
|
||||||
import '../src/utils/run_in_background.dart';
|
import '../src/utils/run_in_background.dart';
|
||||||
import '../src/utils/run_in_root.dart';
|
import '../src/utils/run_in_root.dart';
|
||||||
|
|
||||||
const MEGOLM_KEY = 'm.megolm_backup.v1';
|
const MEGOLM_KEY = EventTypes.MegolmBackup;
|
||||||
|
|
||||||
class KeyManager {
|
class KeyManager {
|
||||||
final Encryption encryption;
|
final Encryption encryption;
|
||||||
|
|
|
||||||
|
|
@ -30,20 +30,28 @@ import '../famedlysdk.dart';
|
||||||
import '../matrix_api.dart';
|
import '../matrix_api.dart';
|
||||||
import '../src/database/database.dart';
|
import '../src/database/database.dart';
|
||||||
import '../matrix_api/utils/logs.dart';
|
import '../matrix_api/utils/logs.dart';
|
||||||
|
import '../src/utils/run_in_background.dart';
|
||||||
import 'encryption.dart';
|
import 'encryption.dart';
|
||||||
|
|
||||||
const CACHE_TYPES = <String>[
|
const CACHE_TYPES = <String>{
|
||||||
'm.cross_signing.self_signing',
|
EventTypes.CrossSigningSelfSigning,
|
||||||
'm.cross_signing.user_signing',
|
EventTypes.CrossSigningUserSigning,
|
||||||
'm.megolm_backup.v1'
|
EventTypes.MegolmBackup,
|
||||||
];
|
};
|
||||||
|
|
||||||
const ZERO_STR =
|
const ZERO_STR =
|
||||||
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00';
|
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00';
|
||||||
const BASE58_ALPHABET =
|
const BASE58_ALPHABET =
|
||||||
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||||
const base58 = Base58Codec(BASE58_ALPHABET);
|
const base58 = Base58Codec(BASE58_ALPHABET);
|
||||||
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
||||||
const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm
|
const SSSS_KEY_LENGTH = 32;
|
||||||
|
const PBKDF2_DEFAULT_ITERATIONS = 500000;
|
||||||
|
const PBKDF2_SALT_LENGTH = 64;
|
||||||
|
|
||||||
|
Map<String, dynamic> _deepcopy(Map<String, dynamic> data) {
|
||||||
|
return json.decode(json.encode(data));
|
||||||
|
}
|
||||||
|
|
||||||
class SSSS {
|
class SSSS {
|
||||||
final Encryption encryption;
|
final Encryption encryption;
|
||||||
|
|
@ -129,17 +137,16 @@ class SSSS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.length !=
|
if (result.length != OLM_RECOVERY_KEY_PREFIX.length + SSSS_KEY_LENGTH + 1) {
|
||||||
OLM_RECOVERY_KEY_PREFIX.length + OLM_PRIVATE_KEY_LENGTH + 1) {
|
|
||||||
throw 'Incorrect length';
|
throw 'Incorrect length';
|
||||||
}
|
}
|
||||||
|
|
||||||
return Uint8List.fromList(result.sublist(OLM_RECOVERY_KEY_PREFIX.length,
|
return Uint8List.fromList(result.sublist(OLM_RECOVERY_KEY_PREFIX.length,
|
||||||
OLM_RECOVERY_KEY_PREFIX.length + OLM_PRIVATE_KEY_LENGTH));
|
OLM_RECOVERY_KEY_PREFIX.length + SSSS_KEY_LENGTH));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Uint8List keyFromPassphrase(String passphrase, _PassphraseInfo info) {
|
static Uint8List keyFromPassphrase(String passphrase, _PassphraseInfo info) {
|
||||||
if (info.algorithm != 'm.pbkdf2') {
|
if (info.algorithm != AlgorithmTypes.pbkdf2) {
|
||||||
throw 'Unknown algorithm';
|
throw 'Unknown algorithm';
|
||||||
}
|
}
|
||||||
final generator = PBKDF2(hashAlgorithm: sha512);
|
final generator = PBKDF2(hashAlgorithm: sha512);
|
||||||
|
|
@ -156,15 +163,84 @@ class SSSS {
|
||||||
}
|
}
|
||||||
|
|
||||||
String get defaultKeyId {
|
String get defaultKeyId {
|
||||||
final keyData = client.accountData['m.secret_storage.default_key'];
|
final keyData = client.accountData[EventTypes.SecretStorageDefaultKey];
|
||||||
if (keyData == null || !(keyData.content['key'] is String)) {
|
if (keyData == null || !(keyData.content['key'] is String)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return keyData.content['key'];
|
return keyData.content['key'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setDefaultKeyId(String keyId) async {
|
||||||
|
await client.setAccountData(
|
||||||
|
client.userID, EventTypes.SecretStorageDefaultKey, <String, dynamic>{
|
||||||
|
'key': keyId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
BasicEvent getKey(String keyId) {
|
BasicEvent getKey(String keyId) {
|
||||||
return client.accountData['m.secret_storage.key.${keyId}'];
|
return client.accountData[EventTypes.secretStorageKey(keyId)];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isKeyValid(String keyId) {
|
||||||
|
final keyData = getKey(keyId);
|
||||||
|
if (keyData == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return keyData.content['algorithm'] ==
|
||||||
|
AlgorithmTypes.secretStorageV1AesHmcSha2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<OpenSSSS> createKey([String passphrase]) async {
|
||||||
|
Uint8List privateKey;
|
||||||
|
final content = <String, dynamic>{};
|
||||||
|
if (passphrase != null) {
|
||||||
|
// we need to derive the key off of the passphrase
|
||||||
|
content['passphrase'] = <String, dynamic>{};
|
||||||
|
content['passphrase']['algorithm'] = AlgorithmTypes.pbkdf2;
|
||||||
|
content['passphrase']['salt'] = base64
|
||||||
|
.encode(SecureRandom(PBKDF2_SALT_LENGTH).bytes); // generate salt
|
||||||
|
content['passphrase']['iterations'] = PBKDF2_DEFAULT_ITERATIONS;
|
||||||
|
content['passphrase']['bits'] = SSSS_KEY_LENGTH * 8;
|
||||||
|
privateKey = await runInBackground(
|
||||||
|
_keyFromPassphrase,
|
||||||
|
_KeyFromPassphraseArgs(
|
||||||
|
passphrase: passphrase,
|
||||||
|
info: _PassphraseInfo(
|
||||||
|
algorithm: content['passphrase']['algorithm'],
|
||||||
|
salt: content['passphrase']['salt'],
|
||||||
|
iterations: content['passphrase']['iterations'],
|
||||||
|
bits: content['passphrase']['bits'],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// we need to just generate a new key from scratch
|
||||||
|
privateKey = Uint8List.fromList(SecureRandom(SSSS_KEY_LENGTH).bytes);
|
||||||
|
}
|
||||||
|
// now that we have the private key, let's create the iv and mac
|
||||||
|
final encrypted = encryptAes(ZERO_STR, privateKey, '');
|
||||||
|
content['iv'] = encrypted.iv;
|
||||||
|
content['mac'] = encrypted.mac;
|
||||||
|
content['algorithm'] = AlgorithmTypes.secretStorageV1AesHmcSha2;
|
||||||
|
|
||||||
|
const KEYID_BYTE_LENGTH = 24;
|
||||||
|
|
||||||
|
// make sure we generate a unique key id
|
||||||
|
var keyId = base64.encode(SecureRandom(KEYID_BYTE_LENGTH).bytes);
|
||||||
|
while (getKey(keyId) != null) {
|
||||||
|
keyId = base64.encode(SecureRandom(KEYID_BYTE_LENGTH).bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
final accountDataType = EventTypes.secretStorageKey(keyId);
|
||||||
|
// noooow we set the account data
|
||||||
|
final waitForAccountData = client.onSync.stream.firstWhere((syncUpdate) =>
|
||||||
|
syncUpdate.accountData
|
||||||
|
.any((accountData) => accountData.type == accountDataType));
|
||||||
|
await client.setAccountData(client.userID, accountDataType, content);
|
||||||
|
await waitForAccountData;
|
||||||
|
|
||||||
|
final key = open(keyId);
|
||||||
|
key.setPrivateKey(privateKey);
|
||||||
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool checkKey(Uint8List key, BasicEvent keyData) {
|
bool checkKey(Uint8List key, BasicEvent keyData) {
|
||||||
|
|
@ -234,12 +310,19 @@ class SSSS {
|
||||||
return decrypted;
|
return decrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> store(
|
Future<void> store(String type, String secret, String keyId, Uint8List key,
|
||||||
String type, String secret, String keyId, Uint8List key) async {
|
{bool add = false}) async {
|
||||||
final triggerCacheCallback =
|
final triggerCacheCallback =
|
||||||
_cacheCallbacks.containsKey(type) && await getCached(type) == null;
|
_cacheCallbacks.containsKey(type) && await getCached(type) == null;
|
||||||
final encrypted = encryptAes(secret, key, type);
|
final encrypted = encryptAes(secret, key, type);
|
||||||
final content = <String, dynamic>{
|
Map<String, dynamic> content;
|
||||||
|
if (add && client.accountData[type] != null) {
|
||||||
|
content = _deepcopy(client.accountData[type].content);
|
||||||
|
if (!(content['encrypted'] is Map)) {
|
||||||
|
content['encrypted'] = <String, dynamic>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content ??= <String, dynamic>{
|
||||||
'encrypted': <String, dynamic>{},
|
'encrypted': <String, dynamic>{},
|
||||||
};
|
};
|
||||||
content['encrypted'][keyId] = <String, dynamic>{
|
content['encrypted'][keyId] = <String, dynamic>{
|
||||||
|
|
@ -259,6 +342,29 @@ class SSSS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> validateAndStripOtherKeys(
|
||||||
|
String type, String secret, String keyId, Uint8List key) async {
|
||||||
|
if (await getStored(type, keyId, key) != secret) {
|
||||||
|
throw 'Secrets do not match up!';
|
||||||
|
}
|
||||||
|
// now remove all other keys
|
||||||
|
final content = _deepcopy(client.accountData[type].content);
|
||||||
|
final otherKeys =
|
||||||
|
Set<String>.from(content['encrypted'].keys.where((k) => k != keyId));
|
||||||
|
content['encrypted'].removeWhere((k, v) => otherKeys.contains(k));
|
||||||
|
// yes, we are paranoid...
|
||||||
|
if (await getStored(type, keyId, key) != secret) {
|
||||||
|
throw 'Secrets do not match up!';
|
||||||
|
}
|
||||||
|
// store the thing in your account data
|
||||||
|
await client.setAccountData(client.userID, type, content);
|
||||||
|
if (CACHE_TYPES.contains(type) && client.database != null) {
|
||||||
|
// cache the thing
|
||||||
|
await client.database.storeSSSSCache(client.id, type, keyId,
|
||||||
|
content['encrypted'][keyId]['ciphertext'], secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> maybeCacheAll(String keyId, Uint8List key) async {
|
Future<void> maybeCacheAll(String keyId, Uint8List key) async {
|
||||||
for (final type in CACHE_TYPES) {
|
for (final type in CACHE_TYPES) {
|
||||||
final secret = await getCached(type);
|
final secret = await getCached(type);
|
||||||
|
|
@ -512,15 +618,27 @@ class OpenSSSS {
|
||||||
|
|
||||||
bool get isUnlocked => privateKey != null;
|
bool get isUnlocked => privateKey != null;
|
||||||
|
|
||||||
void unlock({String passphrase, String recoveryKey}) {
|
Future<void> unlock(
|
||||||
if (passphrase != null) {
|
{String passphrase, String recoveryKey, String keyOrPassphrase}) async {
|
||||||
privateKey = SSSS.keyFromPassphrase(
|
if (keyOrPassphrase != null) {
|
||||||
passphrase,
|
try {
|
||||||
_PassphraseInfo(
|
await unlock(recoveryKey: keyOrPassphrase);
|
||||||
|
} catch (_) {
|
||||||
|
await unlock(passphrase: keyOrPassphrase);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (passphrase != null) {
|
||||||
|
privateKey = await runInBackground(
|
||||||
|
_keyFromPassphrase,
|
||||||
|
_KeyFromPassphraseArgs(
|
||||||
|
passphrase: passphrase,
|
||||||
|
info: _PassphraseInfo(
|
||||||
algorithm: keyData.content['passphrase']['algorithm'],
|
algorithm: keyData.content['passphrase']['algorithm'],
|
||||||
salt: keyData.content['passphrase']['salt'],
|
salt: keyData.content['passphrase']['salt'],
|
||||||
iterations: keyData.content['passphrase']['iterations'],
|
iterations: keyData.content['passphrase']['iterations'],
|
||||||
bits: keyData.content['passphrase']['bits']));
|
bits: keyData.content['passphrase']['bits'],
|
||||||
|
),
|
||||||
|
));
|
||||||
} else if (recoveryKey != null) {
|
} else if (recoveryKey != null) {
|
||||||
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
|
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -533,15 +651,36 @@ class OpenSSSS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setPrivateKey(Uint8List key) {
|
||||||
|
if (!ssss.checkKey(key, keyData)) {
|
||||||
|
throw 'Invalid key';
|
||||||
|
}
|
||||||
|
privateKey = key;
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> getStored(String type) async {
|
Future<String> getStored(String type) async {
|
||||||
return await ssss.getStored(type, keyId, privateKey);
|
return await ssss.getStored(type, keyId, privateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> store(String type, String secret) async {
|
Future<void> store(String type, String secret, {bool add = false}) async {
|
||||||
await ssss.store(type, secret, keyId, privateKey);
|
await ssss.store(type, secret, keyId, privateKey, add: add);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> validateAndStripOtherKeys(String type, String secret) async {
|
||||||
|
await ssss.validateAndStripOtherKeys(type, secret, keyId, privateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> maybeCacheAll() async {
|
Future<void> maybeCacheAll() async {
|
||||||
await ssss.maybeCacheAll(keyId, privateKey);
|
await ssss.maybeCacheAll(keyId, privateKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _KeyFromPassphraseArgs {
|
||||||
|
final String passphrase;
|
||||||
|
final _PassphraseInfo info;
|
||||||
|
_KeyFromPassphraseArgs({this.passphrase, this.info});
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List _keyFromPassphrase(_KeyFromPassphraseArgs args) {
|
||||||
|
return SSSS.keyFromPassphrase(args.passphrase, args.info);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,543 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2020 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:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:canonical_json/canonical_json.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
import '../encryption.dart';
|
||||||
|
import '../ssss.dart';
|
||||||
|
import '../cross_signing.dart';
|
||||||
|
import '../key_manager.dart';
|
||||||
|
import '../../famedlysdk.dart';
|
||||||
|
import '../../matrix_api/utils/logs.dart';
|
||||||
|
|
||||||
|
enum BootstrapState {
|
||||||
|
loading, // loading
|
||||||
|
askWipeSsss, // existing SSSS found, should we wipe it?
|
||||||
|
askUseExistingSsss, // ask if an existing SSSS should be userDeviceKeys
|
||||||
|
askBadSsss, // SSSS is in a bad state, continue with potential dataloss?
|
||||||
|
askUnlockSsss, // Ask to unlock all the SSSS keys
|
||||||
|
askNewSsss, // Ask for new SSSS key / passphrase
|
||||||
|
openExistingSsss, // Open an existing SSSS key
|
||||||
|
askWipeCrossSigning, // Ask if cross signing should be wiped
|
||||||
|
askSetupCrossSigning, // Ask if cross signing should be set up
|
||||||
|
askWipeOnlineKeyBackup, // Ask if online key backup should be wiped
|
||||||
|
askSetupOnlineKeyBackup, // Ask if the online key backup should be set up
|
||||||
|
error, // error
|
||||||
|
done, // done
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bootstrap {
|
||||||
|
final Encryption encryption;
|
||||||
|
Client get client => encryption.client;
|
||||||
|
void Function() onUpdate;
|
||||||
|
BootstrapState get state => _state;
|
||||||
|
BootstrapState _state = BootstrapState.loading;
|
||||||
|
Map<String, OpenSSSS> oldSsssKeys;
|
||||||
|
OpenSSSS newSsssKey;
|
||||||
|
Map<String, String> secretMap;
|
||||||
|
|
||||||
|
Bootstrap({this.encryption, this.onUpdate}) {
|
||||||
|
if (analyzeSecrets().isNotEmpty) {
|
||||||
|
state = BootstrapState.askWipeSsss;
|
||||||
|
} else {
|
||||||
|
state = BootstrapState.askNewSsss;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache the secret analyzing so that we don't drop stuff a different client sets during bootstrapping
|
||||||
|
Map<String, Set<String>> _secretsCache;
|
||||||
|
Map<String, Set<String>> analyzeSecrets() {
|
||||||
|
if (_secretsCache != null) {
|
||||||
|
// deep-copy so that we can do modifications
|
||||||
|
final newSecrets = <String, Set<String>>{};
|
||||||
|
for (final s in _secretsCache.entries) {
|
||||||
|
newSecrets[s.key] = Set<String>.from(s.value);
|
||||||
|
}
|
||||||
|
return newSecrets;
|
||||||
|
}
|
||||||
|
final secrets = <String, Set<String>>{};
|
||||||
|
for (final entry in client.accountData.entries) {
|
||||||
|
final type = entry.key;
|
||||||
|
final event = entry.value;
|
||||||
|
if (!(event.content['encrypted'] is Map)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final validKeys = <String>{};
|
||||||
|
final invalidKeys = <String>{};
|
||||||
|
for (final keyEntry in event.content['encrypted'].entries) {
|
||||||
|
final key = keyEntry.key;
|
||||||
|
final value = keyEntry.value;
|
||||||
|
if (!(value is Map)) {
|
||||||
|
// we don't add the key to invalidKeys as this was not a proper secret anyways!
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!(value['iv'] is String) ||
|
||||||
|
!(value['ciphertext'] is String) ||
|
||||||
|
!(value['mac'] is String)) {
|
||||||
|
invalidKeys.add(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!encryption.ssss.isKeyValid(key)) {
|
||||||
|
invalidKeys.add(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validKeys.add(key);
|
||||||
|
}
|
||||||
|
if (validKeys.isEmpty && invalidKeys.isEmpty) {
|
||||||
|
continue; // this didn't contain any keys anyways!
|
||||||
|
}
|
||||||
|
// if there are no valid keys and only invalid keys then the validKeys set will be empty
|
||||||
|
// from that we know that there were errors with this secret and that we won't be able to migrate it
|
||||||
|
secrets[type] = validKeys;
|
||||||
|
}
|
||||||
|
_secretsCache = secrets;
|
||||||
|
return analyzeSecrets();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> badSecrets() {
|
||||||
|
final secrets = analyzeSecrets();
|
||||||
|
secrets.removeWhere((k, v) => v.isNotEmpty);
|
||||||
|
return Set<String>.from(secrets.keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
String mostUsedKey(Map<String, Set<String>> secrets) {
|
||||||
|
final usage = <String, int>{};
|
||||||
|
for (final keys in secrets.values) {
|
||||||
|
for (final key in keys) {
|
||||||
|
if (!usage.containsKey(key)) {
|
||||||
|
usage[key] = 0;
|
||||||
|
}
|
||||||
|
usage[key]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final entriesList = usage.entries.toList();
|
||||||
|
entriesList.sort((a, b) => a.value.compareTo(b.value));
|
||||||
|
return entriesList.first.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> allNeededKeys() {
|
||||||
|
final secrets = analyzeSecrets();
|
||||||
|
secrets.removeWhere(
|
||||||
|
(k, v) => v.isEmpty); // we don't care about the failed secrets here
|
||||||
|
final keys = <String>{};
|
||||||
|
final defaultKeyId = encryption.ssss.defaultKeyId;
|
||||||
|
final removeKey = (String key) {
|
||||||
|
final sizeBefore = secrets.length;
|
||||||
|
secrets.removeWhere((k, v) => v.contains(key));
|
||||||
|
return sizeBefore - secrets.length;
|
||||||
|
};
|
||||||
|
// first we want to try the default key id
|
||||||
|
if (defaultKeyId != null) {
|
||||||
|
if (removeKey(defaultKeyId) > 0) {
|
||||||
|
keys.add(defaultKeyId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now we re-try as long as we have keys for all secrets
|
||||||
|
while (secrets.isNotEmpty) {
|
||||||
|
final key = mostUsedKey(secrets);
|
||||||
|
removeKey(key);
|
||||||
|
keys.add(key);
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wipeSsss(bool wipe) {
|
||||||
|
if (state != BootstrapState.askWipeSsss) {
|
||||||
|
throw Exception('Wrong State');
|
||||||
|
}
|
||||||
|
if (wipe) {
|
||||||
|
state = BootstrapState.askNewSsss;
|
||||||
|
} else if (encryption.ssss.defaultKeyId != null &&
|
||||||
|
encryption.ssss.isKeyValid(encryption.ssss.defaultKeyId)) {
|
||||||
|
state = BootstrapState.askUseExistingSsss;
|
||||||
|
} else if (badSecrets().isNotEmpty) {
|
||||||
|
state = BootstrapState.askBadSsss;
|
||||||
|
} else {
|
||||||
|
migrateOldSsss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void useExistingSsss(bool use) {
|
||||||
|
if (state != BootstrapState.askUseExistingSsss) {
|
||||||
|
throw Exception('Wrong State');
|
||||||
|
}
|
||||||
|
if (use) {
|
||||||
|
newSsssKey = encryption.ssss.open(encryption.ssss.defaultKeyId);
|
||||||
|
state = BootstrapState.openExistingSsss;
|
||||||
|
} else if (badSecrets().isNotEmpty) {
|
||||||
|
state = BootstrapState.askBadSsss;
|
||||||
|
} else {
|
||||||
|
migrateOldSsss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ignoreBadSecrets(bool ignore) {
|
||||||
|
if (state != BootstrapState.askBadSsss) {
|
||||||
|
throw Exception('Wrong State');
|
||||||
|
}
|
||||||
|
if (ignore) {
|
||||||
|
migrateOldSsss();
|
||||||
|
} else {
|
||||||
|
// that's it, folks. We can't do anything here
|
||||||
|
state = BootstrapState.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void migrateOldSsss() {
|
||||||
|
final keys = allNeededKeys();
|
||||||
|
oldSsssKeys = <String, OpenSSSS>{};
|
||||||
|
try {
|
||||||
|
for (final key in keys) {
|
||||||
|
oldSsssKeys[key] = encryption.ssss.open(key);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// very bad
|
||||||
|
Logs.error(
|
||||||
|
'[Bootstrapping] Error construction ssss key: ' + e.toString());
|
||||||
|
state = BootstrapState.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state = BootstrapState.askUnlockSsss;
|
||||||
|
}
|
||||||
|
|
||||||
|
void unlockedSsss() {
|
||||||
|
if (state != BootstrapState.askUnlockSsss) {
|
||||||
|
throw Exception('Wrong State');
|
||||||
|
}
|
||||||
|
state = BootstrapState.askNewSsss;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> newSsss([String passphrase]) async {
|
||||||
|
if (state != BootstrapState.askNewSsss) {
|
||||||
|
throw Exception('Wrong State');
|
||||||
|
}
|
||||||
|
state = BootstrapState.loading;
|
||||||
|
try {
|
||||||
|
newSsssKey = await encryption.ssss.createKey(passphrase);
|
||||||
|
if (oldSsssKeys != null) {
|
||||||
|
// alright, we have to re-encrypt old secrets with the new key
|
||||||
|
final secrets = analyzeSecrets();
|
||||||
|
final removeKey = (String key) {
|
||||||
|
final s = secrets.entries
|
||||||
|
.where((e) => e.value.contains(key))
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toSet();
|
||||||
|
secrets.removeWhere((k, v) => v.contains(key));
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
secretMap = <String, String>{};
|
||||||
|
for (final entry in oldSsssKeys.entries) {
|
||||||
|
final key = entry.value;
|
||||||
|
final keyId = entry.key;
|
||||||
|
if (!key.isUnlocked) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (final s in removeKey(keyId)) {
|
||||||
|
secretMap[s] = await key.getStored(s);
|
||||||
|
await newSsssKey.store(s, secretMap[s], add: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// alright, we re-encrypted all the secrets. We delete the dead weight only *after* we set our key to the default key
|
||||||
|
}
|
||||||
|
final updatedAccountData = client.onSync.stream.firstWhere((syncUpdate) =>
|
||||||
|
syncUpdate.accountData.any((accountData) =>
|
||||||
|
accountData.type == EventTypes.SecretStorageDefaultKey));
|
||||||
|
await encryption.ssss.setDefaultKeyId(newSsssKey.keyId);
|
||||||
|
await updatedAccountData;
|
||||||
|
if (oldSsssKeys != null) {
|
||||||
|
for (final entry in secretMap.entries) {
|
||||||
|
await newSsssKey.validateAndStripOtherKeys(entry.key, entry.value);
|
||||||
|
}
|
||||||
|
// and make super sure we have everything cached
|
||||||
|
await newSsssKey.maybeCacheAll();
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error(
|
||||||
|
'[Bootstrapping] Error trying to migrate old secrets: ' +
|
||||||
|
e.toString(),
|
||||||
|
s);
|
||||||
|
state = BootstrapState.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// alright, we successfully migrated all secrets, if needed
|
||||||
|
|
||||||
|
checkCrossSigning();
|
||||||
|
}
|
||||||
|
|
||||||
|
void openExistingSsss() {
|
||||||
|
if (state != BootstrapState.openExistingSsss) {
|
||||||
|
throw 'Bad State';
|
||||||
|
}
|
||||||
|
if (!newSsssKey.isUnlocked) {
|
||||||
|
throw 'Key not unlocked';
|
||||||
|
}
|
||||||
|
checkCrossSigning();
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkCrossSigning() {
|
||||||
|
// so, let's see if we have cross signing set up
|
||||||
|
if (encryption.crossSigning.enabled) {
|
||||||
|
// cross signing present, ask for wipe
|
||||||
|
state = BootstrapState.askWipeCrossSigning;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// no cross signing present
|
||||||
|
state = BootstrapState.askSetupCrossSigning;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wipeCrossSigning(bool wipe) {
|
||||||
|
if (state != BootstrapState.askWipeCrossSigning) {
|
||||||
|
throw 'Bad State';
|
||||||
|
}
|
||||||
|
if (wipe) {
|
||||||
|
state = BootstrapState.askSetupCrossSigning;
|
||||||
|
} else {
|
||||||
|
checkOnlineKeyBackup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> askSetupCrossSigning(
|
||||||
|
{bool setupMasterKey = false,
|
||||||
|
bool setupSelfSigningKey = false,
|
||||||
|
bool setupUserSigningKey = false}) async {
|
||||||
|
if (state != BootstrapState.askSetupCrossSigning) {
|
||||||
|
throw 'Bad State';
|
||||||
|
}
|
||||||
|
if (!setupMasterKey && !setupSelfSigningKey && !setupUserSigningKey) {
|
||||||
|
checkOnlineKeyBackup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Uint8List masterSigningKey;
|
||||||
|
final secretsToStore = <String, String>{};
|
||||||
|
MatrixCrossSigningKey masterKey;
|
||||||
|
MatrixCrossSigningKey selfSigningKey;
|
||||||
|
MatrixCrossSigningKey userSigningKey;
|
||||||
|
String masterPub;
|
||||||
|
if (setupMasterKey) {
|
||||||
|
final master = olm.PkSigning();
|
||||||
|
try {
|
||||||
|
masterSigningKey = master.generate_seed();
|
||||||
|
masterPub = master.init_with_seed(masterSigningKey);
|
||||||
|
final json = <String, dynamic>{
|
||||||
|
'user_id': client.userID,
|
||||||
|
'usage': ['master'],
|
||||||
|
'keys': <String, dynamic>{
|
||||||
|
'ed25519:$masterPub': masterPub,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
masterKey = MatrixCrossSigningKey.fromJson(json);
|
||||||
|
secretsToStore[MASTER_KEY] = base64.encode(masterSigningKey);
|
||||||
|
} finally {
|
||||||
|
master.free();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
masterSigningKey =
|
||||||
|
base64.decode(await newSsssKey.getStored(MASTER_KEY) ?? '');
|
||||||
|
if (masterSigningKey == null || masterSigningKey.isEmpty) {
|
||||||
|
// no master signing key :(
|
||||||
|
throw 'No master key';
|
||||||
|
}
|
||||||
|
final master = olm.PkSigning();
|
||||||
|
try {
|
||||||
|
masterPub = master.init_with_seed(masterSigningKey);
|
||||||
|
} finally {
|
||||||
|
master.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final _sign = (Map<String, dynamic> object) {
|
||||||
|
final keyObj = olm.PkSigning();
|
||||||
|
try {
|
||||||
|
keyObj.init_with_seed(masterSigningKey);
|
||||||
|
return keyObj.sign(String.fromCharCodes(canonicalJson.encode(object)));
|
||||||
|
} finally {
|
||||||
|
keyObj.free();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (setupSelfSigningKey) {
|
||||||
|
final selfSigning = olm.PkSigning();
|
||||||
|
try {
|
||||||
|
final selfSigningPriv = selfSigning.generate_seed();
|
||||||
|
final selfSigningPub = selfSigning.init_with_seed(selfSigningPriv);
|
||||||
|
final json = <String, dynamic>{
|
||||||
|
'user_id': client.userID,
|
||||||
|
'usage': ['self_signing'],
|
||||||
|
'keys': <String, dynamic>{
|
||||||
|
'ed25519:$selfSigningPub': selfSigningPub,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
final signature = _sign(json);
|
||||||
|
json['signatures'] = <String, dynamic>{
|
||||||
|
client.userID: <String, dynamic>{
|
||||||
|
'ed25519:$masterPub': signature,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
selfSigningKey = MatrixCrossSigningKey.fromJson(json);
|
||||||
|
secretsToStore[SELF_SIGNING_KEY] = base64.encode(selfSigningPriv);
|
||||||
|
} finally {
|
||||||
|
selfSigning.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (setupUserSigningKey) {
|
||||||
|
final userSigning = olm.PkSigning();
|
||||||
|
try {
|
||||||
|
final userSigningPriv = userSigning.generate_seed();
|
||||||
|
final userSigningPub = userSigning.init_with_seed(userSigningPriv);
|
||||||
|
final json = <String, dynamic>{
|
||||||
|
'user_id': client.userID,
|
||||||
|
'usage': ['user_signing'],
|
||||||
|
'keys': <String, dynamic>{
|
||||||
|
'ed25519:$userSigningPub': userSigningPub,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
final signature = _sign(json);
|
||||||
|
json['signatures'] = <String, dynamic>{
|
||||||
|
client.userID: <String, dynamic>{
|
||||||
|
'ed25519:$masterPub': signature,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
userSigningKey = MatrixCrossSigningKey.fromJson(json);
|
||||||
|
secretsToStore[USER_SIGNING_KEY] = base64.encode(userSigningPriv);
|
||||||
|
} finally {
|
||||||
|
userSigning.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// upload the keys!
|
||||||
|
await client.uiaRequestBackground(
|
||||||
|
(Map<String, dynamic> auth) => client.uploadDeviceSigningKeys(
|
||||||
|
masterKey: masterKey,
|
||||||
|
selfSigningKey: selfSigningKey,
|
||||||
|
userSigningKey: userSigningKey,
|
||||||
|
auth: auth,
|
||||||
|
));
|
||||||
|
// aaaand set the SSSS secrets
|
||||||
|
final futures = <Future<void>>[];
|
||||||
|
if (masterKey != null) {
|
||||||
|
futures.add(client.onSync.stream.firstWhere((syncUpdate) =>
|
||||||
|
client.userDeviceKeys.containsKey(client.userID) &&
|
||||||
|
client.userDeviceKeys[client.userID].masterKey != null &&
|
||||||
|
client.userDeviceKeys[client.userID].masterKey.ed25519Key ==
|
||||||
|
masterKey.publicKey));
|
||||||
|
}
|
||||||
|
for (final entry in secretsToStore.entries) {
|
||||||
|
futures.add(client.onSync.stream.firstWhere((syncUpdate) => syncUpdate
|
||||||
|
.accountData
|
||||||
|
.any((accountData) => accountData.type == entry.key)));
|
||||||
|
await newSsssKey.store(entry.key, entry.value);
|
||||||
|
}
|
||||||
|
for (final f in futures) {
|
||||||
|
await f;
|
||||||
|
}
|
||||||
|
final keysToSign = <SignableKey>[];
|
||||||
|
if (masterKey != null) {
|
||||||
|
if (client.userDeviceKeys[client.userID].masterKey.ed25519Key !=
|
||||||
|
masterKey.publicKey) {
|
||||||
|
throw 'ERROR: New master key does not match up!';
|
||||||
|
}
|
||||||
|
await client.userDeviceKeys[client.userID].masterKey
|
||||||
|
.setVerified(true, false);
|
||||||
|
keysToSign.add(client.userDeviceKeys[client.userID].masterKey);
|
||||||
|
}
|
||||||
|
// and sign ourself!
|
||||||
|
if (selfSigningKey != null) {
|
||||||
|
keysToSign.add(
|
||||||
|
client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]);
|
||||||
|
}
|
||||||
|
await encryption.crossSigning.sign(keysToSign);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error(
|
||||||
|
'[Bootstrapping] Error setting up cross signing: ' + e.toString(), s);
|
||||||
|
state = BootstrapState.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkOnlineKeyBackup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkOnlineKeyBackup() {
|
||||||
|
// check if we have online key backup set up
|
||||||
|
if (encryption.keyManager.enabled) {
|
||||||
|
state = BootstrapState.askWipeOnlineKeyBackup;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state = BootstrapState.askSetupOnlineKeyBackup;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wipeOnlineKeyBackup(bool wipe) {
|
||||||
|
if (state != BootstrapState.askWipeOnlineKeyBackup) {
|
||||||
|
throw 'Bad State';
|
||||||
|
}
|
||||||
|
if (wipe) {
|
||||||
|
state = BootstrapState.askSetupOnlineKeyBackup;
|
||||||
|
} else {
|
||||||
|
state = BootstrapState.done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> askSetupOnlineKeyBackup(bool setup) async {
|
||||||
|
if (state != BootstrapState.askSetupOnlineKeyBackup) {
|
||||||
|
throw 'Bad State';
|
||||||
|
}
|
||||||
|
if (!setup) {
|
||||||
|
state = BootstrapState.done;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final keyObj = olm.PkDecryption();
|
||||||
|
String pubKey;
|
||||||
|
Uint8List privKey;
|
||||||
|
try {
|
||||||
|
pubKey = keyObj.generate_key();
|
||||||
|
privKey = keyObj.get_private_key();
|
||||||
|
} finally {
|
||||||
|
keyObj.free();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// create the new backup version
|
||||||
|
await client.createRoomKeysBackup(
|
||||||
|
RoomKeysAlgorithmType.v1Curve25519AesSha2,
|
||||||
|
<String, dynamic>{
|
||||||
|
'public_key': pubKey,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// store the secret
|
||||||
|
await newSsssKey.store(MEGOLM_KEY, base64.encode(privKey));
|
||||||
|
// and finally set all megolm keys as needing to be uploaded again
|
||||||
|
await client.database?.markInboundGroupSessionsAsNeedingUpload(client.id);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error(
|
||||||
|
'[Bootstrapping] Error setting up online key backup: ' + e.toString(),
|
||||||
|
s);
|
||||||
|
state = BootstrapState.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state = BootstrapState.done;
|
||||||
|
}
|
||||||
|
|
||||||
|
set state(BootstrapState newState) {
|
||||||
|
if (state != BootstrapState.error) {
|
||||||
|
_state = newState;
|
||||||
|
}
|
||||||
|
if (onUpdate != null) {
|
||||||
|
onUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -362,7 +362,10 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openSSSS(
|
Future<void> openSSSS(
|
||||||
{String passphrase, String recoveryKey, bool skip = false}) async {
|
{String passphrase,
|
||||||
|
String recoveryKey,
|
||||||
|
String keyOrPassphrase,
|
||||||
|
bool skip = false}) async {
|
||||||
final next = () {
|
final next = () {
|
||||||
if (_nextAction == 'request') {
|
if (_nextAction == 'request') {
|
||||||
sendStart();
|
sendStart();
|
||||||
|
|
@ -378,8 +381,11 @@ class KeyVerification {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final handle = encryption.ssss.open('m.cross_signing.user_signing');
|
final handle = encryption.ssss.open(EventTypes.CrossSigningUserSigning);
|
||||||
await handle.unlock(passphrase: passphrase, recoveryKey: recoveryKey);
|
await handle.unlock(
|
||||||
|
passphrase: passphrase,
|
||||||
|
recoveryKey: recoveryKey,
|
||||||
|
keyOrPassphrase: keyOrPassphrase);
|
||||||
await handle.maybeCacheAll();
|
await handle.maybeCacheAll();
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export 'src/utils/receipt.dart';
|
||||||
export 'src/utils/states_map.dart';
|
export 'src/utils/states_map.dart';
|
||||||
export 'src/utils/sync_update_extension.dart';
|
export 'src/utils/sync_update_extension.dart';
|
||||||
export 'src/utils/to_device_event.dart';
|
export 'src/utils/to_device_event.dart';
|
||||||
|
export 'src/utils/uia_request.dart';
|
||||||
export 'src/client.dart';
|
export 'src/client.dart';
|
||||||
export 'src/event.dart';
|
export 'src/event.dart';
|
||||||
export 'src/room.dart';
|
export 'src/room.dart';
|
||||||
|
|
|
||||||
|
|
@ -1470,14 +1470,16 @@ class MatrixApi {
|
||||||
MatrixCrossSigningKey masterKey,
|
MatrixCrossSigningKey masterKey,
|
||||||
MatrixCrossSigningKey selfSigningKey,
|
MatrixCrossSigningKey selfSigningKey,
|
||||||
MatrixCrossSigningKey userSigningKey,
|
MatrixCrossSigningKey userSigningKey,
|
||||||
|
Map<String, dynamic> auth,
|
||||||
}) async {
|
}) async {
|
||||||
await request(
|
await request(
|
||||||
RequestType.POST,
|
RequestType.POST,
|
||||||
'/client/r0/keys/device_signing/upload',
|
'/client/unstable/keys/device_signing/upload',
|
||||||
data: {
|
data: {
|
||||||
'master_key': masterKey.toJson(),
|
if (masterKey != null) 'master_key': masterKey.toJson(),
|
||||||
'self_signing_key': selfSigningKey.toJson(),
|
if (selfSigningKey != null) 'self_signing_key': selfSigningKey.toJson(),
|
||||||
'user_signing_key': userSigningKey.toJson(),
|
if (userSigningKey != null) 'user_signing_key': userSigningKey.toJson(),
|
||||||
|
if (auth != null) 'auth': auth,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,5 @@ abstract class AlgorithmTypes {
|
||||||
'm.secret_storage.v1.aes-hmac-sha2';
|
'm.secret_storage.v1.aes-hmac-sha2';
|
||||||
static const String megolmBackupV1Curve25519AesSha2 =
|
static const String megolmBackupV1Curve25519AesSha2 =
|
||||||
'm.megolm_backup.v1.curve25519-aes-sha2';
|
'm.megolm_backup.v1.curve25519-aes-sha2';
|
||||||
|
static const String pbkdf2 = 'm.pbkdf2';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,10 @@ abstract class EventTypes {
|
||||||
static const String CallCandidates = 'm.call.candidates';
|
static const String CallCandidates = 'm.call.candidates';
|
||||||
static const String CallHangup = 'm.call.hangup';
|
static const String CallHangup = 'm.call.hangup';
|
||||||
static const String Unknown = 'm.unknown';
|
static const String Unknown = 'm.unknown';
|
||||||
|
|
||||||
|
static const String CrossSigningSelfSigning = 'm.cross_signing.self_signing';
|
||||||
|
static const String CrossSigningUserSigning = 'm.cross_signing.user_signing';
|
||||||
|
static const String MegolmBackup = 'm.megolm_backup.v1';
|
||||||
|
static const String SecretStorageDefaultKey = 'm.secret_storage.default_key';
|
||||||
|
static String secretStorageKey(String keyId) => 'm.secret_storage.key.$keyId';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import '../matrix_api/utils/logs.dart';
|
||||||
import 'utils/matrix_file.dart';
|
import 'utils/matrix_file.dart';
|
||||||
import 'utils/room_update.dart';
|
import 'utils/room_update.dart';
|
||||||
import 'utils/to_device_event.dart';
|
import 'utils/to_device_event.dart';
|
||||||
|
import 'utils/uia_request.dart';
|
||||||
|
|
||||||
typedef RoomSorter = int Function(Room a, Room b);
|
typedef RoomSorter = int Function(Room a, Room b);
|
||||||
|
|
||||||
|
|
@ -409,6 +410,24 @@ class Client extends MatrixApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<T> uiaRequestBackground<T>(
|
||||||
|
Future<T> Function(Map<String, dynamic> auth) request) {
|
||||||
|
final completer = Completer<T>();
|
||||||
|
UiaRequest uia;
|
||||||
|
uia = UiaRequest(
|
||||||
|
request: request,
|
||||||
|
onDone: () {
|
||||||
|
if (uia.done) {
|
||||||
|
completer.complete(uia.result);
|
||||||
|
} else if (uia.fail) {
|
||||||
|
completer.completeError(uia.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
onUiaRequest.add(uia);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the user's own displayname and avatar url. In Matrix it is possible that
|
/// Returns the user's own displayname and avatar url. In Matrix it is possible that
|
||||||
/// one user can have different displaynames and avatar urls in different rooms. So
|
/// one user can have different displaynames and avatar urls in different rooms. So
|
||||||
/// this endpoint first checks if the profile is the same in all rooms. If not, the
|
/// this endpoint first checks if the profile is the same in all rooms. If not, the
|
||||||
|
|
@ -590,6 +609,11 @@ class Client extends MatrixApi {
|
||||||
final StreamController<KeyVerification> onKeyVerificationRequest =
|
final StreamController<KeyVerification> onKeyVerificationRequest =
|
||||||
StreamController.broadcast();
|
StreamController.broadcast();
|
||||||
|
|
||||||
|
/// When the library calls an endpoint that needs UIA the `UiaRequest` is passed down this screen.
|
||||||
|
/// The client can open a UIA prompt based on this.
|
||||||
|
final StreamController<UiaRequest> onUiaRequest =
|
||||||
|
StreamController.broadcast();
|
||||||
|
|
||||||
/// How long should the app wait until it retrys the synchronisation after
|
/// How long should the app wait until it retrys the synchronisation after
|
||||||
/// an error?
|
/// an error?
|
||||||
int syncErrorTimeoutSec = 3;
|
int syncErrorTimeoutSec = 3;
|
||||||
|
|
|
||||||
|
|
@ -6347,6 +6347,15 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> markInboundGroupSessionsAsNeedingUpload(int client_id) {
|
||||||
|
return customUpdate(
|
||||||
|
'UPDATE inbound_group_sessions SET uploaded = false WHERE client_id = :client_id',
|
||||||
|
variables: [Variable.withInt(client_id)],
|
||||||
|
updates: {inboundGroupSessions},
|
||||||
|
updateKind: UpdateKind.update,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> storeUserDeviceKeysInfo(
|
Future<int> storeUserDeviceKeysInfo(
|
||||||
int client_id, String user_id, bool outdated) {
|
int client_id, String user_id, bool outdated) {
|
||||||
return customInsert(
|
return customInsert(
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,7 @@ storeInboundGroupSession: INSERT OR REPLACE INTO inbound_group_sessions (client_
|
||||||
updateInboundGroupSessionIndexes: UPDATE inbound_group_sessions SET indexes = :indexes WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
updateInboundGroupSessionIndexes: UPDATE inbound_group_sessions SET indexes = :indexes WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
||||||
getInboundGroupSessionsToUpload: SELECT * FROM inbound_group_sessions WHERE uploaded = false LIMIT 500;
|
getInboundGroupSessionsToUpload: SELECT * FROM inbound_group_sessions WHERE uploaded = false LIMIT 500;
|
||||||
markInboundGroupSessionAsUploaded: UPDATE inbound_group_sessions SET uploaded = true WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
markInboundGroupSessionAsUploaded: UPDATE inbound_group_sessions SET uploaded = true WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
||||||
|
markInboundGroupSessionsAsNeedingUpload: UPDATE inbound_group_sessions SET uploaded = false WHERE client_id = :client_id;
|
||||||
storeUserDeviceKeysInfo: INSERT OR REPLACE INTO user_device_keys (client_id, user_id, outdated) VALUES (:client_id, :user_id, :outdated);
|
storeUserDeviceKeysInfo: INSERT OR REPLACE INTO user_device_keys (client_id, user_id, outdated) VALUES (:client_id, :user_id, :outdated);
|
||||||
setVerifiedUserDeviceKey: UPDATE user_device_keys_key SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
setVerifiedUserDeviceKey: UPDATE user_device_keys_key SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
||||||
setBlockedUserDeviceKey: UPDATE user_device_keys_key SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
setBlockedUserDeviceKey: UPDATE user_device_keys_key SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2020 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 '../../famedlysdk.dart';
|
||||||
|
|
||||||
|
class UiaRequest<T> {
|
||||||
|
void Function() onUpdate;
|
||||||
|
void Function() onDone;
|
||||||
|
final Future<T> Function(Map<String, dynamic> auth) request;
|
||||||
|
String session;
|
||||||
|
bool done = false;
|
||||||
|
bool fail = false;
|
||||||
|
T result;
|
||||||
|
Exception error;
|
||||||
|
Set<String> nextStages = <String>{};
|
||||||
|
Map<String, dynamic> params = <String, dynamic>{};
|
||||||
|
UiaRequest({this.onUpdate, this.request, this.onDone}) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> run([Map<String, dynamic> auth]) async {
|
||||||
|
try {
|
||||||
|
auth ??= <String, dynamic>{};
|
||||||
|
if (session != null) {
|
||||||
|
auth['session'] = session;
|
||||||
|
}
|
||||||
|
final res = await request(auth);
|
||||||
|
done = true;
|
||||||
|
result = res;
|
||||||
|
return res;
|
||||||
|
} on MatrixException catch (err) {
|
||||||
|
if (!(err.session is String)) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
session ??= err.session;
|
||||||
|
final completed = err.completedAuthenticationFlows ?? <String>[];
|
||||||
|
final flows = err.authenticationFlows ?? <AuthenticationFlow>[];
|
||||||
|
params = err.authenticationParams ?? <String, dynamic>{};
|
||||||
|
nextStages = getNextStages(flows, completed);
|
||||||
|
if (nextStages.isEmpty) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
error = err is Exception ? err : Exception(err);
|
||||||
|
fail = true;
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (onUpdate != null) {
|
||||||
|
onUpdate();
|
||||||
|
}
|
||||||
|
if ((fail || done) && onDone != null) {
|
||||||
|
onDone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> completeStage(String type, [Map<String, dynamic> auth]) async {
|
||||||
|
auth ??= <String, dynamic>{};
|
||||||
|
auth['type'] = type;
|
||||||
|
return await run(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> getNextStages(
|
||||||
|
List<AuthenticationFlow> flows, List<String> completed) {
|
||||||
|
final nextStages = <String>{};
|
||||||
|
for (final flow in flows) {
|
||||||
|
final stages = flow.stages;
|
||||||
|
final nextStage = stages[completed.length];
|
||||||
|
if (nextStage != null) {
|
||||||
|
var stagesValid = true;
|
||||||
|
for (var i = 0; i < completed.length; i++) {
|
||||||
|
if (stages[i] != completed[i]) {
|
||||||
|
stagesValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stagesValid) {
|
||||||
|
nextStages.add(nextStage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextStages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -97,7 +97,7 @@ void main() {
|
||||||
masterKey.setDirectVerified(true);
|
masterKey.setDirectVerified(true);
|
||||||
// we need to populate the ssss cache to be able to test signing easily
|
// we need to populate the ssss cache to be able to test signing easily
|
||||||
final handle = client.encryption.ssss.open();
|
final handle = client.encryption.ssss.open();
|
||||||
handle.unlock(recoveryKey: SSSS_KEY);
|
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||||
await handle.maybeCacheAll();
|
await handle.maybeCacheAll();
|
||||||
|
|
||||||
expect(key.verified, true);
|
expect(key.verified, true);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
/*
|
||||||
|
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||||
|
* Copyright (C) 2020 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:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/encryption.dart';
|
||||||
|
import 'package:famedlysdk/matrix_api/utils/logs.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
import '../fake_client.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Bootstrap', () {
|
||||||
|
var olmEnabled = true;
|
||||||
|
try {
|
||||||
|
olm.init();
|
||||||
|
olm.Account();
|
||||||
|
} catch (_) {
|
||||||
|
olmEnabled = false;
|
||||||
|
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||||
|
}
|
||||||
|
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||||
|
|
||||||
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
Client client;
|
||||||
|
Map<String, dynamic> oldSecret;
|
||||||
|
String origKeyId;
|
||||||
|
|
||||||
|
test('setupClient', () async {
|
||||||
|
client = await getClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setup', () async {
|
||||||
|
Bootstrap bootstrap;
|
||||||
|
bootstrap = client.encryption.bootstrap(
|
||||||
|
onUpdate: () async {
|
||||||
|
while (bootstrap == null) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 5));
|
||||||
|
}
|
||||||
|
if (bootstrap.state == BootstrapState.askWipeSsss) {
|
||||||
|
bootstrap.wipeSsss(true);
|
||||||
|
} else if (bootstrap.state == BootstrapState.askNewSsss) {
|
||||||
|
await bootstrap.newSsss('foxies');
|
||||||
|
} else if (bootstrap.state == BootstrapState.askWipeCrossSigning) {
|
||||||
|
bootstrap.wipeCrossSigning(true);
|
||||||
|
} else if (bootstrap.state == BootstrapState.askSetupCrossSigning) {
|
||||||
|
await bootstrap.askSetupCrossSigning(
|
||||||
|
setupMasterKey: true,
|
||||||
|
setupSelfSigningKey: true,
|
||||||
|
setupUserSigningKey: true,
|
||||||
|
);
|
||||||
|
} else if (bootstrap.state == BootstrapState.askWipeOnlineKeyBackup) {
|
||||||
|
bootstrap.wipeOnlineKeyBackup(true);
|
||||||
|
} else if (bootstrap.state ==
|
||||||
|
BootstrapState.askSetupOnlineKeyBackup) {
|
||||||
|
await bootstrap.askSetupOnlineKeyBackup(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
while (bootstrap.state != BootstrapState.done) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
}
|
||||||
|
final defaultKey = client.encryption.ssss.open();
|
||||||
|
await defaultKey.unlock(passphrase: 'foxies');
|
||||||
|
|
||||||
|
// test all the x-signing keys match up
|
||||||
|
for (final keyType in {'master', 'user_signing', 'self_signing'}) {
|
||||||
|
final privateKey = base64
|
||||||
|
.decode(await defaultKey.getStored('m.cross_signing.$keyType'));
|
||||||
|
final keyObj = olm.PkSigning();
|
||||||
|
try {
|
||||||
|
final pubKey = keyObj.init_with_seed(privateKey);
|
||||||
|
expect(
|
||||||
|
pubKey,
|
||||||
|
client.userDeviceKeys[client.userID]
|
||||||
|
.getCrossSigningKey(keyType)
|
||||||
|
.publicKey);
|
||||||
|
} finally {
|
||||||
|
keyObj.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await defaultKey.store('foxes', 'floof');
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
oldSecret = json.decode(json.encode(client.accountData['foxes'].content));
|
||||||
|
origKeyId = defaultKey.keyId;
|
||||||
|
}, timeout: Timeout(Duration(minutes: 2)));
|
||||||
|
|
||||||
|
test('change recovery passphrase', () async {
|
||||||
|
Bootstrap bootstrap;
|
||||||
|
bootstrap = client.encryption.bootstrap(
|
||||||
|
onUpdate: () async {
|
||||||
|
while (bootstrap == null) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 5));
|
||||||
|
}
|
||||||
|
if (bootstrap.state == BootstrapState.askWipeSsss) {
|
||||||
|
bootstrap.wipeSsss(false);
|
||||||
|
} else if (bootstrap.state == BootstrapState.askUseExistingSsss) {
|
||||||
|
bootstrap.useExistingSsss(false);
|
||||||
|
} else if (bootstrap.state == BootstrapState.askUnlockSsss) {
|
||||||
|
await bootstrap.oldSsssKeys[client.encryption.ssss.defaultKeyId]
|
||||||
|
.unlock(passphrase: 'foxies');
|
||||||
|
bootstrap.unlockedSsss();
|
||||||
|
} else if (bootstrap.state == BootstrapState.askNewSsss) {
|
||||||
|
await bootstrap.newSsss('newfoxies');
|
||||||
|
} else if (bootstrap.state == BootstrapState.askWipeCrossSigning) {
|
||||||
|
bootstrap.wipeCrossSigning(false);
|
||||||
|
} else if (bootstrap.state == BootstrapState.askWipeOnlineKeyBackup) {
|
||||||
|
bootstrap.wipeOnlineKeyBackup(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
while (bootstrap.state != BootstrapState.done) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
}
|
||||||
|
final defaultKey = client.encryption.ssss.open();
|
||||||
|
await defaultKey.unlock(passphrase: 'newfoxies');
|
||||||
|
|
||||||
|
// test all the x-signing keys match up
|
||||||
|
for (final keyType in {'master', 'user_signing', 'self_signing'}) {
|
||||||
|
final privateKey = base64
|
||||||
|
.decode(await defaultKey.getStored('m.cross_signing.$keyType'));
|
||||||
|
final keyObj = olm.PkSigning();
|
||||||
|
try {
|
||||||
|
final pubKey = keyObj.init_with_seed(privateKey);
|
||||||
|
expect(
|
||||||
|
pubKey,
|
||||||
|
client.userDeviceKeys[client.userID]
|
||||||
|
.getCrossSigningKey(keyType)
|
||||||
|
.publicKey);
|
||||||
|
} finally {
|
||||||
|
keyObj.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await defaultKey.getStored('foxes'), 'floof');
|
||||||
|
}, timeout: Timeout(Duration(minutes: 2)));
|
||||||
|
|
||||||
|
test('change passphrase with multiple keys', () async {
|
||||||
|
await client.setAccountData(client.userID, 'foxes', oldSecret);
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
Bootstrap bootstrap;
|
||||||
|
bootstrap = client.encryption.bootstrap(
|
||||||
|
onUpdate: () async {
|
||||||
|
while (bootstrap == null) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 5));
|
||||||
|
}
|
||||||
|
if (bootstrap.state == BootstrapState.askWipeSsss) {
|
||||||
|
bootstrap.wipeSsss(false);
|
||||||
|
} else if (bootstrap.state == BootstrapState.askUseExistingSsss) {
|
||||||
|
bootstrap.useExistingSsss(false);
|
||||||
|
} else if (bootstrap.state == BootstrapState.askUnlockSsss) {
|
||||||
|
await bootstrap.oldSsssKeys[client.encryption.ssss.defaultKeyId]
|
||||||
|
.unlock(passphrase: 'newfoxies');
|
||||||
|
await bootstrap.oldSsssKeys[origKeyId].unlock(passphrase: 'foxies');
|
||||||
|
bootstrap.unlockedSsss();
|
||||||
|
} else if (bootstrap.state == BootstrapState.askNewSsss) {
|
||||||
|
await bootstrap.newSsss('supernewfoxies');
|
||||||
|
} else if (bootstrap.state == BootstrapState.askWipeCrossSigning) {
|
||||||
|
bootstrap.wipeCrossSigning(false);
|
||||||
|
} else if (bootstrap.state == BootstrapState.askWipeOnlineKeyBackup) {
|
||||||
|
bootstrap.wipeOnlineKeyBackup(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
while (bootstrap.state != BootstrapState.done) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
}
|
||||||
|
final defaultKey = client.encryption.ssss.open();
|
||||||
|
await defaultKey.unlock(passphrase: 'supernewfoxies');
|
||||||
|
|
||||||
|
// test all the x-signing keys match up
|
||||||
|
for (final keyType in {'master', 'user_signing', 'self_signing'}) {
|
||||||
|
final privateKey = base64
|
||||||
|
.decode(await defaultKey.getStored('m.cross_signing.$keyType'));
|
||||||
|
final keyObj = olm.PkSigning();
|
||||||
|
try {
|
||||||
|
final pubKey = keyObj.init_with_seed(privateKey);
|
||||||
|
expect(
|
||||||
|
pubKey,
|
||||||
|
client.userDeviceKeys[client.userID]
|
||||||
|
.getCrossSigningKey(keyType)
|
||||||
|
.publicKey);
|
||||||
|
} finally {
|
||||||
|
keyObj.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await defaultKey.getStored('foxes'), 'floof');
|
||||||
|
}, timeout: Timeout(Duration(minutes: 2)));
|
||||||
|
|
||||||
|
test('setup new ssss', () async {
|
||||||
|
client.accountData.clear();
|
||||||
|
Bootstrap bootstrap;
|
||||||
|
bootstrap = client.encryption.bootstrap(
|
||||||
|
onUpdate: () async {
|
||||||
|
while (bootstrap == null) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 5));
|
||||||
|
}
|
||||||
|
if (bootstrap.state == BootstrapState.askNewSsss) {
|
||||||
|
await bootstrap.newSsss('thenewestfoxies');
|
||||||
|
} else if (bootstrap.state == BootstrapState.askSetupCrossSigning) {
|
||||||
|
await bootstrap.askSetupCrossSigning();
|
||||||
|
} else if (bootstrap.state ==
|
||||||
|
BootstrapState.askSetupOnlineKeyBackup) {
|
||||||
|
await bootstrap.askSetupOnlineKeyBackup(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
while (bootstrap.state != BootstrapState.done) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
}
|
||||||
|
final defaultKey = client.encryption.ssss.open();
|
||||||
|
await defaultKey.unlock(passphrase: 'thenewestfoxies');
|
||||||
|
}, timeout: Timeout(Duration(minutes: 2)));
|
||||||
|
|
||||||
|
test('bad ssss', () async {
|
||||||
|
client.accountData.clear();
|
||||||
|
await client.setAccountData(client.userID, 'foxes', oldSecret);
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
var askedBadSsss = false;
|
||||||
|
Bootstrap bootstrap;
|
||||||
|
bootstrap = client.encryption.bootstrap(
|
||||||
|
onUpdate: () async {
|
||||||
|
while (bootstrap == null) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 5));
|
||||||
|
}
|
||||||
|
if (bootstrap.state == BootstrapState.askWipeSsss) {
|
||||||
|
bootstrap.wipeSsss(false);
|
||||||
|
} else if (bootstrap.state == BootstrapState.askBadSsss) {
|
||||||
|
askedBadSsss = true;
|
||||||
|
bootstrap.ignoreBadSecrets(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
while (bootstrap.state != BootstrapState.error) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
}
|
||||||
|
expect(askedBadSsss, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dispose client', () async {
|
||||||
|
await client.dispose(closeDatabase: true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -35,7 +35,7 @@ class MockSSSS extends SSSS {
|
||||||
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
|
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
|
||||||
requestedSecrets = true;
|
requestedSecrets = true;
|
||||||
final handle = open();
|
final handle = open();
|
||||||
handle.unlock(recoveryKey: SSSS_KEY);
|
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||||
await handle.maybeCacheAll();
|
await handle.maybeCacheAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ void main() {
|
||||||
expect(client.encryption.keyManager.enabled, true);
|
expect(client.encryption.keyManager.enabled, true);
|
||||||
expect(await client.encryption.keyManager.isCached(), false);
|
expect(await client.encryption.keyManager.isCached(), false);
|
||||||
final handle = client.encryption.ssss.open();
|
final handle = client.encryption.ssss.open();
|
||||||
handle.unlock(recoveryKey: SSSS_KEY);
|
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||||
await handle.maybeCacheAll();
|
await handle.maybeCacheAll();
|
||||||
expect(await client.encryption.keyManager.isCached(), true);
|
expect(await client.encryption.keyManager.isCached(), true);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class MockSSSS extends SSSS {
|
||||||
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
|
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
|
||||||
requestedSecrets = true;
|
requestedSecrets = true;
|
||||||
final handle = open();
|
final handle = open();
|
||||||
handle.unlock(recoveryKey: SSSS_KEY);
|
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||||
await handle.maybeCacheAll();
|
await handle.maybeCacheAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +80,7 @@ void main() {
|
||||||
final handle = client.encryption.ssss.open();
|
final handle = client.encryption.ssss.open();
|
||||||
var failed = false;
|
var failed = false;
|
||||||
try {
|
try {
|
||||||
handle.unlock(passphrase: 'invalid');
|
await handle.unlock(passphrase: 'invalid');
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
failed = true;
|
failed = true;
|
||||||
}
|
}
|
||||||
|
|
@ -88,14 +88,14 @@ void main() {
|
||||||
expect(handle.isUnlocked, false);
|
expect(handle.isUnlocked, false);
|
||||||
failed = false;
|
failed = false;
|
||||||
try {
|
try {
|
||||||
handle.unlock(recoveryKey: 'invalid');
|
await handle.unlock(recoveryKey: 'invalid');
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
failed = true;
|
failed = true;
|
||||||
}
|
}
|
||||||
expect(failed, true);
|
expect(failed, true);
|
||||||
expect(handle.isUnlocked, false);
|
expect(handle.isUnlocked, false);
|
||||||
handle.unlock(passphrase: SSSS_PASSPHRASE);
|
await handle.unlock(passphrase: SSSS_PASSPHRASE);
|
||||||
handle.unlock(recoveryKey: SSSS_KEY);
|
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||||
expect(handle.isUnlocked, true);
|
expect(handle.isUnlocked, true);
|
||||||
FakeMatrixApi.calledEndpoints.clear();
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
await handle.store('best animal', 'foxies');
|
await handle.store('best animal', 'foxies');
|
||||||
|
|
@ -114,32 +114,32 @@ void main() {
|
||||||
|
|
||||||
test('cache', () async {
|
test('cache', () async {
|
||||||
final handle =
|
final handle =
|
||||||
client.encryption.ssss.open('m.cross_signing.self_signing');
|
client.encryption.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||||
handle.unlock(recoveryKey: SSSS_KEY);
|
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||||
expect(
|
expect(
|
||||||
(await client.encryption.ssss
|
(await client.encryption.ssss
|
||||||
.getCached('m.cross_signing.self_signing')) !=
|
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||||
null,
|
null,
|
||||||
false);
|
false);
|
||||||
expect(
|
expect(
|
||||||
(await client.encryption.ssss
|
(await client.encryption.ssss
|
||||||
.getCached('m.cross_signing.user_signing')) !=
|
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||||
null,
|
null,
|
||||||
false);
|
false);
|
||||||
await handle.getStored('m.cross_signing.self_signing');
|
await handle.getStored(EventTypes.CrossSigningSelfSigning);
|
||||||
expect(
|
expect(
|
||||||
(await client.encryption.ssss
|
(await client.encryption.ssss
|
||||||
.getCached('m.cross_signing.self_signing')) !=
|
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||||
null,
|
null,
|
||||||
true);
|
true);
|
||||||
await handle.maybeCacheAll();
|
await handle.maybeCacheAll();
|
||||||
expect(
|
expect(
|
||||||
(await client.encryption.ssss
|
(await client.encryption.ssss
|
||||||
.getCached('m.cross_signing.user_signing')) !=
|
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||||
null,
|
null,
|
||||||
true);
|
true);
|
||||||
expect(
|
expect(
|
||||||
(await client.encryption.ssss.getCached('m.megolm_backup.v1')) !=
|
(await client.encryption.ssss.getCached(EventTypes.MegolmBackup)) !=
|
||||||
null,
|
null,
|
||||||
true);
|
true);
|
||||||
});
|
});
|
||||||
|
|
@ -163,7 +163,7 @@ void main() {
|
||||||
content: {
|
content: {
|
||||||
'action': 'request',
|
'action': 'request',
|
||||||
'requesting_device_id': 'OTHERDEVICE',
|
'requesting_device_id': 'OTHERDEVICE',
|
||||||
'name': 'm.cross_signing.self_signing',
|
'name': EventTypes.CrossSigningSelfSigning,
|
||||||
'request_id': '1',
|
'request_id': '1',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -183,7 +183,7 @@ void main() {
|
||||||
content: {
|
content: {
|
||||||
'action': 'request',
|
'action': 'request',
|
||||||
'requesting_device_id': 'OTHERDEVICE',
|
'requesting_device_id': 'OTHERDEVICE',
|
||||||
'name': 'm.cross_signing.self_signing',
|
'name': EventTypes.CrossSigningSelfSigning,
|
||||||
'request_id': '1',
|
'request_id': '1',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -219,7 +219,7 @@ void main() {
|
||||||
content: {
|
content: {
|
||||||
'action': 'request_cancellation',
|
'action': 'request_cancellation',
|
||||||
'requesting_device_id': 'OTHERDEVICE',
|
'requesting_device_id': 'OTHERDEVICE',
|
||||||
'name': 'm.cross_signing.self_signing',
|
'name': EventTypes.CrossSigningSelfSigning,
|
||||||
'request_id': '1',
|
'request_id': '1',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -240,7 +240,7 @@ void main() {
|
||||||
content: {
|
content: {
|
||||||
'action': 'request',
|
'action': 'request',
|
||||||
'requesting_device_id': 'OTHERDEVICE',
|
'requesting_device_id': 'OTHERDEVICE',
|
||||||
'name': 'm.cross_signing.self_signing',
|
'name': EventTypes.CrossSigningSelfSigning,
|
||||||
'request_id': '1',
|
'request_id': '1',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -258,8 +258,8 @@ void main() {
|
||||||
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
|
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
|
||||||
key.setDirectVerified(true);
|
key.setDirectVerified(true);
|
||||||
final handle =
|
final handle =
|
||||||
client.encryption.ssss.open('m.cross_signing.self_signing');
|
client.encryption.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||||
handle.unlock(recoveryKey: SSSS_KEY);
|
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||||
|
|
||||||
await client.encryption.ssss.clearCache();
|
await client.encryption.ssss.clearCache();
|
||||||
client.encryption.ssss.pendingShareRequests.clear();
|
client.encryption.ssss.pendingShareRequests.clear();
|
||||||
|
|
@ -280,9 +280,9 @@ void main() {
|
||||||
|
|
||||||
// test the different validators
|
// test the different validators
|
||||||
for (final type in [
|
for (final type in [
|
||||||
'm.cross_signing.self_signing',
|
EventTypes.CrossSigningSelfSigning,
|
||||||
'm.cross_signing.user_signing',
|
EventTypes.CrossSigningUserSigning,
|
||||||
'm.megolm_backup.v1'
|
EventTypes.MegolmBackup
|
||||||
]) {
|
]) {
|
||||||
final secret = await handle.getStored(type);
|
final secret = await handle.getStored(type);
|
||||||
await client.encryption.ssss.clearCache();
|
await client.encryption.ssss.clearCache();
|
||||||
|
|
@ -378,7 +378,7 @@ void main() {
|
||||||
// validator doesn't check out
|
// validator doesn't check out
|
||||||
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('m.megolm_backup.v1', [key]);
|
await client.encryption.ssss.request(EventTypes.MegolmBackup, [key]);
|
||||||
event = ToDeviceEvent(
|
event = ToDeviceEvent(
|
||||||
sender: client.userID,
|
sender: client.userID,
|
||||||
type: 'm.secret.send',
|
type: 'm.secret.send',
|
||||||
|
|
@ -391,8 +391,8 @@ void main() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||||
expect(
|
expect(await client.encryption.ssss.getCached(EventTypes.MegolmBackup),
|
||||||
await client.encryption.ssss.getCached('m.megolm_backup.v1'), null);
|
null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('request all', () async {
|
test('request all', () async {
|
||||||
|
|
@ -417,6 +417,21 @@ void main() {
|
||||||
expect((client.encryption.ssss as MockSSSS).requestedSecrets, false);
|
expect((client.encryption.ssss as MockSSSS).requestedSecrets, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('createKey', () async {
|
||||||
|
// with passphrase
|
||||||
|
var newKey = await client.encryption.ssss.createKey('test');
|
||||||
|
expect(client.encryption.ssss.isKeyValid(newKey.keyId), true);
|
||||||
|
var testKey = await client.encryption.ssss.open(newKey.keyId);
|
||||||
|
await testKey.unlock(passphrase: 'test');
|
||||||
|
await testKey.setPrivateKey(newKey.privateKey);
|
||||||
|
|
||||||
|
// without passphrase
|
||||||
|
newKey = await client.encryption.ssss.createKey();
|
||||||
|
expect(client.encryption.ssss.isKeyValid(newKey.keyId), true);
|
||||||
|
testKey = await client.encryption.ssss.open(newKey.keyId);
|
||||||
|
await testKey.setPrivateKey(newKey.privateKey);
|
||||||
|
});
|
||||||
|
|
||||||
test('dispose client', () async {
|
test('dispose client', () async {
|
||||||
await client.dispose(closeDatabase: true);
|
await client.dispose(closeDatabase: true);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,12 @@ const pickledOlmAccount =
|
||||||
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
|
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
|
||||||
|
|
||||||
Future<Client> getClient() async {
|
Future<Client> getClient() async {
|
||||||
final client = Client('testclient',
|
final client = Client(
|
||||||
httpClient: FakeMatrixApi(), databaseBuilder: getDatabase);
|
'testclient',
|
||||||
|
httpClient: FakeMatrixApi(),
|
||||||
|
databaseBuilder: getDatabase,
|
||||||
|
);
|
||||||
|
FakeMatrixApi.client = client;
|
||||||
await client.checkHomeserver('https://fakeServer.notExisting');
|
await client.checkHomeserver('https://fakeServer.notExisting');
|
||||||
client.init(
|
client.init(
|
||||||
newToken: 'abcd',
|
newToken: 'abcd',
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart' as sdk;
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
@ -27,6 +29,7 @@ import 'package:http/testing.dart';
|
||||||
class FakeMatrixApi extends MockClient {
|
class FakeMatrixApi extends MockClient {
|
||||||
static final calledEndpoints = <String, List<dynamic>>{};
|
static final calledEndpoints = <String, List<dynamic>>{};
|
||||||
static int eventCounter = 0;
|
static int eventCounter = 0;
|
||||||
|
static sdk.Client client;
|
||||||
|
|
||||||
FakeMatrixApi()
|
FakeMatrixApi()
|
||||||
: super((request) async {
|
: super((request) async {
|
||||||
|
|
@ -87,6 +90,25 @@ class FakeMatrixApi extends MockClient {
|
||||||
res = {
|
res = {
|
||||||
'next_batch': DateTime.now().millisecondsSinceEpoch.toString
|
'next_batch': DateTime.now().millisecondsSinceEpoch.toString
|
||||||
};
|
};
|
||||||
|
} else if (method == 'PUT' &&
|
||||||
|
client != null &&
|
||||||
|
action.contains('/account_data/') &&
|
||||||
|
!action.contains('/room/')) {
|
||||||
|
final type = Uri.decodeComponent(action.split('/').last);
|
||||||
|
final syncUpdate = sdk.SyncUpdate()
|
||||||
|
..accountData = [
|
||||||
|
sdk.BasicEvent()
|
||||||
|
..content = json.decode(data)
|
||||||
|
..type = type
|
||||||
|
];
|
||||||
|
if (client.database != null) {
|
||||||
|
await client.database.transaction(() async {
|
||||||
|
await client.handleSync(syncUpdate);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await client.handleSync(syncUpdate);
|
||||||
|
}
|
||||||
|
res = {};
|
||||||
} else {
|
} else {
|
||||||
res = {
|
res = {
|
||||||
'errcode': 'M_UNRECOGNIZED',
|
'errcode': 'M_UNRECOGNIZED',
|
||||||
|
|
@ -538,7 +560,7 @@ class FakeMatrixApi extends MockClient {
|
||||||
'type': 'm.direct'
|
'type': 'm.direct'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'type': 'm.secret_storage.default_key',
|
'type': EventTypes.SecretStorageDefaultKey,
|
||||||
'content': {'key': '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'}
|
'content': {'key': '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -546,7 +568,7 @@ class FakeMatrixApi extends MockClient {
|
||||||
'content': {
|
'content': {
|
||||||
'algorithm': AlgorithmTypes.secretStorageV1AesHmcSha2,
|
'algorithm': AlgorithmTypes.secretStorageV1AesHmcSha2,
|
||||||
'passphrase': {
|
'passphrase': {
|
||||||
'algorithm': 'm.pbkdf2',
|
'algorithm': AlgorithmTypes.pbkdf2,
|
||||||
'iterations': 500000,
|
'iterations': 500000,
|
||||||
'salt': 'F4jJ80mr0Fc8mRwU9JgA3lQDyjPuZXQL'
|
'salt': 'F4jJ80mr0Fc8mRwU9JgA3lQDyjPuZXQL'
|
||||||
},
|
},
|
||||||
|
|
@ -568,7 +590,7 @@ class FakeMatrixApi extends MockClient {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'type': 'm.cross_signing.self_signing',
|
'type': EventTypes.CrossSigningSelfSigning,
|
||||||
'content': {
|
'content': {
|
||||||
'encrypted': {
|
'encrypted': {
|
||||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
||||||
|
|
@ -581,7 +603,7 @@ class FakeMatrixApi extends MockClient {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'type': 'm.cross_signing.user_signing',
|
'type': EventTypes.CrossSigningUserSigning,
|
||||||
'content': {
|
'content': {
|
||||||
'encrypted': {
|
'encrypted': {
|
||||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
||||||
|
|
@ -594,7 +616,7 @@ class FakeMatrixApi extends MockClient {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'type': 'm.megolm_backup.v1',
|
'type': EventTypes.MegolmBackup,
|
||||||
'content': {
|
'content': {
|
||||||
'encrypted': {
|
'encrypted': {
|
||||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
||||||
|
|
@ -1978,7 +2000,28 @@ class FakeMatrixApi extends MockClient {
|
||||||
'/client/r0/rooms/!localpart%3Aserver.abc/ban': (var reqI) => {},
|
'/client/r0/rooms/!localpart%3Aserver.abc/ban': (var reqI) => {},
|
||||||
'/client/r0/rooms/!localpart%3Aserver.abc/unban': (var reqI) => {},
|
'/client/r0/rooms/!localpart%3Aserver.abc/unban': (var reqI) => {},
|
||||||
'/client/r0/rooms/!localpart%3Aserver.abc/invite': (var reqI) => {},
|
'/client/r0/rooms/!localpart%3Aserver.abc/invite': (var reqI) => {},
|
||||||
'/client/r0/keys/device_signing/upload': (var reqI) => {},
|
'/client/unstable/keys/device_signing/upload': (var reqI) {
|
||||||
|
if (client != null) {
|
||||||
|
final jsonBody = json.decode(reqI);
|
||||||
|
for (final keyType in {
|
||||||
|
'master_key',
|
||||||
|
'self_signing_key',
|
||||||
|
'user_signing_key'
|
||||||
|
}) {
|
||||||
|
if (jsonBody[keyType] != null) {
|
||||||
|
final key =
|
||||||
|
sdk.CrossSigningKey.fromJson(jsonBody[keyType], client);
|
||||||
|
client.userDeviceKeys[client.userID].crossSigningKeys
|
||||||
|
.removeWhere((k, v) => v.usage.contains(key.usage.first));
|
||||||
|
client.userDeviceKeys[client.userID]
|
||||||
|
.crossSigningKeys[key.publicKey] = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// and generate a fake sync
|
||||||
|
client.handleSync(sdk.SyncUpdate());
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
'/client/r0/keys/signatures/upload': (var reqI) => {'failures': {}},
|
'/client/r0/keys/signatures/upload': (var reqI) => {'failures': {}},
|
||||||
'/client/unstable/room_keys/version': (var reqI) => {'version': '5'},
|
'/client/unstable/room_keys/version': (var reqI) => {'version': '5'},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||||
|
* Copyright (C) 2020 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:async';
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('UIA', () {
|
||||||
|
test('it should work', () async {
|
||||||
|
var completed = <String>[];
|
||||||
|
var updated = 0;
|
||||||
|
var finished = false;
|
||||||
|
final request = UiaRequest(
|
||||||
|
request: (auth) async {
|
||||||
|
if (auth['session'] != null && auth['session'] != 'foxies') {
|
||||||
|
throw MatrixException.fromJson(<String, dynamic>{});
|
||||||
|
}
|
||||||
|
if (auth['type'] == 'stage1') {
|
||||||
|
if (completed.isEmpty) {
|
||||||
|
completed.add('stage1');
|
||||||
|
}
|
||||||
|
} else if (auth['type'] == 'stage2') {
|
||||||
|
if (completed.length == 1 && completed[0] == 'stage1') {
|
||||||
|
// okay, we are done!
|
||||||
|
return 'FOXIES ARE FLOOOOOFY!!!!!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final res = <String, dynamic>{
|
||||||
|
'session': 'foxies',
|
||||||
|
'completed': completed,
|
||||||
|
'flows': [
|
||||||
|
<String, dynamic>{
|
||||||
|
'stages': ['stage1', 'stage2'],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'params': <String, dynamic>{},
|
||||||
|
};
|
||||||
|
throw MatrixException.fromJson(res);
|
||||||
|
},
|
||||||
|
onUpdate: () => updated++,
|
||||||
|
onDone: () => finished = true,
|
||||||
|
);
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(request.nextStages.contains('stage1'), true);
|
||||||
|
expect(request.nextStages.length, 1);
|
||||||
|
expect(updated, 1);
|
||||||
|
expect(finished, false);
|
||||||
|
await request.completeStage('stage1');
|
||||||
|
expect(request.nextStages.contains('stage2'), true);
|
||||||
|
expect(request.nextStages.length, 1);
|
||||||
|
expect(updated, 2);
|
||||||
|
expect(finished, false);
|
||||||
|
final res = await request.completeStage('stage2');
|
||||||
|
expect(res, 'FOXIES ARE FLOOOOOFY!!!!!');
|
||||||
|
expect(request.result, 'FOXIES ARE FLOOOOOFY!!!!!');
|
||||||
|
expect(request.done, true);
|
||||||
|
expect(updated, 3);
|
||||||
|
expect(finished, true);
|
||||||
|
});
|
||||||
|
test('it should throw errors', () async {
|
||||||
|
var updated = false;
|
||||||
|
var finished = false;
|
||||||
|
final request = UiaRequest(
|
||||||
|
request: (auth) async {
|
||||||
|
throw 'nope';
|
||||||
|
},
|
||||||
|
onUpdate: () => updated = true,
|
||||||
|
onDone: () => finished = true,
|
||||||
|
);
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(request.fail, true);
|
||||||
|
expect(updated, true);
|
||||||
|
expect(finished, true);
|
||||||
|
expect(request.error.toString(), Exception('nope').toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue