feat: Add bootstrapping

This commit is contained in:
Sorunome 2020-10-20 18:25:41 +02:00 committed by Christian Pauly
parent 88888a43f1
commit 49f0679fbf
23 changed files with 1330 additions and 69 deletions

View File

@ -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';

View File

@ -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 {

View File

@ -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));
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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();
}
}
}

View File

@ -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();
}

View File

@ -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';

View File

@ -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,
},
);
}

View File

@ -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';
}

View File

@ -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';
}

View File

@ -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;

View File

@ -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(

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);
});
});
}

View File

@ -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();
}
}

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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',

View File

@ -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'},
},

94
test/uia_test.dart Normal file
View File

@ -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());
});
});
}