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/ssss.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 'encryption.dart';
|
||||
|
||||
const SELF_SIGNING_KEY = 'm.cross_signing.self_signing';
|
||||
const USER_SIGNING_KEY = 'm.cross_signing.user_signing';
|
||||
const SELF_SIGNING_KEY = EventTypes.CrossSigningSelfSigning;
|
||||
const USER_SIGNING_KEY = EventTypes.CrossSigningUserSigning;
|
||||
const MASTER_KEY = 'm.cross_signing.master';
|
||||
|
||||
class CrossSigning {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import 'key_manager.dart';
|
|||
import 'key_verification_manager.dart';
|
||||
import 'olm_manager.dart';
|
||||
import 'ssss.dart';
|
||||
import 'utils/bootstrap.dart';
|
||||
|
||||
class Encryption {
|
||||
final Client client;
|
||||
|
|
@ -69,6 +70,11 @@ class Encryption {
|
|||
_backgroundTasks(); // start the background tasks
|
||||
}
|
||||
|
||||
Bootstrap bootstrap({void Function() onUpdate}) => Bootstrap(
|
||||
encryption: this,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
|
||||
void handleDeviceOneTimeKeysCount(Map<String, int> 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_root.dart';
|
||||
|
||||
const MEGOLM_KEY = 'm.megolm_backup.v1';
|
||||
const MEGOLM_KEY = EventTypes.MegolmBackup;
|
||||
|
||||
class KeyManager {
|
||||
final Encryption encryption;
|
||||
|
|
|
|||
|
|
@ -30,20 +30,28 @@ import '../famedlysdk.dart';
|
|||
import '../matrix_api.dart';
|
||||
import '../src/database/database.dart';
|
||||
import '../matrix_api/utils/logs.dart';
|
||||
import '../src/utils/run_in_background.dart';
|
||||
import 'encryption.dart';
|
||||
|
||||
const CACHE_TYPES = <String>[
|
||||
'm.cross_signing.self_signing',
|
||||
'm.cross_signing.user_signing',
|
||||
'm.megolm_backup.v1'
|
||||
];
|
||||
const CACHE_TYPES = <String>{
|
||||
EventTypes.CrossSigningSelfSigning,
|
||||
EventTypes.CrossSigningUserSigning,
|
||||
EventTypes.MegolmBackup,
|
||||
};
|
||||
|
||||
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';
|
||||
const BASE58_ALPHABET =
|
||||
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
const base58 = Base58Codec(BASE58_ALPHABET);
|
||||
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 {
|
||||
final Encryption encryption;
|
||||
|
|
@ -129,17 +137,16 @@ class SSSS {
|
|||
}
|
||||
}
|
||||
|
||||
if (result.length !=
|
||||
OLM_RECOVERY_KEY_PREFIX.length + OLM_PRIVATE_KEY_LENGTH + 1) {
|
||||
if (result.length != OLM_RECOVERY_KEY_PREFIX.length + SSSS_KEY_LENGTH + 1) {
|
||||
throw 'Incorrect 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) {
|
||||
if (info.algorithm != 'm.pbkdf2') {
|
||||
if (info.algorithm != AlgorithmTypes.pbkdf2) {
|
||||
throw 'Unknown algorithm';
|
||||
}
|
||||
final generator = PBKDF2(hashAlgorithm: sha512);
|
||||
|
|
@ -156,15 +163,84 @@ class SSSS {
|
|||
}
|
||||
|
||||
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)) {
|
||||
return null;
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
|
|
@ -234,12 +310,19 @@ class SSSS {
|
|||
return decrypted;
|
||||
}
|
||||
|
||||
Future<void> store(
|
||||
String type, String secret, String keyId, Uint8List key) async {
|
||||
Future<void> store(String type, String secret, String keyId, Uint8List key,
|
||||
{bool add = false}) async {
|
||||
final triggerCacheCallback =
|
||||
_cacheCallbacks.containsKey(type) && await getCached(type) == null;
|
||||
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>{},
|
||||
};
|
||||
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 {
|
||||
for (final type in CACHE_TYPES) {
|
||||
final secret = await getCached(type);
|
||||
|
|
@ -512,15 +618,27 @@ class OpenSSSS {
|
|||
|
||||
bool get isUnlocked => privateKey != null;
|
||||
|
||||
void unlock({String passphrase, String recoveryKey}) {
|
||||
if (passphrase != null) {
|
||||
privateKey = SSSS.keyFromPassphrase(
|
||||
passphrase,
|
||||
_PassphraseInfo(
|
||||
Future<void> unlock(
|
||||
{String passphrase, String recoveryKey, String keyOrPassphrase}) async {
|
||||
if (keyOrPassphrase != null) {
|
||||
try {
|
||||
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'],
|
||||
salt: keyData.content['passphrase']['salt'],
|
||||
iterations: keyData.content['passphrase']['iterations'],
|
||||
bits: keyData.content['passphrase']['bits']));
|
||||
bits: keyData.content['passphrase']['bits'],
|
||||
),
|
||||
));
|
||||
} else if (recoveryKey != null) {
|
||||
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
|
||||
} 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 {
|
||||
return await ssss.getStored(type, keyId, privateKey);
|
||||
}
|
||||
|
||||
Future<void> store(String type, String secret) async {
|
||||
await ssss.store(type, secret, keyId, privateKey);
|
||||
Future<void> store(String type, String secret, {bool add = false}) async {
|
||||
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 {
|
||||
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(
|
||||
{String passphrase, String recoveryKey, bool skip = false}) async {
|
||||
{String passphrase,
|
||||
String recoveryKey,
|
||||
String keyOrPassphrase,
|
||||
bool skip = false}) async {
|
||||
final next = () {
|
||||
if (_nextAction == 'request') {
|
||||
sendStart();
|
||||
|
|
@ -378,8 +381,11 @@ class KeyVerification {
|
|||
next();
|
||||
return;
|
||||
}
|
||||
final handle = encryption.ssss.open('m.cross_signing.user_signing');
|
||||
await handle.unlock(passphrase: passphrase, recoveryKey: recoveryKey);
|
||||
final handle = encryption.ssss.open(EventTypes.CrossSigningUserSigning);
|
||||
await handle.unlock(
|
||||
passphrase: passphrase,
|
||||
recoveryKey: recoveryKey,
|
||||
keyOrPassphrase: keyOrPassphrase);
|
||||
await handle.maybeCacheAll();
|
||||
next();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export 'src/utils/receipt.dart';
|
|||
export 'src/utils/states_map.dart';
|
||||
export 'src/utils/sync_update_extension.dart';
|
||||
export 'src/utils/to_device_event.dart';
|
||||
export 'src/utils/uia_request.dart';
|
||||
export 'src/client.dart';
|
||||
export 'src/event.dart';
|
||||
export 'src/room.dart';
|
||||
|
|
|
|||
|
|
@ -1470,14 +1470,16 @@ class MatrixApi {
|
|||
MatrixCrossSigningKey masterKey,
|
||||
MatrixCrossSigningKey selfSigningKey,
|
||||
MatrixCrossSigningKey userSigningKey,
|
||||
Map<String, dynamic> auth,
|
||||
}) async {
|
||||
await request(
|
||||
RequestType.POST,
|
||||
'/client/r0/keys/device_signing/upload',
|
||||
'/client/unstable/keys/device_signing/upload',
|
||||
data: {
|
||||
'master_key': masterKey.toJson(),
|
||||
'self_signing_key': selfSigningKey.toJson(),
|
||||
'user_signing_key': userSigningKey.toJson(),
|
||||
if (masterKey != null) 'master_key': masterKey.toJson(),
|
||||
if (selfSigningKey != null) 'self_signing_key': selfSigningKey.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';
|
||||
static const String megolmBackupV1Curve25519AesSha2 =
|
||||
'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 CallHangup = 'm.call.hangup';
|
||||
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/room_update.dart';
|
||||
import 'utils/to_device_event.dart';
|
||||
import 'utils/uia_request.dart';
|
||||
|
||||
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
|
||||
/// 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
|
||||
|
|
@ -590,6 +609,11 @@ class Client extends MatrixApi {
|
|||
final StreamController<KeyVerification> onKeyVerificationRequest =
|
||||
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
|
||||
/// an error?
|
||||
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(
|
||||
int client_id, String user_id, bool outdated) {
|
||||
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;
|
||||
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;
|
||||
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);
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
// we need to populate the ssss cache to be able to test signing easily
|
||||
final handle = client.encryption.ssss.open();
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.maybeCacheAll();
|
||||
|
||||
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 {
|
||||
requestedSecrets = true;
|
||||
final handle = open();
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.maybeCacheAll();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ void main() {
|
|||
expect(client.encryption.keyManager.enabled, true);
|
||||
expect(await client.encryption.keyManager.isCached(), false);
|
||||
final handle = client.encryption.ssss.open();
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.maybeCacheAll();
|
||||
expect(await client.encryption.keyManager.isCached(), true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class MockSSSS extends SSSS {
|
|||
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
|
||||
requestedSecrets = true;
|
||||
final handle = open();
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.maybeCacheAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ void main() {
|
|||
final handle = client.encryption.ssss.open();
|
||||
var failed = false;
|
||||
try {
|
||||
handle.unlock(passphrase: 'invalid');
|
||||
await handle.unlock(passphrase: 'invalid');
|
||||
} catch (_) {
|
||||
failed = true;
|
||||
}
|
||||
|
|
@ -88,14 +88,14 @@ void main() {
|
|||
expect(handle.isUnlocked, false);
|
||||
failed = false;
|
||||
try {
|
||||
handle.unlock(recoveryKey: 'invalid');
|
||||
await handle.unlock(recoveryKey: 'invalid');
|
||||
} catch (_) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed, true);
|
||||
expect(handle.isUnlocked, false);
|
||||
handle.unlock(passphrase: SSSS_PASSPHRASE);
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.unlock(passphrase: SSSS_PASSPHRASE);
|
||||
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||
expect(handle.isUnlocked, true);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await handle.store('best animal', 'foxies');
|
||||
|
|
@ -114,32 +114,32 @@ void main() {
|
|||
|
||||
test('cache', () async {
|
||||
final handle =
|
||||
client.encryption.ssss.open('m.cross_signing.self_signing');
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
client.encryption.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||
expect(
|
||||
(await client.encryption.ssss
|
||||
.getCached('m.cross_signing.self_signing')) !=
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
false);
|
||||
expect(
|
||||
(await client.encryption.ssss
|
||||
.getCached('m.cross_signing.user_signing')) !=
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
false);
|
||||
await handle.getStored('m.cross_signing.self_signing');
|
||||
await handle.getStored(EventTypes.CrossSigningSelfSigning);
|
||||
expect(
|
||||
(await client.encryption.ssss
|
||||
.getCached('m.cross_signing.self_signing')) !=
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
true);
|
||||
await handle.maybeCacheAll();
|
||||
expect(
|
||||
(await client.encryption.ssss
|
||||
.getCached('m.cross_signing.user_signing')) !=
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
true);
|
||||
expect(
|
||||
(await client.encryption.ssss.getCached('m.megolm_backup.v1')) !=
|
||||
(await client.encryption.ssss.getCached(EventTypes.MegolmBackup)) !=
|
||||
null,
|
||||
true);
|
||||
});
|
||||
|
|
@ -163,7 +163,7 @@ void main() {
|
|||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.cross_signing.self_signing',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
|
|
@ -183,7 +183,7 @@ void main() {
|
|||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.cross_signing.self_signing',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
|
|
@ -219,7 +219,7 @@ void main() {
|
|||
content: {
|
||||
'action': 'request_cancellation',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.cross_signing.self_signing',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
|
|
@ -240,7 +240,7 @@ void main() {
|
|||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.cross_signing.self_signing',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
|
|
@ -258,8 +258,8 @@ void main() {
|
|||
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
|
||||
key.setDirectVerified(true);
|
||||
final handle =
|
||||
client.encryption.ssss.open('m.cross_signing.self_signing');
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
client.encryption.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: SSSS_KEY);
|
||||
|
||||
await client.encryption.ssss.clearCache();
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
|
|
@ -280,9 +280,9 @@ void main() {
|
|||
|
||||
// test the different validators
|
||||
for (final type in [
|
||||
'm.cross_signing.self_signing',
|
||||
'm.cross_signing.user_signing',
|
||||
'm.megolm_backup.v1'
|
||||
EventTypes.CrossSigningSelfSigning,
|
||||
EventTypes.CrossSigningUserSigning,
|
||||
EventTypes.MegolmBackup
|
||||
]) {
|
||||
final secret = await handle.getStored(type);
|
||||
await client.encryption.ssss.clearCache();
|
||||
|
|
@ -378,7 +378,7 @@ void main() {
|
|||
// validator doesn't check out
|
||||
await client.encryption.ssss.clearCache();
|
||||
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(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.send',
|
||||
|
|
@ -391,8 +391,8 @@ void main() {
|
|||
},
|
||||
);
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
await client.encryption.ssss.getCached('m.megolm_backup.v1'), null);
|
||||
expect(await client.encryption.ssss.getCached(EventTypes.MegolmBackup),
|
||||
null);
|
||||
});
|
||||
|
||||
test('request all', () async {
|
||||
|
|
@ -417,6 +417,21 @@ void main() {
|
|||
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 {
|
||||
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';
|
||||
|
||||
Future<Client> getClient() async {
|
||||
final client = Client('testclient',
|
||||
httpClient: FakeMatrixApi(), databaseBuilder: getDatabase);
|
||||
final client = Client(
|
||||
'testclient',
|
||||
httpClient: FakeMatrixApi(),
|
||||
databaseBuilder: getDatabase,
|
||||
);
|
||||
FakeMatrixApi.client = client;
|
||||
await client.checkHomeserver('https://fakeServer.notExisting');
|
||||
client.init(
|
||||
newToken: 'abcd',
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart' as sdk;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
import 'dart:math';
|
||||
|
|
@ -27,6 +29,7 @@ import 'package:http/testing.dart';
|
|||
class FakeMatrixApi extends MockClient {
|
||||
static final calledEndpoints = <String, List<dynamic>>{};
|
||||
static int eventCounter = 0;
|
||||
static sdk.Client client;
|
||||
|
||||
FakeMatrixApi()
|
||||
: super((request) async {
|
||||
|
|
@ -87,6 +90,25 @@ class FakeMatrixApi extends MockClient {
|
|||
res = {
|
||||
'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 {
|
||||
res = {
|
||||
'errcode': 'M_UNRECOGNIZED',
|
||||
|
|
@ -538,7 +560,7 @@ class FakeMatrixApi extends MockClient {
|
|||
'type': 'm.direct'
|
||||
},
|
||||
{
|
||||
'type': 'm.secret_storage.default_key',
|
||||
'type': EventTypes.SecretStorageDefaultKey,
|
||||
'content': {'key': '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'}
|
||||
},
|
||||
{
|
||||
|
|
@ -546,7 +568,7 @@ class FakeMatrixApi extends MockClient {
|
|||
'content': {
|
||||
'algorithm': AlgorithmTypes.secretStorageV1AesHmcSha2,
|
||||
'passphrase': {
|
||||
'algorithm': 'm.pbkdf2',
|
||||
'algorithm': AlgorithmTypes.pbkdf2,
|
||||
'iterations': 500000,
|
||||
'salt': 'F4jJ80mr0Fc8mRwU9JgA3lQDyjPuZXQL'
|
||||
},
|
||||
|
|
@ -568,7 +590,7 @@ class FakeMatrixApi extends MockClient {
|
|||
}
|
||||
},
|
||||
{
|
||||
'type': 'm.cross_signing.self_signing',
|
||||
'type': EventTypes.CrossSigningSelfSigning,
|
||||
'content': {
|
||||
'encrypted': {
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
||||
|
|
@ -581,7 +603,7 @@ class FakeMatrixApi extends MockClient {
|
|||
}
|
||||
},
|
||||
{
|
||||
'type': 'm.cross_signing.user_signing',
|
||||
'type': EventTypes.CrossSigningUserSigning,
|
||||
'content': {
|
||||
'encrypted': {
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
||||
|
|
@ -594,7 +616,7 @@ class FakeMatrixApi extends MockClient {
|
|||
}
|
||||
},
|
||||
{
|
||||
'type': 'm.megolm_backup.v1',
|
||||
'type': EventTypes.MegolmBackup,
|
||||
'content': {
|
||||
'encrypted': {
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
||||
|
|
@ -1978,7 +2000,28 @@ class FakeMatrixApi extends MockClient {
|
|||
'/client/r0/rooms/!localpart%3Aserver.abc/ban': (var reqI) => {},
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/unban': (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/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