755 lines
25 KiB
Dart
755 lines
25 KiB
Dart
/*
|
|
* Famedly Matrix SDK
|
|
* Copyright (C) 2020, 2021 Famedly GmbH
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:core';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:base58check/base58.dart';
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:matrix/encryption/utils/base64_unpadded.dart';
|
|
|
|
import '../matrix.dart';
|
|
import '../src/utils/crypto/crypto.dart' as uc;
|
|
import '../src/utils/run_in_root.dart';
|
|
import 'encryption.dart';
|
|
import 'utils/ssss_cache.dart';
|
|
|
|
const cacheTypes = <String>{
|
|
EventTypes.CrossSigningSelfSigning,
|
|
EventTypes.CrossSigningUserSigning,
|
|
EventTypes.MegolmBackup,
|
|
};
|
|
|
|
const zeroStr =
|
|
'\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 base58Alphabet =
|
|
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
const base58 = Base58Codec(base58Alphabet);
|
|
const olmRecoveryKeyPrefix = [0x8B, 0x01];
|
|
const ssssKeyLength = 32;
|
|
const pbkdf2DefaultIterations = 500000;
|
|
const pbkdf2SaltLength = 64;
|
|
|
|
/// SSSS: **S**ecure **S**ecret **S**torage and **S**haring
|
|
/// Read more about SSSS at:
|
|
/// https://matrix.org/docs/guides/implementing-more-advanced-e-2-ee-features-such-as-cross-signing#3-implementing-ssss
|
|
class SSSS {
|
|
final Encryption encryption;
|
|
|
|
Client get client => encryption.client;
|
|
final pendingShareRequests = <String, _ShareRequest>{};
|
|
final _validators = <String, FutureOr<bool> Function(String)>{};
|
|
final _cacheCallbacks = <String, FutureOr<void> Function(String)>{};
|
|
final Map<String, SSSSCache> _cache = <String, SSSSCache>{};
|
|
|
|
SSSS(this.encryption);
|
|
|
|
// for testing
|
|
Future<void> clearCache() async {
|
|
await client.database?.clearSSSSCache();
|
|
_cache.clear();
|
|
}
|
|
|
|
static _DerivedKeys deriveKeys(Uint8List key, String name) {
|
|
final zerosalt = Uint8List(8);
|
|
final prk = Hmac(sha256, zerosalt).convert(key);
|
|
final b = Uint8List(1);
|
|
b[0] = 1;
|
|
final aesKey = Hmac(sha256, prk.bytes).convert(utf8.encode(name) + b);
|
|
b[0] = 2;
|
|
final hmacKey =
|
|
Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b);
|
|
return _DerivedKeys(
|
|
aesKey: Uint8List.fromList(aesKey.bytes),
|
|
hmacKey: Uint8List.fromList(hmacKey.bytes));
|
|
}
|
|
|
|
static Future<_Encrypted> encryptAes(String data, Uint8List key, String name,
|
|
[String? ivStr]) async {
|
|
Uint8List iv;
|
|
if (ivStr != null) {
|
|
iv = base64decodeUnpadded(ivStr);
|
|
} else {
|
|
iv = Uint8List.fromList(uc.secureRandomBytes(16));
|
|
}
|
|
// we need to clear bit 63 of the IV
|
|
iv[8] &= 0x7f;
|
|
|
|
final keys = deriveKeys(key, name);
|
|
|
|
final plain = Uint8List.fromList(utf8.encode(data));
|
|
final ciphertext = await uc.aesCtr.encrypt(plain, keys.aesKey, iv);
|
|
|
|
final hmac = Hmac(sha256, keys.hmacKey).convert(ciphertext);
|
|
|
|
return _Encrypted(
|
|
iv: base64.encode(iv),
|
|
ciphertext: base64.encode(ciphertext),
|
|
mac: base64.encode(hmac.bytes));
|
|
}
|
|
|
|
static Future<String> decryptAes(
|
|
_Encrypted data, Uint8List key, String name) async {
|
|
final keys = deriveKeys(key, name);
|
|
final cipher = base64decodeUnpadded(data.ciphertext);
|
|
final hmac = base64
|
|
.encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes)
|
|
.replaceAll(RegExp(r'=+$'), '');
|
|
if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) {
|
|
throw Exception('Bad MAC');
|
|
}
|
|
final decipher = await uc.aesCtr
|
|
.encrypt(cipher, keys.aesKey, base64decodeUnpadded(data.iv));
|
|
return String.fromCharCodes(decipher);
|
|
}
|
|
|
|
static Uint8List decodeRecoveryKey(String recoveryKey) {
|
|
final result = base58.decode(recoveryKey.replaceAll(RegExp(r'\s'), ''));
|
|
|
|
final parity = result.fold<int>(0, (a, b) => a ^ b);
|
|
if (parity != 0) {
|
|
throw Exception('Incorrect parity');
|
|
}
|
|
|
|
for (var i = 0; i < olmRecoveryKeyPrefix.length; i++) {
|
|
if (result[i] != olmRecoveryKeyPrefix[i]) {
|
|
throw Exception('Incorrect prefix');
|
|
}
|
|
}
|
|
|
|
if (result.length != olmRecoveryKeyPrefix.length + ssssKeyLength + 1) {
|
|
throw Exception('Incorrect length');
|
|
}
|
|
|
|
return Uint8List.fromList(result.sublist(olmRecoveryKeyPrefix.length,
|
|
olmRecoveryKeyPrefix.length + ssssKeyLength));
|
|
}
|
|
|
|
static String encodeRecoveryKey(Uint8List recoveryKey) {
|
|
final keyToEncode = <int>[...olmRecoveryKeyPrefix, ...recoveryKey];
|
|
final parity = keyToEncode.fold<int>(0, (a, b) => a ^ b);
|
|
keyToEncode.add(parity);
|
|
// base58-encode and add a space every four chars
|
|
return base58
|
|
.encode(keyToEncode)
|
|
.replaceAllMapped(RegExp(r'.{4}'), (s) => '${s.group(0)} ')
|
|
.trim();
|
|
}
|
|
|
|
static Future<Uint8List> keyFromPassphrase(
|
|
String passphrase, PassphraseInfo info) async {
|
|
if (info.algorithm != AlgorithmTypes.pbkdf2) {
|
|
throw Exception('Unknown algorithm');
|
|
}
|
|
if (info.iterations == null) {
|
|
throw Exception('Passphrase info without iterations');
|
|
}
|
|
if (info.salt == null) {
|
|
throw Exception('Passphrase info without salt');
|
|
}
|
|
return await uc.pbkdf2(
|
|
Uint8List.fromList(utf8.encode(passphrase)),
|
|
Uint8List.fromList(utf8.encode(info.salt!)),
|
|
uc.sha512,
|
|
info.iterations!,
|
|
info.bits ?? 256);
|
|
}
|
|
|
|
void setValidator(String type, FutureOr<bool> Function(String) validator) {
|
|
_validators[type] = validator;
|
|
}
|
|
|
|
void setCacheCallback(String type, FutureOr<void> Function(String) callback) {
|
|
_cacheCallbacks[type] = callback;
|
|
}
|
|
|
|
String? get defaultKeyId => client
|
|
.accountData[EventTypes.SecretStorageDefaultKey]
|
|
?.parsedSecretStorageDefaultKeyContent
|
|
.key;
|
|
|
|
Future<void> setDefaultKeyId(String keyId) async {
|
|
await client.setAccountData(
|
|
client.userID!,
|
|
EventTypes.SecretStorageDefaultKey,
|
|
SecretStorageDefaultKeyContent(key: keyId).toJson(),
|
|
);
|
|
}
|
|
|
|
SecretStorageKeyContent? getKey(String keyId) {
|
|
return client.accountData[EventTypes.secretStorageKey(keyId)]
|
|
?.parsedSecretStorageKeyContent;
|
|
}
|
|
|
|
bool isKeyValid(String keyId) =>
|
|
getKey(keyId)?.algorithm == AlgorithmTypes.secretStorageV1AesHmcSha2;
|
|
|
|
/// Creates a new secret storage key, optional encrypts it with [passphrase]
|
|
/// and stores it in the user's `accountData`.
|
|
Future<OpenSSSS> createKey([String? passphrase]) async {
|
|
Uint8List privateKey;
|
|
final content = SecretStorageKeyContent();
|
|
if (passphrase != null) {
|
|
// we need to derive the key off of the passphrase
|
|
content.passphrase = PassphraseInfo(
|
|
iterations: pbkdf2DefaultIterations,
|
|
salt: base64.encode(uc.secureRandomBytes(pbkdf2SaltLength)),
|
|
algorithm: AlgorithmTypes.pbkdf2,
|
|
bits: ssssKeyLength * 8,
|
|
);
|
|
privateKey = await client
|
|
.runInBackground(
|
|
_keyFromPassphrase,
|
|
_KeyFromPassphraseArgs(
|
|
passphrase: passphrase,
|
|
info: content.passphrase!,
|
|
),
|
|
)
|
|
.timeout(Duration(seconds: 10));
|
|
} else {
|
|
// we need to just generate a new key from scratch
|
|
privateKey = Uint8List.fromList(uc.secureRandomBytes(ssssKeyLength));
|
|
}
|
|
// now that we have the private key, let's create the iv and mac
|
|
final encrypted = await encryptAes(zeroStr, privateKey, '');
|
|
content.iv = encrypted.iv;
|
|
content.mac = encrypted.mac;
|
|
content.algorithm = AlgorithmTypes.secretStorageV1AesHmcSha2;
|
|
|
|
const keyidByteLength = 24;
|
|
|
|
// make sure we generate a unique key id
|
|
final keyId = () sync* {
|
|
for (;;) {
|
|
yield base64.encode(uc.secureRandomBytes(keyidByteLength));
|
|
}
|
|
}()
|
|
.firstWhere((keyId) => getKey(keyId) == null);
|
|
|
|
final accountDataType = EventTypes.secretStorageKey(keyId);
|
|
// noooow we set the account data
|
|
final waitForAccountData = client.onSync.stream.firstWhere((syncUpdate) =>
|
|
syncUpdate.accountData != null &&
|
|
syncUpdate.accountData!
|
|
.any((accountData) => accountData.type == accountDataType));
|
|
await client.setAccountData(
|
|
client.userID!, accountDataType, content.toJson());
|
|
await waitForAccountData;
|
|
|
|
final key = open(keyId);
|
|
await key.setPrivateKey(privateKey);
|
|
return key;
|
|
}
|
|
|
|
Future<bool> checkKey(Uint8List key, SecretStorageKeyContent info) async {
|
|
if (info.algorithm == AlgorithmTypes.secretStorageV1AesHmcSha2) {
|
|
if ((info.mac is String) && (info.iv is String)) {
|
|
final encrypted = await encryptAes(zeroStr, key, '', info.iv);
|
|
return info.mac!.replaceAll(RegExp(r'=+$'), '') ==
|
|
encrypted.mac.replaceAll(RegExp(r'=+$'), '');
|
|
} else {
|
|
// no real information about the key, assume it is valid
|
|
return true;
|
|
}
|
|
} else {
|
|
throw Exception('Unknown Algorithm');
|
|
}
|
|
}
|
|
|
|
bool isSecret(String type) =>
|
|
client.accountData[type] != null &&
|
|
client.accountData[type]!.content['encrypted'] is Map;
|
|
|
|
Future<String?> getCached(String type) async {
|
|
if (client.database == null) {
|
|
return null;
|
|
}
|
|
// check if it is still valid
|
|
final keys = keyIdsFromType(type);
|
|
if (keys == null) {
|
|
return null;
|
|
}
|
|
final isValid = (dbEntry) =>
|
|
keys.contains(dbEntry.keyId) &&
|
|
dbEntry.ciphertext != null &&
|
|
client.accountData[type]?.content['encrypted'][dbEntry.keyId]
|
|
['ciphertext'] ==
|
|
dbEntry.ciphertext;
|
|
if (_cache.containsKey(type) && isValid(_cache[type])) {
|
|
return _cache[type]?.content;
|
|
}
|
|
final ret = await client.database?.getSSSSCache(type);
|
|
if (ret == null) {
|
|
return null;
|
|
}
|
|
if (isValid(ret)) {
|
|
_cache[type] = ret;
|
|
return ret.content;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<String> getStored(String type, String keyId, Uint8List key) async {
|
|
final secretInfo = client.accountData[type];
|
|
if (secretInfo == null) {
|
|
throw Exception('Not found');
|
|
}
|
|
if (!(secretInfo.content['encrypted'] is Map)) {
|
|
throw Exception('Content is not encrypted');
|
|
}
|
|
if (!(secretInfo.content['encrypted'][keyId] is Map)) {
|
|
throw Exception('Wrong / unknown key');
|
|
}
|
|
final enc = secretInfo.content['encrypted'][keyId];
|
|
final encryptInfo = _Encrypted(
|
|
iv: enc['iv'], ciphertext: enc['ciphertext'], mac: enc['mac']);
|
|
final decrypted = await decryptAes(encryptInfo, key, type);
|
|
final db = client.database;
|
|
if (cacheTypes.contains(type) && db != null) {
|
|
// cache the thing
|
|
await db.storeSSSSCache(type, keyId, enc['ciphertext'], decrypted);
|
|
if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) {
|
|
_cacheCallbacks[type]!(decrypted);
|
|
}
|
|
}
|
|
return decrypted;
|
|
}
|
|
|
|
Future<void> store(String type, String secret, String keyId, Uint8List key,
|
|
{bool add = false}) async {
|
|
final encrypted = await encryptAes(secret, key, type);
|
|
Map<String, dynamic>? content;
|
|
if (add && client.accountData[type] != null) {
|
|
content = client.accountData[type]!.content.copy();
|
|
if (!(content['encrypted'] is Map)) {
|
|
content['encrypted'] = <String, dynamic>{};
|
|
}
|
|
}
|
|
content ??= <String, dynamic>{
|
|
'encrypted': <String, dynamic>{},
|
|
};
|
|
content['encrypted'][keyId] = <String, dynamic>{
|
|
'iv': encrypted.iv,
|
|
'ciphertext': encrypted.ciphertext,
|
|
'mac': encrypted.mac,
|
|
};
|
|
// store the thing in your account data
|
|
await client.setAccountData(client.userID!, type, content);
|
|
final db = client.database;
|
|
if (cacheTypes.contains(type) && db != null) {
|
|
// cache the thing
|
|
await db.storeSSSSCache(type, keyId, encrypted.ciphertext, secret);
|
|
if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) {
|
|
_cacheCallbacks[type]!(secret);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> validateAndStripOtherKeys(
|
|
String type, String secret, String keyId, Uint8List key) async {
|
|
if (await getStored(type, keyId, key) != secret) {
|
|
throw Exception('Secrets do not match up!');
|
|
}
|
|
// now remove all other keys
|
|
final content = client.accountData[type]?.content.copy();
|
|
if (content == null) {
|
|
throw Exception('Key has no 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 Exception('Secrets do not match up!');
|
|
}
|
|
// store the thing in your account data
|
|
await client.setAccountData(client.userID!, type, content);
|
|
if (cacheTypes.contains(type)) {
|
|
// cache the thing
|
|
await client.database?.storeSSSSCache(
|
|
type, keyId, content['encrypted'][keyId]['ciphertext'], secret);
|
|
}
|
|
}
|
|
|
|
Future<void> maybeCacheAll(String keyId, Uint8List key) async {
|
|
for (final type in cacheTypes) {
|
|
final secret = await getCached(type);
|
|
if (secret == null) {
|
|
try {
|
|
await getStored(type, keyId, key);
|
|
} catch (_) {
|
|
// the entry wasn't stored, just ignore it
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> maybeRequestAll([List<DeviceKeys>? devices]) async {
|
|
for (final type in cacheTypes) {
|
|
if (keyIdsFromType(type) != null) {
|
|
final secret = await getCached(type);
|
|
if (secret == null) {
|
|
await request(type, devices);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> request(String type, [List<DeviceKeys>? devices]) async {
|
|
// only send to own, verified devices
|
|
Logs().i('[SSSS] Requesting type $type...');
|
|
if (devices == null || devices.isEmpty) {
|
|
if (!client.userDeviceKeys.containsKey(client.userID)) {
|
|
Logs().w('[SSSS] User does not have any devices');
|
|
return;
|
|
}
|
|
devices =
|
|
client.userDeviceKeys[client.userID]!.deviceKeys.values.toList();
|
|
}
|
|
devices.removeWhere((DeviceKeys d) =>
|
|
d.userId != client.userID ||
|
|
!d.verified ||
|
|
d.blocked ||
|
|
d.deviceId == client.deviceID);
|
|
if (devices.isEmpty) {
|
|
Logs().w('[SSSS] No devices');
|
|
return;
|
|
}
|
|
final requestId = client.generateUniqueTransactionId();
|
|
final request = _ShareRequest(
|
|
requestId: requestId,
|
|
type: type,
|
|
devices: devices,
|
|
);
|
|
pendingShareRequests[requestId] = request;
|
|
await client.sendToDeviceEncrypted(devices, EventTypes.SecretRequest, {
|
|
'action': 'request',
|
|
'requesting_device_id': client.deviceID,
|
|
'request_id': requestId,
|
|
'name': type,
|
|
});
|
|
}
|
|
|
|
DateTime? _lastCacheRequest;
|
|
bool _isPeriodicallyRequestingMissingCache = false;
|
|
|
|
Future<void> periodicallyRequestMissingCache() async {
|
|
if (_isPeriodicallyRequestingMissingCache ||
|
|
(_lastCacheRequest != null &&
|
|
DateTime.now()
|
|
.subtract(Duration(minutes: 15))
|
|
.isBefore(_lastCacheRequest!)) ||
|
|
client.isUnknownSession) {
|
|
// we are already requesting right now or we attempted to within the last 15 min
|
|
return;
|
|
}
|
|
_lastCacheRequest = DateTime.now();
|
|
_isPeriodicallyRequestingMissingCache = true;
|
|
try {
|
|
await maybeRequestAll();
|
|
} finally {
|
|
_isPeriodicallyRequestingMissingCache = false;
|
|
}
|
|
}
|
|
|
|
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
|
if (event.type == EventTypes.SecretRequest) {
|
|
// got a request to share a secret
|
|
Logs().i('[SSSS] Received sharing request...');
|
|
if (event.sender != client.userID ||
|
|
!client.userDeviceKeys.containsKey(client.userID)) {
|
|
Logs().i('[SSSS] Not sent by us');
|
|
return; // we aren't asking for it ourselves, so ignore
|
|
}
|
|
if (event.content['action'] != 'request') {
|
|
Logs().i('[SSSS] it is actually a cancelation');
|
|
return; // not actually requesting, so ignore
|
|
}
|
|
final device = client.userDeviceKeys[client.userID]!
|
|
.deviceKeys[event.content['requesting_device_id']];
|
|
if (device == null || !device.verified || device.blocked) {
|
|
Logs().i('[SSSS] Unknown / unverified devices, ignoring');
|
|
return; // nope....unknown or untrusted device
|
|
}
|
|
// alright, all seems fine...let's check if we actually have the secret they are asking for
|
|
final type = event.content['name'];
|
|
final secret = await getCached(type);
|
|
if (secret == null) {
|
|
Logs()
|
|
.i('[SSSS] We don\'t have the secret for $type ourself, ignoring');
|
|
return; // seems like we don't have this, either
|
|
}
|
|
// okay, all checks out...time to share this secret!
|
|
Logs().i('[SSSS] Replying with secret for $type');
|
|
await client.sendToDeviceEncrypted(
|
|
[device],
|
|
EventTypes.SecretSend,
|
|
{
|
|
'request_id': event.content['request_id'],
|
|
'secret': secret,
|
|
});
|
|
} else if (event.type == EventTypes.SecretSend) {
|
|
// receiving a secret we asked for
|
|
Logs().i('[SSSS] Received shared secret...');
|
|
final encryptedContent = event.encryptedContent;
|
|
if (event.sender != client.userID ||
|
|
!pendingShareRequests.containsKey(event.content['request_id']) ||
|
|
encryptedContent == null) {
|
|
Logs().i('[SSSS] Not by us or unknown request');
|
|
return; // we have no idea what we just received
|
|
}
|
|
final request = pendingShareRequests[event.content['request_id']]!;
|
|
// alright, as we received a known request id, let's check if the sender is valid
|
|
final device = request.devices.firstWhereOrNull((d) =>
|
|
d.userId == event.sender &&
|
|
d.curve25519Key == encryptedContent['sender_key']);
|
|
if (device == null) {
|
|
Logs().i('[SSSS] Someone else replied?');
|
|
return; // someone replied whom we didn't send the share request to
|
|
}
|
|
final secret = event.content['secret'];
|
|
if (!(event.content['secret'] is String)) {
|
|
Logs().i('[SSSS] Secret wasn\'t a string');
|
|
return; // the secret wasn't a string....wut?
|
|
}
|
|
// let's validate if the secret is, well, valid
|
|
if (_validators.containsKey(request.type) &&
|
|
!(await _validators[request.type]!(secret))) {
|
|
Logs().i('[SSSS] The received secret was invalid');
|
|
return; // didn't pass the validator
|
|
}
|
|
pendingShareRequests.remove(request.requestId);
|
|
if (request.start.add(Duration(minutes: 15)).isBefore(DateTime.now())) {
|
|
Logs().i('[SSSS] Request is too far in the past');
|
|
return; // our request is more than 15min in the past...better not trust it anymore
|
|
}
|
|
Logs().i('[SSSS] Secret for type ${request.type} is ok, storing it');
|
|
final db = client.database;
|
|
if (db != null) {
|
|
final keyId = keyIdFromType(request.type);
|
|
if (keyId != null) {
|
|
final ciphertext = client.accountData[request.type]!
|
|
.content['encrypted'][keyId]['ciphertext'];
|
|
await db.storeSSSSCache(request.type, keyId, ciphertext, secret);
|
|
if (_cacheCallbacks.containsKey(request.type)) {
|
|
_cacheCallbacks[request.type]!(secret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Set<String>? keyIdsFromType(String type) {
|
|
final data = client.accountData[type];
|
|
if (data == null) {
|
|
return null;
|
|
}
|
|
if (data.content['encrypted'] is Map) {
|
|
return data.content['encrypted'].keys.toSet();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
String? keyIdFromType(String type) {
|
|
final keys = keyIdsFromType(type);
|
|
if (keys == null || keys.isEmpty) {
|
|
return null;
|
|
}
|
|
if (keys.contains(defaultKeyId)) {
|
|
return defaultKeyId;
|
|
}
|
|
return keys.first;
|
|
}
|
|
|
|
OpenSSSS open([String? identifier]) {
|
|
identifier ??= defaultKeyId;
|
|
if (identifier == null) {
|
|
throw Exception('Dont know what to open');
|
|
}
|
|
final keyToOpen = keyIdFromType(identifier) ?? identifier;
|
|
final key = getKey(keyToOpen);
|
|
if (key == null) {
|
|
throw Exception('Unknown key to open');
|
|
}
|
|
return OpenSSSS(ssss: this, keyId: keyToOpen, keyData: key);
|
|
}
|
|
}
|
|
|
|
class _ShareRequest {
|
|
final String requestId;
|
|
final String type;
|
|
final List<DeviceKeys> devices;
|
|
final DateTime start;
|
|
|
|
_ShareRequest(
|
|
{required this.requestId, required this.type, required this.devices})
|
|
: start = DateTime.now();
|
|
}
|
|
|
|
class _Encrypted {
|
|
final String iv;
|
|
final String ciphertext;
|
|
final String mac;
|
|
|
|
_Encrypted({required this.iv, required this.ciphertext, required this.mac});
|
|
}
|
|
|
|
class _DerivedKeys {
|
|
final Uint8List aesKey;
|
|
final Uint8List hmacKey;
|
|
|
|
_DerivedKeys({required this.aesKey, required this.hmacKey});
|
|
}
|
|
|
|
class OpenSSSS {
|
|
final SSSS ssss;
|
|
final String keyId;
|
|
final SecretStorageKeyContent keyData;
|
|
|
|
OpenSSSS({required this.ssss, required this.keyId, required this.keyData});
|
|
|
|
Uint8List? privateKey;
|
|
|
|
bool get isUnlocked => privateKey != null;
|
|
|
|
bool get hasPassphrase => keyData.passphrase != null;
|
|
|
|
String? get recoveryKey =>
|
|
isUnlocked ? SSSS.encodeRecoveryKey(privateKey!) : null;
|
|
|
|
Future<void> unlock(
|
|
{String? passphrase,
|
|
String? recoveryKey,
|
|
String? keyOrPassphrase,
|
|
bool postUnlock = true}) async {
|
|
if (keyOrPassphrase != null) {
|
|
try {
|
|
await unlock(recoveryKey: keyOrPassphrase, postUnlock: postUnlock);
|
|
} catch (_) {
|
|
if (hasPassphrase) {
|
|
await unlock(passphrase: keyOrPassphrase, postUnlock: postUnlock);
|
|
} else {
|
|
rethrow;
|
|
}
|
|
}
|
|
return;
|
|
} else if (passphrase != null) {
|
|
if (!hasPassphrase) {
|
|
throw Exception(
|
|
'Tried to unlock with passphrase while key does not have a passphrase');
|
|
}
|
|
privateKey = await ssss.client
|
|
.runInBackground(
|
|
_keyFromPassphrase,
|
|
_KeyFromPassphraseArgs(
|
|
passphrase: passphrase,
|
|
info: keyData.passphrase!,
|
|
),
|
|
)
|
|
.timeout(Duration(seconds: 10));
|
|
} else if (recoveryKey != null) {
|
|
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
|
|
} else {
|
|
throw Exception('Nothing specified');
|
|
}
|
|
// verify the validity of the key
|
|
if (!await ssss.checkKey(privateKey!, keyData)) {
|
|
privateKey = null;
|
|
throw Exception('Inalid key');
|
|
}
|
|
if (postUnlock) {
|
|
await runInRoot(() => _postUnlock());
|
|
}
|
|
}
|
|
|
|
Future<void> setPrivateKey(Uint8List key) async {
|
|
if (!await ssss.checkKey(key, keyData)) {
|
|
throw Exception('Invalid key');
|
|
}
|
|
privateKey = key;
|
|
}
|
|
|
|
Future<String> getStored(String type) async {
|
|
final privateKey = this.privateKey;
|
|
if (privateKey == null) {
|
|
throw Exception('SSSS not unlocked');
|
|
}
|
|
return await ssss.getStored(type, keyId, privateKey);
|
|
}
|
|
|
|
Future<void> store(String type, String secret, {bool add = false}) async {
|
|
final privateKey = this.privateKey;
|
|
if (privateKey == null) {
|
|
throw Exception('SSSS not unlocked');
|
|
}
|
|
await ssss.store(type, secret, keyId, privateKey, add: add);
|
|
}
|
|
|
|
Future<void> validateAndStripOtherKeys(String type, String secret) async {
|
|
final privateKey = this.privateKey;
|
|
if (privateKey == null) {
|
|
throw Exception('SSSS not unlocked');
|
|
}
|
|
await ssss.validateAndStripOtherKeys(type, secret, keyId, privateKey);
|
|
}
|
|
|
|
Future<void> maybeCacheAll() async {
|
|
final privateKey = this.privateKey;
|
|
if (privateKey == null) {
|
|
throw Exception('SSSS not unlocked');
|
|
}
|
|
await ssss.maybeCacheAll(keyId, privateKey);
|
|
}
|
|
|
|
Future<void> _postUnlock() async {
|
|
// first try to cache all secrets that aren't cached yet
|
|
await maybeCacheAll();
|
|
// now try to self-sign
|
|
if (ssss.encryption.crossSigning.enabled &&
|
|
ssss.client.userDeviceKeys[ssss.client.userID]?.masterKey != null &&
|
|
(ssss
|
|
.keyIdsFromType(EventTypes.CrossSigningMasterKey)
|
|
?.contains(keyId) ??
|
|
false) &&
|
|
(ssss.client.isUnknownSession ||
|
|
ssss.client.userDeviceKeys[ssss.client.userID]!.masterKey
|
|
?.directVerified !=
|
|
true)) {
|
|
try {
|
|
await ssss.encryption.crossSigning.selfSign(openSsss: this);
|
|
} catch (e, s) {
|
|
Logs().e('[SSSS] Failed to self-sign', e, s);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class _KeyFromPassphraseArgs {
|
|
final String passphrase;
|
|
final PassphraseInfo info;
|
|
|
|
_KeyFromPassphraseArgs({required this.passphrase, required this.info});
|
|
}
|
|
|
|
Future<Uint8List> _keyFromPassphrase(_KeyFromPassphraseArgs args) async {
|
|
return await SSSS.keyFromPassphrase(args.passphrase, args.info);
|
|
}
|