refactor: make client nullsafe

This commit is contained in:
Nicolas Werner 2021-10-15 16:35:22 +02:00
parent 17fd1f22b3
commit fb0a84d7b2
No known key found for this signature in database
GPG Key ID: C8D75E610773F2D9
18 changed files with 596 additions and 529 deletions

View File

@ -62,7 +62,8 @@ class Encryption {
crossSigning = CrossSigning(this); crossSigning = CrossSigning(this);
} }
Future<void> init(String olmAccount) async { // initial login passes null to init a new olm account
Future<void> init(String? olmAccount) async {
await olmManager.init(olmAccount); await olmManager.init(olmAccount);
_backgroundTasksRunning = true; _backgroundTasksRunning = true;
_backgroundTasks(); // start the background tasks _backgroundTasks(); // start the background tasks
@ -86,7 +87,7 @@ class Encryption {
); );
void handleDeviceOneTimeKeysCount( void handleDeviceOneTimeKeysCount(
Map<String, int> countJson, List<String> unusedFallbackKeyTypes) { Map<String, int>? countJson, List<String>? unusedFallbackKeyTypes) {
runInRoot(() => olmManager.handleDeviceOneTimeKeysCount( runInRoot(() => olmManager.handleDeviceOneTimeKeysCount(
countJson, unusedFallbackKeyTypes)); countJson, unusedFallbackKeyTypes));
} }
@ -374,12 +375,13 @@ class Encryption {
Future<void> autovalidateMasterOwnKey() async { Future<void> autovalidateMasterOwnKey() async {
// check if we can set our own master key as verified, if it isn't yet // check if we can set our own master key as verified, if it isn't yet
final masterKey = client.userDeviceKeys[client.userID]?.masterKey; final userId = client.userID;
final masterKey = client.userDeviceKeys[userId]?.masterKey;
if (client.database != null && if (client.database != null &&
masterKey != null && masterKey != null &&
userId != null &&
!masterKey.directVerified && !masterKey.directVerified &&
masterKey masterKey.hasValidSignatureChain(onlyValidateUserIds: {userId})) {
.hasValidSignatureChain(onlyValidateUserIds: {client.userID})) {
await masterKey.setVerified(true); await masterKey.setVerified(true);
} }
} }

View File

@ -95,6 +95,8 @@ class KeyManager {
}) { }) {
final senderClaimedKeys_ = senderClaimedKeys ?? <String, String>{}; final senderClaimedKeys_ = senderClaimedKeys ?? <String, String>{};
final allowedAtIndex_ = allowedAtIndex ?? <String, Map<String, int>>{}; final allowedAtIndex_ = allowedAtIndex ?? <String, Map<String, int>>{};
final userId = client.userID;
if (userId == null) return;
if (!senderClaimedKeys_.containsKey('ed25519')) { if (!senderClaimedKeys_.containsKey('ed25519')) {
final device = client.getUserDeviceKeysByCurve25519Key(senderKey); final device = client.getUserDeviceKeysByCurve25519Key(senderKey);
@ -126,7 +128,7 @@ class KeyManager {
indexes: {}, indexes: {},
roomId: roomId, roomId: roomId,
sessionId: sessionId, sessionId: sessionId,
key: client.userID, key: userId,
senderKey: senderKey, senderKey: senderKey,
senderClaimedKeys: senderClaimedKeys_, senderClaimedKeys: senderClaimedKeys_,
allowedAtIndex: allowedAtIndex_, allowedAtIndex: allowedAtIndex_,
@ -157,7 +159,7 @@ class KeyManager {
?.storeInboundGroupSession( ?.storeInboundGroupSession(
roomId, roomId,
sessionId, sessionId,
inboundGroupSession.pickle(client.userID), inboundGroupSession.pickle(userId),
json.encode(content), json.encode(content),
json.encode({}), json.encode({}),
json.encode(allowedAtIndex_), json.encode(allowedAtIndex_),
@ -169,7 +171,7 @@ class KeyManager {
return; return;
} }
if (uploaded) { if (uploaded) {
client.database.markInboundGroupSessionAsUploaded(roomId, sessionId); client.database?.markInboundGroupSessionAsUploaded(roomId, sessionId);
} else { } else {
_haveKeysToUpload = true; _haveKeysToUpload = true;
} }
@ -239,10 +241,10 @@ class KeyManager {
} }
final session = final session =
await client.database?.getInboundGroupSession(roomId, sessionId); await client.database?.getInboundGroupSession(roomId, sessionId);
if (session == null) { if (session == null) return null;
return null; final userID = client.userID;
} if (userID == null) return null;
final dbSess = SessionKey.fromDb(session, client.userID); final dbSess = SessionKey.fromDb(session, userID);
final roomInboundGroupSessions = final roomInboundGroupSessions =
_inboundGroupSessions[roomId] ??= <String, SessionKey>{}; _inboundGroupSessions[roomId] ??= <String, SessionKey>{};
if (!dbSess.isValid || if (!dbSess.isValid ||
@ -394,12 +396,10 @@ class KeyManager {
sess.outboundGroupSession!.message_index(); sess.outboundGroupSession!.message_index();
} }
} }
if (client.database != null) { await client.database?.updateInboundGroupSessionAllowedAtIndex(
await client.database.updateInboundGroupSessionAllowedAtIndex(
json.encode(inboundSess!.allowedAtIndex), json.encode(inboundSess!.allowedAtIndex),
room.id, room.id,
sess.outboundGroupSession!.session_id()); sess.outboundGroupSession!.session_id());
}
// send out the key // send out the key
await client.sendToDeviceEncryptedChunked( await client.sendToDeviceEncryptedChunked(
devicesToReceive, EventTypes.RoomKey, rawSession); devicesToReceive, EventTypes.RoomKey, rawSession);
@ -422,9 +422,11 @@ class KeyManager {
/// Store an outbound group session in the database /// Store an outbound group session in the database
Future<void> storeOutboundGroupSession( Future<void> storeOutboundGroupSession(
String roomId, OutboundGroupSession sess) async { String roomId, OutboundGroupSession sess) async {
final userID = client.userID;
if (userID == null) return;
await client.database?.storeOutboundGroupSession( await client.database?.storeOutboundGroupSession(
roomId, roomId,
sess.outboundGroupSession!.pickle(client.userID), sess.outboundGroupSession!.pickle(userID),
json.encode(sess.devices), json.encode(sess.devices),
sess.creationTime.millisecondsSinceEpoch); sess.creationTime.millisecondsSinceEpoch);
} }
@ -464,6 +466,12 @@ class KeyManager {
throw Exception( throw Exception(
'Tried to create a megolm session in a non-existing room ($roomId)!'); 'Tried to create a megolm session in a non-existing room ($roomId)!');
} }
final userID = client.userID;
if (userID == null) {
throw Exception(
'Tried to create a megolm session without being logged in!');
}
final deviceKeys = await room.getUserDeviceKeys(); final deviceKeys = await room.getUserDeviceKeys();
final deviceKeyIds = _getDeviceKeyIdMap(deviceKeys); final deviceKeyIds = _getDeviceKeyIdMap(deviceKeys);
deviceKeys.removeWhere((k) => !k.encryptToDevice); deviceKeys.removeWhere((k) => !k.encryptToDevice);
@ -498,7 +506,7 @@ class KeyManager {
devices: deviceKeyIds, devices: deviceKeyIds,
creationTime: DateTime.now(), creationTime: DateTime.now(),
outboundGroupSession: outboundGroupSession, outboundGroupSession: outboundGroupSession,
key: client.userID, key: userID,
); );
try { try {
await client.sendToDeviceEncryptedChunked( await client.sendToDeviceEncryptedChunked(
@ -523,15 +531,18 @@ class KeyManager {
/// Load an outbound group session from database /// Load an outbound group session from database
Future<void> loadOutboundGroupSession(String roomId) async { Future<void> loadOutboundGroupSession(String roomId) async {
final database = client.database;
final userID = client.userID;
if (_loadedOutboundGroupSessions.contains(roomId) || if (_loadedOutboundGroupSessions.contains(roomId) ||
_outboundGroupSessions.containsKey(roomId) || _outboundGroupSessions.containsKey(roomId) ||
client.database == null) { database == null ||
userID == null) {
return; // nothing to do return; // nothing to do
} }
_loadedOutboundGroupSessions.add(roomId); _loadedOutboundGroupSessions.add(roomId);
final sess = await client.database.getOutboundGroupSession( final sess = await database.getOutboundGroupSession(
roomId, roomId,
client.userID, userID,
); );
if (sess == null || !sess.isValid) { if (sess == null || !sess.isValid) {
return; return;
@ -699,7 +710,9 @@ class KeyManager {
bool _isUploadingKeys = false; bool _isUploadingKeys = false;
bool _haveKeysToUpload = true; bool _haveKeysToUpload = true;
Future<void> backgroundTasks() async { Future<void> backgroundTasks() async {
if (_isUploadingKeys || client.database == null) { final database = client.database;
final userID = client.userID;
if (_isUploadingKeys || database == null || userID == null) {
return; return;
} }
_isUploadingKeys = true; _isUploadingKeys = true;
@ -707,8 +720,7 @@ class KeyManager {
if (!_haveKeysToUpload || !(await isCached())) { if (!_haveKeysToUpload || !(await isCached())) {
return; // we can't backup anyways return; // we can't backup anyways
} }
final dbSessions = final dbSessions = await database.getInboundGroupSessionsToUpload();
await client.database.getInboundGroupSessionsToUpload();
if (dbSessions.isEmpty) { if (dbSessions.isEmpty) {
_haveKeysToUpload = false; _haveKeysToUpload = false;
return; // nothing to do return; // nothing to do
@ -730,7 +742,7 @@ class KeyManager {
final args = _GenerateUploadKeysArgs( final args = _GenerateUploadKeysArgs(
pubkey: backupPubKey, pubkey: backupPubKey,
dbSessions: <_DbInboundGroupSessionBundle>[], dbSessions: <_DbInboundGroupSessionBundle>[],
userId: client.userID, userId: userID,
); );
// we need to calculate verified beforehand, as else we pass a closure to an isolate // we need to calculate verified beforehand, as else we pass a closure to an isolate
// with 500 keys they do, however, noticably block the UI, which is why we give brief async suspentions in here // with 500 keys they do, however, noticably block the UI, which is why we give brief async suspentions in here
@ -741,7 +753,7 @@ class KeyManager {
client.getUserDeviceKeysByCurve25519Key(dbSession.senderKey); client.getUserDeviceKeysByCurve25519Key(dbSession.senderKey);
args.dbSessions.add(_DbInboundGroupSessionBundle( args.dbSessions.add(_DbInboundGroupSessionBundle(
dbSession: dbSession, dbSession: dbSession,
verified: device.verified, verified: device?.verified ?? false,
)); ));
i++; i++;
if (i > 10) { if (i > 10) {
@ -758,7 +770,7 @@ class KeyManager {
// and now finally mark all the keys as uploaded // and now finally mark all the keys as uploaded
// no need to optimze this, as we only run it so seldomly and almost never with many keys at once // no need to optimze this, as we only run it so seldomly and almost never with many keys at once
for (final dbSession in dbSessions) { for (final dbSession in dbSessions) {
await client.database.markInboundGroupSessionAsUploaded( await database.markInboundGroupSessionAsUploaded(
dbSession.roomId, dbSession.sessionId); dbSession.roomId, dbSession.sessionId);
} }
} finally { } finally {

View File

@ -56,7 +56,7 @@ class KeyVerificationManager {
Future<void> handleToDeviceEvent(ToDeviceEvent event) async { Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
if (!event.type.startsWith('m.key.verification.') || if (!event.type.startsWith('m.key.verification.') ||
client.verificationMethods.isEmpty) { client.verificationMethods.isEmpty != false) {
return; return;
} }
// we have key verification going on! // we have key verification going on!
@ -95,7 +95,7 @@ class KeyVerificationManager {
: event['content']['msgtype']; : event['content']['msgtype'];
if (type == null || if (type == null ||
!type.startsWith('m.key.verification.') || !type.startsWith('m.key.verification.') ||
client.verificationMethods.isEmpty) { client.verificationMethods.isEmpty != false) {
return; return;
} }
if (type == EventTypes.KeyVerificationRequest) { if (type == EventTypes.KeyVerificationRequest) {

View File

@ -36,7 +36,7 @@ class OlmManager {
/// Returns the base64 encoded keys to store them in a store. /// Returns the base64 encoded keys to store them in a store.
/// This String should **never** leave the device! /// This String should **never** leave the device!
String? get pickledOlmAccount => String? get pickledOlmAccount =>
enabled ? _olmAccount!.pickle(client.userID) : null; enabled ? _olmAccount!.pickle(client.userID!) : null;
String? get fingerprintKey => String? get fingerprintKey =>
enabled ? json.decode(_olmAccount!.identity_keys())['ed25519'] : null; enabled ? json.decode(_olmAccount!.identity_keys())['ed25519'] : null;
String? get identityKey => String? get identityKey =>
@ -70,7 +70,7 @@ class OlmManager {
try { try {
await olm.init(); await olm.init();
_olmAccount = olm.Account(); _olmAccount = olm.Account();
_olmAccount!.unpickle(client.userID, olmAccount); _olmAccount!.unpickle(client.userID!, olmAccount);
} catch (_) { } catch (_) {
_olmAccount?.free(); _olmAccount?.free();
_olmAccount = null; _olmAccount = null;
@ -279,7 +279,7 @@ class OlmManager {
// fixup accidental too many uploads. We delete only one of them so that the server has time to update the counts and because we will get rate limited anyway. // fixup accidental too many uploads. We delete only one of them so that the server has time to update the counts and because we will get rate limited anyway.
if (keyCount > _olmAccount!.max_number_of_one_time_keys()) { if (keyCount > _olmAccount!.max_number_of_one_time_keys()) {
final requestingKeysFrom = { final requestingKeysFrom = {
client.userID: {client.deviceID: 'signed_curve25519'} client.userID!: {client.deviceID!: 'signed_curve25519'}
}; };
client.claimKeys(requestingKeysFrom, timeout: 10000); client.claimKeys(requestingKeysFrom, timeout: 10000);
} }
@ -311,10 +311,7 @@ class OlmManager {
// update an existing session // update an existing session
_olmSessions[session.identityKey]![ix] = session; _olmSessions[session.identityKey]![ix] = session;
} }
if (client.database == null) { await client.database?.storeOlmSession(
return;
}
await client.database.storeOlmSession(
session.identityKey, session.identityKey,
session.sessionId!, session.sessionId!,
session.pickledSession!, session.pickledSession!,
@ -395,7 +392,7 @@ class OlmManager {
client.database?.updateClientKeys(pickledOlmAccount!); client.database?.updateClientKeys(pickledOlmAccount!);
plaintext = newSession.decrypt(type, body); plaintext = newSession.decrypt(type, body);
runInRoot(() => storeOlmSession(OlmSession( runInRoot(() => storeOlmSession(OlmSession(
key: client.userID, key: client.userID!,
identityKey: senderKey, identityKey: senderKey,
sessionId: newSession.session_id(), sessionId: newSession.session_id(),
session: newSession, session: newSession,
@ -428,25 +425,19 @@ class OlmManager {
} }
Future<List<OlmSession>> getOlmSessionsFromDatabase(String senderKey) async { Future<List<OlmSession>> getOlmSessionsFromDatabase(String senderKey) async {
if (client.database == null) {
return [];
}
final olmSessions = final olmSessions =
await client.database.getOlmSessions(senderKey, client.userID); await client.database?.getOlmSessions(senderKey, client.userID!);
return olmSessions.where((sess) => sess.isValid).toList(); return olmSessions?.where((sess) => sess.isValid).toList() ?? [];
} }
Future<void> getOlmSessionsForDevicesFromDatabase( Future<void> getOlmSessionsForDevicesFromDatabase(
List<String> senderKeys) async { List<String> senderKeys) async {
if (client.database == null) { final rows = await client.database?.getOlmSessionsForDevices(
return;
}
final rows = await client.database.getOlmSessionsForDevices(
senderKeys, senderKeys,
client.userID, client.userID!,
); );
final res = <String, List<OlmSession>>{}; final res = <String, List<OlmSession>>{};
for (final sess in rows) { for (final sess in rows ?? []) {
res[sess.identityKey] ??= <OlmSession>[]; res[sess.identityKey] ??= <OlmSession>[];
if (sess.isValid) { if (sess.isValid) {
res[sess.identityKey]!.add(sess); res[sess.identityKey]!.add(sess);
@ -565,7 +556,7 @@ class OlmManager {
session.create_outbound( session.create_outbound(
_olmAccount!, identityKey, deviceKey['key']); _olmAccount!, identityKey, deviceKey['key']);
await storeOlmSession(OlmSession( await storeOlmSession(OlmSession(
key: client.userID, key: client.userID!,
identityKey: identityKey, identityKey: identityKey,
sessionId: session.session_id(), sessionId: session.session_id(),
session: session, session: session,
@ -602,7 +593,7 @@ class OlmManager {
await storeOlmSession(sess.first); await storeOlmSession(sess.first);
if (client.database != null) { if (client.database != null) {
// ignore: unawaited_futures // ignore: unawaited_futures
runInRoot(() => client.database.setLastSentMessageUserDeviceKey( runInRoot(() => client.database?.setLastSentMessageUserDeviceKey(
json.encode({ json.encode({
'type': type, 'type': type,
'content': payload, 'content': payload,
@ -668,8 +659,10 @@ class OlmManager {
Logs().v( Logs().v(
'[OlmManager] Device ${device.userId}:${device.deviceId} generated a new olm session, replaying last sent message...'); '[OlmManager] Device ${device.userId}:${device.deviceId} generated a new olm session, replaying last sent message...');
final lastSentMessageRes = await client.database final lastSentMessageRes = await client.database
.getLastSentMessageUserDeviceKey(device.userId, device.deviceId!); ?.getLastSentMessageUserDeviceKey(device.userId, device.deviceId!);
if (lastSentMessageRes.isEmpty || (lastSentMessageRes.first.isEmpty)) { if (lastSentMessageRes == null ||
lastSentMessageRes.isEmpty ||
lastSentMessageRes.first.isEmpty) {
return; return;
} }
final lastSentMessage = json.decode(lastSentMessageRes.first); final lastSentMessage = json.decode(lastSentMessageRes.first);

View File

@ -187,7 +187,7 @@ class SSSS {
Future<void> setDefaultKeyId(String keyId) async { Future<void> setDefaultKeyId(String keyId) async {
await client.setAccountData( await client.setAccountData(
client.userID, client.userID!,
EventTypes.SecretStorageDefaultKey, EventTypes.SecretStorageDefaultKey,
SecretStorageDefaultKeyContent(key: keyId).toJson(), SecretStorageDefaultKeyContent(key: keyId).toJson(),
); );
@ -250,7 +250,7 @@ class SSSS {
syncUpdate.accountData! syncUpdate.accountData!
.any((accountData) => accountData.type == accountDataType)); .any((accountData) => accountData.type == accountDataType));
await client.setAccountData( await client.setAccountData(
client.userID, accountDataType, content.toJson()); client.userID!, accountDataType, content.toJson());
await waitForAccountData; await waitForAccountData;
final key = open(keyId); final key = open(keyId);
@ -295,7 +295,7 @@ class SSSS {
if (_cache.containsKey(type) && isValid(_cache[type])) { if (_cache.containsKey(type) && isValid(_cache[type])) {
return _cache[type]?.content; return _cache[type]?.content;
} }
final ret = await client.database.getSSSSCache(type); final ret = await client.database?.getSSSSCache(type);
if (ret == null) { if (ret == null) {
return null; return null;
} }
@ -321,10 +321,10 @@ class SSSS {
final encryptInfo = _Encrypted( final encryptInfo = _Encrypted(
iv: enc['iv'], ciphertext: enc['ciphertext'], mac: enc['mac']); iv: enc['iv'], ciphertext: enc['ciphertext'], mac: enc['mac']);
final decrypted = await decryptAes(encryptInfo, key, type); final decrypted = await decryptAes(encryptInfo, key, type);
if (cacheTypes.contains(type) && client.database != null) { final db = client.database;
if (cacheTypes.contains(type) && db != null) {
// cache the thing // cache the thing
await client.database await db.storeSSSSCache(type, keyId, enc['ciphertext'], decrypted);
.storeSSSSCache(type, keyId, enc['ciphertext'], decrypted);
if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) { if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) {
_cacheCallbacks[type]!(decrypted); _cacheCallbacks[type]!(decrypted);
} }
@ -351,11 +351,11 @@ class SSSS {
'mac': encrypted.mac, 'mac': encrypted.mac,
}; };
// store the thing in your account data // store the thing in your account data
await client.setAccountData(client.userID, type, content); await client.setAccountData(client.userID!, type, content);
if (cacheTypes.contains(type) && client.database != null) { final db = client.database;
if (cacheTypes.contains(type) && db != null) {
// cache the thing // cache the thing
await client.database await db.storeSSSSCache(type, keyId, encrypted.ciphertext, secret);
.storeSSSSCache(type, keyId, encrypted.ciphertext, secret);
if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) { if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) {
_cacheCallbacks[type]!(secret); _cacheCallbacks[type]!(secret);
} }
@ -381,10 +381,10 @@ class SSSS {
throw Exception('Secrets do not match up!'); throw Exception('Secrets do not match up!');
} }
// store the thing in your account data // store the thing in your account data
await client.setAccountData(client.userID, type, content); await client.setAccountData(client.userID!, type, content);
if (cacheTypes.contains(type) && client.database != null) { if (cacheTypes.contains(type)) {
// cache the thing // cache the thing
await client.database.storeSSSSCache( await client.database?.storeSSSSCache(
type, keyId, content['encrypted'][keyId]['ciphertext'], secret); type, keyId, content['encrypted'][keyId]['ciphertext'], secret);
} }
} }
@ -542,13 +542,13 @@ class SSSS {
return; // our request is more than 15min in the past...better not trust it anymore 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'); Logs().i('[SSSS] Secret for type ${request.type} is ok, storing it');
if (client.database != null) { final db = client.database;
if (db != null) {
final keyId = keyIdFromType(request.type); final keyId = keyIdFromType(request.type);
if (keyId != null) { if (keyId != null) {
final ciphertext = client.accountData[request.type]! final ciphertext = client.accountData[request.type]!
.content['encrypted'][keyId]['ciphertext']; .content['encrypted'][keyId]['ciphertext'];
await client.database await db.storeSSSSCache(request.type, keyId, ciphertext, secret);
.storeSSSSCache(request.type, keyId, ciphertext, secret);
if (_cacheCallbacks.containsKey(request.type)) { if (_cacheCallbacks.containsKey(request.type)) {
_cacheCallbacks[request.type]!(secret); _cacheCallbacks[request.type]!(secret);
} }

View File

@ -357,6 +357,7 @@ class Bootstrap {
checkOnlineKeyBackup(); checkOnlineKeyBackup();
return; return;
} }
final userID = client.userID!;
try { try {
Uint8List masterSigningKey; Uint8List masterSigningKey;
final secretsToStore = <String, String>{}; final secretsToStore = <String, String>{};
@ -370,7 +371,7 @@ class Bootstrap {
masterSigningKey = master.generate_seed(); masterSigningKey = master.generate_seed();
masterPub = master.init_with_seed(masterSigningKey); masterPub = master.init_with_seed(masterSigningKey);
final json = <String, dynamic>{ final json = <String, dynamic>{
'user_id': client.userID, 'user_id': userID,
'usage': ['master'], 'usage': ['master'],
'keys': <String, dynamic>{ 'keys': <String, dynamic>{
'ed25519:$masterPub': masterPub, 'ed25519:$masterPub': masterPub,
@ -414,7 +415,7 @@ class Bootstrap {
final selfSigningPriv = selfSigning.generate_seed(); final selfSigningPriv = selfSigning.generate_seed();
final selfSigningPub = selfSigning.init_with_seed(selfSigningPriv); final selfSigningPub = selfSigning.init_with_seed(selfSigningPriv);
final json = <String, dynamic>{ final json = <String, dynamic>{
'user_id': client.userID, 'user_id': userID,
'usage': ['self_signing'], 'usage': ['self_signing'],
'keys': <String, dynamic>{ 'keys': <String, dynamic>{
'ed25519:$selfSigningPub': selfSigningPub, 'ed25519:$selfSigningPub': selfSigningPub,
@ -422,7 +423,7 @@ class Bootstrap {
}; };
final signature = _sign(json); final signature = _sign(json);
json['signatures'] = <String, dynamic>{ json['signatures'] = <String, dynamic>{
client.userID: <String, dynamic>{ userID: <String, dynamic>{
'ed25519:$masterPub': signature, 'ed25519:$masterPub': signature,
}, },
}; };
@ -439,7 +440,7 @@ class Bootstrap {
final userSigningPriv = userSigning.generate_seed(); final userSigningPriv = userSigning.generate_seed();
final userSigningPub = userSigning.init_with_seed(userSigningPriv); final userSigningPub = userSigning.init_with_seed(userSigningPriv);
final json = <String, dynamic>{ final json = <String, dynamic>{
'user_id': client.userID, 'user_id': userID,
'usage': ['user_signing'], 'usage': ['user_signing'],
'keys': <String, dynamic>{ 'keys': <String, dynamic>{
'ed25519:$userSigningPub': userSigningPub, 'ed25519:$userSigningPub': userSigningPub,
@ -447,7 +448,7 @@ class Bootstrap {
}; };
final signature = _sign(json); final signature = _sign(json);
json['signatures'] = <String, dynamic>{ json['signatures'] = <String, dynamic>{
client.userID: <String, dynamic>{ userID: <String, dynamic>{
'ed25519:$masterPub': signature, 'ed25519:$masterPub': signature,
}, },
}; };
@ -462,7 +463,7 @@ class Bootstrap {
state = BootstrapState.loading; state = BootstrapState.loading;
Logs().v('Upload device signing keys.'); Logs().v('Upload device signing keys.');
await client.uiaRequestBackground( await client.uiaRequestBackground(
(AuthenticationData auth) => client.uploadCrossSigningKeys( (AuthenticationData? auth) => client.uploadCrossSigningKeys(
masterKey: masterKey, masterKey: masterKey,
selfSigningKey: selfSigningKey, selfSigningKey: selfSigningKey,
userSigningKey: userSigningKey, userSigningKey: userSigningKey,

View File

@ -152,8 +152,10 @@ class KeyVerification {
List<String> get knownVerificationMethods { List<String> get knownVerificationMethods {
final methods = <String>[]; final methods = <String>[];
if (client.verificationMethods.contains(KeyVerificationMethod.numbers) || if (client.verificationMethods?.contains(KeyVerificationMethod.numbers) ==
client.verificationMethods.contains(KeyVerificationMethod.emoji)) { true ||
client.verificationMethods?.contains(KeyVerificationMethod.emoji) ==
true) {
methods.add('m.sas.v1'); methods.add('m.sas.v1');
} }
return methods; return methods;
@ -673,11 +675,13 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
List<String> get knownAuthentificationTypes { List<String> get knownAuthentificationTypes {
final types = <String>[]; final types = <String>[];
if (request.client.verificationMethods if (request.client.verificationMethods
.contains(KeyVerificationMethod.emoji)) { ?.contains(KeyVerificationMethod.emoji) ==
true) {
types.add('emoji'); types.add('emoji');
} }
if (request.client.verificationMethods if (request.client.verificationMethods
.contains(KeyVerificationMethod.numbers)) { ?.contains(KeyVerificationMethod.numbers) ==
true) {
types.add('decimal'); types.add('decimal');
} }
return types; return types;
@ -876,7 +880,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
: theirInfo + ourInfo) + : theirInfo + ourInfo) +
request.transactionId!; request.transactionId!;
} else if (keyAgreementProtocol == 'curve25519') { } else if (keyAgreementProtocol == 'curve25519') {
final ourInfo = client.userID + client.deviceID; final ourInfo = client.userID! + client.deviceID!;
final theirInfo = request.userId + request.deviceId!; final theirInfo = request.userId + request.deviceId!;
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' + sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' +
(request.startedVerification (request.startedVerification
@ -891,8 +895,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
Future<void> _sendMac() async { Future<void> _sendMac() async {
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
client.userID + client.userID! +
client.deviceID + client.deviceID! +
request.userId + request.userId +
request.deviceId! + request.deviceId! +
request.transactionId!; request.transactionId!;
@ -929,8 +933,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
request.userId + request.userId +
request.deviceId! + request.deviceId! +
client.userID + client.userID! +
client.deviceID + client.deviceID! +
request.transactionId!; request.transactionId!;
final keyList = payload['mac'].keys.toList(); final keyList = payload['mac'].keys.toList();

File diff suppressed because it is too large Load Diff

View File

@ -35,10 +35,10 @@ abstract class DatabaseApi {
String homeserverUrl, String homeserverUrl,
String token, String token,
String userId, String userId,
String deviceId, String? deviceId,
String deviceName, String? deviceName,
String prevBatch, String? prevBatch,
String olmAccount, String? olmAccount,
); );
Future insertClient( Future insertClient(
@ -46,10 +46,10 @@ abstract class DatabaseApi {
String homeserverUrl, String homeserverUrl,
String token, String token,
String userId, String userId,
String deviceId, String? deviceId,
String deviceName, String? deviceName,
String prevBatch, String? prevBatch,
String olmAccount, String? olmAccount,
); );
Future<List<Room>> getRoomList(Client client); Future<List<Room>> getRoomList(Client client);
@ -58,8 +58,7 @@ abstract class DatabaseApi {
/// Stores a RoomUpdate object in the database. Must be called inside of /// Stores a RoomUpdate object in the database. Must be called inside of
/// [transaction]. /// [transaction].
Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate, Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate);
[Room oldRoom]);
/// Stores an EventUpdate object in the database. Must be called inside of /// Stores an EventUpdate object in the database. Must be called inside of
/// [transaction]. /// [transaction].

View File

@ -507,6 +507,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
Future<List<Room>> getRoomList(Client client) => Future<List<Room>> getRoomList(Client client) =>
runBenchmarked<List<Room>>('Get room list from hive', () async { runBenchmarked<List<Room>>('Get room list from hive', () async {
final rooms = <String, Room>{}; final rooms = <String, Room>{};
final userID = client.userID;
final importantRoomStates = client.importantStateEvents; final importantRoomStates = client.importantStateEvents;
for (final key in _roomsBox.keys) { for (final key in _roomsBox.keys) {
// Get the room // Get the room
@ -515,7 +516,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
// let's see if we need any m.room.member events // let's see if we need any m.room.member events
// We always need the member event for ourself // We always need the member event for ourself
final membersToPostload = <String>{client.userID}; final membersToPostload = <String>{if (userID != null) userID};
// If the room is a direct chat, those IDs should be there too // If the room is a direct chat, those IDs should be there too
if (room.isDirectChat) membersToPostload.add(room.directChatMatrixID); if (room.isDirectChat) membersToPostload.add(room.directChatMatrixID);
// the lastEvent message preview might have an author we need to fetch, if it is a group chat // the lastEvent message preview might have an author we need to fetch, if it is a group chat
@ -671,10 +672,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
String homeserverUrl, String homeserverUrl,
String token, String token,
String userId, String userId,
String deviceId, String? deviceId,
String deviceName, String? deviceName,
String prevBatch, String? prevBatch,
String olmAccount) async { String? olmAccount) async {
await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('homeserver_url', homeserverUrl);
await _clientBox.put('token', token); await _clientBox.put('token', token);
await _clientBox.put('user_id', userId); await _clientBox.put('user_id', userId);
@ -1076,8 +1077,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
} }
@override @override
Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate, Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate) async {
[dynamic _]) async {
// Leave room if membership is leave // Leave room if membership is leave
if (roomUpdate is LeftRoomUpdate) { if (roomUpdate is LeftRoomUpdate) {
await forgetRoom(roomId); await forgetRoom(roomId);
@ -1250,10 +1250,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
String homeserverUrl, String homeserverUrl,
String token, String token,
String userId, String userId,
String deviceId, String? deviceId,
String deviceName, String? deviceName,
String prevBatch, String? prevBatch,
String olmAccount, String? olmAccount,
) async { ) async {
await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('homeserver_url', homeserverUrl);
await _clientBox.put('token', token); await _clientBox.put('token', token);

View File

@ -518,7 +518,7 @@ class Event extends MatrixEvent {
/// event and returns it as a [MatrixFile]. If this event doesn't /// event and returns it as a [MatrixFile]. If this event doesn't
/// contain an attachment, this throws an error. Set [getThumbnail] to /// contain an attachment, this throws an error. Set [getThumbnail] to
/// true to download the thumbnail instead. /// true to download the thumbnail instead.
Future<MatrixFile> downloadAndDecryptAttachment( Future<MatrixFile?> downloadAndDecryptAttachment(
{bool getThumbnail = false, {bool getThumbnail = false,
Future<Uint8List> Function(Uri)? downloadCallback}) async { Future<Uint8List> Function(Uri)? downloadCallback}) async {
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) { if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
@ -548,7 +548,7 @@ class Event extends MatrixEvent {
Uint8List? uint8list; Uint8List? uint8list;
if (storeable) { if (storeable) {
uint8list = await client.database.getFile(mxcUrl); uint8list = await client.database?.getFile(mxcUrl);
} }
// Download the file // Download the file
@ -577,9 +577,10 @@ class Event extends MatrixEvent {
k: fileMap['key']['k'], k: fileMap['key']['k'],
sha256: fileMap['hashes']['sha256'], sha256: fileMap['hashes']['sha256'],
); );
uint8list = await client.runInBackground(decryptFile, encryptedFile); uint8list = await client.runInBackground<Uint8List?, EncryptedFile>(
decryptFile, encryptedFile);
} }
return MatrixFile(bytes: uint8list, name: body); return uint8list != null ? MatrixFile(bytes: uint8list, name: body) : null;
} }
/// Returns if this is a known event type. /// Returns if this is a known event type.

View File

@ -132,15 +132,16 @@ class Timeline {
void _sessionKeyReceived(String sessionId) async { void _sessionKeyReceived(String sessionId) async {
var decryptAtLeastOneEvent = false; var decryptAtLeastOneEvent = false;
final decryptFn = () async { final decryptFn = () async {
if (!room.client.encryptionEnabled) { final encryption = room.client.encryption;
if (!room.client.encryptionEnabled || encryption == null) {
return; return;
} }
for (var i = 0; i < events.length; i++) { for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted && if (events[i].type == EventTypes.Encrypted &&
events[i].messageType == MessageTypes.BadEncrypted && events[i].messageType == MessageTypes.BadEncrypted &&
events[i].content['session_id'] == sessionId) { events[i].content['session_id'] == sessionId) {
events[i] = await room.client.encryption events[i] = await encryption.decryptRoomEvent(room.id, events[i],
.decryptRoomEvent(room.id, events[i], store: true); store: true);
if (events[i].type != EventTypes.Encrypted) { if (events[i].type != EventTypes.Encrypted) {
decryptAtLeastOneEvent = true; decryptAtLeastOneEvent = true;
} }
@ -148,7 +149,7 @@ class Timeline {
} }
}; };
if (room.client.database != null) { if (room.client.database != null) {
await room.client.database.transaction(decryptFn); await room.client.database?.transaction(decryptFn);
} else { } else {
await decryptFn(); await decryptFn();
} }
@ -162,7 +163,7 @@ class Timeline {
event.messageType == MessageTypes.BadEncrypted && event.messageType == MessageTypes.BadEncrypted &&
event.content['can_request_session'] == true) { event.content['can_request_session'] == true) {
try { try {
room.client.encryption.keyManager.maybeAutoRequest(room.id, room.client.encryption?.keyManager.maybeAutoRequest(room.id,
event.content['session_id'], event.content['sender_key']); event.content['session_id'], event.content['sender_key']);
} catch (_) { } catch (_) {
// dispose // dispose

View File

@ -178,7 +178,7 @@ extension CommandsClientExtension on Client {
return await args.room.client.setRoomStateWithKey( return await args.room.client.setRoomStateWithKey(
args.room.id, args.room.id,
EventTypes.RoomMember, EventTypes.RoomMember,
args.room.client.userID, args.room.client.userID!,
currentEventJson, currentEventJson,
); );
}); });
@ -191,7 +191,7 @@ extension CommandsClientExtension on Client {
return await args.room.client.setRoomStateWithKey( return await args.room.client.setRoomStateWithKey(
args.room.id, args.room.id,
EventTypes.RoomMember, EventTypes.RoomMember,
args.room.client.userID, args.room.client.userID!,
currentEventJson, currentEventJson,
); );
}); });

View File

@ -68,17 +68,21 @@ class DeviceKeysList {
} }
Future<KeyVerification> startVerification() async { Future<KeyVerification> startVerification() async {
final encryption = client.encryption;
if (encryption == null) {
throw Exception('Encryption not enabled');
}
if (userId != client.userID) { if (userId != client.userID) {
// in-room verification with someone else // in-room verification with someone else
final roomId = await client.startDirectChat(userId); final roomId = await client.startDirectChat(userId);
if (roomId == if (roomId == null) {
null /* can be null as long as startDirectChat is not migrated */) {
throw Exception('Unable to start new room'); throw Exception('Unable to start new room');
} }
final room = final room =
client.getRoomById(roomId) ?? Room(id: roomId, client: client); client.getRoomById(roomId) ?? Room(id: roomId, client: client);
final request = KeyVerification( final request =
encryption: client.encryption, room: room, userId: userId); KeyVerification(encryption: encryption, room: room, userId: userId);
await request.start(); await request.start();
// no need to add to the request client object. As we are doing a room // no need to add to the request client object. As we are doing a room
// verification request that'll happen automatically once we know the transaction id // verification request that'll happen automatically once we know the transaction id
@ -86,9 +90,9 @@ class DeviceKeysList {
} else { } else {
// broadcast self-verification // broadcast self-verification
final request = KeyVerification( final request = KeyVerification(
encryption: client.encryption, userId: userId, deviceId: '*'); encryption: encryption, userId: userId, deviceId: '*');
await request.start(); await request.start();
client.encryption.keyVerificationManager.addRequest(request); encryption.keyVerificationManager.addRequest(request);
return request; return request;
} }
} }
@ -314,13 +318,15 @@ abstract class SignableKey extends MatrixSignableKey {
Future<void> setVerified(bool newVerified, [bool sign = true]) async { Future<void> setVerified(bool newVerified, [bool sign = true]) async {
_verified = newVerified; _verified = newVerified;
final encryption = client.encryption;
if (newVerified && if (newVerified &&
sign && sign &&
encryption != null &&
client.encryptionEnabled && client.encryptionEnabled &&
client.encryption.crossSigning.signable([this])) { encryption.crossSigning.signable([this])) {
// sign the key! // sign the key!
// ignore: unawaited_futures // ignore: unawaited_futures
client.encryption.crossSigning.sign([this]); encryption.crossSigning.sign([this]);
} }
} }
@ -493,11 +499,16 @@ class DeviceKeys extends SignableKey {
if (!isValid) { if (!isValid) {
throw Exception('setVerification called on invalid key'); throw Exception('setVerification called on invalid key');
} }
final encryption = client.encryption;
if (encryption == null) {
throw Exception('setVerification called with disabled encryption');
}
final request = KeyVerification( final request = KeyVerification(
encryption: client.encryption, userId: userId, deviceId: deviceId!); encryption: encryption, userId: userId, deviceId: deviceId!);
request.start(); request.start();
client.encryption.keyVerificationManager.addRequest(request); encryption.keyVerificationManager.addRequest(request);
return request; return request;
} }
} }

View File

@ -49,12 +49,14 @@ class EventUpdate {
}); });
Future<EventUpdate> decrypt(Room room, {bool store = false}) async { Future<EventUpdate> decrypt(Room room, {bool store = false}) async {
final encryption = room.client.encryption;
if (content['type'] != EventTypes.Encrypted || if (content['type'] != EventTypes.Encrypted ||
!room.client.encryptionEnabled) { !room.client.encryptionEnabled ||
encryption == null) {
return this; return this;
} }
try { try {
final decrpytedEvent = await room.client.encryption.decryptRoomEvent( final decrpytedEvent = await encryption.decryptRoomEvent(
room.id, Event.fromJson(content, room), room.id, Event.fromJson(content, room),
store: store, updateType: type); store: store, updateType: type);
return EventUpdate( return EventUpdate(

View File

@ -24,7 +24,9 @@ extension MxcUriExtension on Uri {
/// Returns a download Link to this content. /// Returns a download Link to this content.
Uri getDownloadLink(Client matrix) => isScheme('mxc') Uri getDownloadLink(Client matrix) => isScheme('mxc')
? matrix.homeserver != null ? matrix.homeserver != null
? matrix.homeserver.resolve('_matrix/media/r0/download/$host$path') ? matrix.homeserver
?.resolve('_matrix/media/r0/download/$host$path') ??
Uri()
: Uri() : Uri()
: this; : this;
@ -39,14 +41,15 @@ extension MxcUriExtension on Uri {
ThumbnailMethod? method = ThumbnailMethod.crop, ThumbnailMethod? method = ThumbnailMethod.crop,
bool? animated = false}) { bool? animated = false}) {
if (!isScheme('mxc')) return this; if (!isScheme('mxc')) return this;
if (matrix.homeserver == null) { final homeserver = matrix.homeserver;
if (homeserver == null) {
return Uri(); return Uri();
} }
return Uri( return Uri(
scheme: matrix.homeserver.scheme, scheme: homeserver.scheme,
host: matrix.homeserver.host, host: homeserver.host,
path: '/_matrix/media/r0/thumbnail/$host$path', path: '/_matrix/media/r0/thumbnail/$host$path',
port: matrix.homeserver.port, port: homeserver.port,
queryParameters: { queryParameters: {
if (width != null) 'width': width.round().toString(), if (width != null) 'width': width.round().toString(),
if (height != null) 'height': height.round().toString(), if (height != null) 'height': height.round().toString(),

View File

@ -596,6 +596,7 @@ void main() {
expect(client2.homeserver, client1.homeserver); expect(client2.homeserver, client1.homeserver);
expect(client2.deviceID, client1.deviceID); expect(client2.deviceID, client1.deviceID);
expect(client2.deviceName, client1.deviceName); expect(client2.deviceName, client1.deviceName);
expect(client2.rooms.length, 2);
if (client2.encryptionEnabled) { if (client2.encryptionEnabled) {
expect(client2.encryption.fingerprintKey, expect(client2.encryption.fingerprintKey,
client1.encryption.fingerprintKey); client1.encryption.fingerprintKey);

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH * Copyright (C) 2019, 2020 Famedly GmbH
@ -25,11 +24,11 @@ import 'package:matrix/src/database/hive_database.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
Future<DatabaseApi> getDatabase(Client _) => getHiveDatabase(_); Future<DatabaseApi> getDatabase(Client? _) => getHiveDatabase(_);
bool hiveInitialized = false; bool hiveInitialized = false;
Future<FamedlySdkHiveDatabase> getHiveDatabase(Client c) async { Future<FamedlySdkHiveDatabase> getHiveDatabase(Client? c) async {
if (!hiveInitialized) { if (!hiveInitialized) {
final fileSystem = MemoryFileSystem(); final fileSystem = MemoryFileSystem();
final testHivePath = final testHivePath =
@ -38,7 +37,7 @@ Future<FamedlySdkHiveDatabase> getHiveDatabase(Client c) async {
Hive.init(testHivePath); Hive.init(testHivePath);
hiveInitialized = true; hiveInitialized = true;
} }
final db = FamedlySdkHiveDatabase('unit_test.${c.hashCode}'); final db = FamedlySdkHiveDatabase('unit_test.${c?.hashCode}');
await db.open(); await db.open();
return db; return db;
} }