updateUserDeviceKeys method
Implementation
Future<void> updateUserDeviceKeys({Set<String>? additionalUsers}) async {
try {
final database = this.database;
if (!isLogged() || database == null) return;
final dbActions = <Future<dynamic> Function()>[];
final trackedUserIds = await _getUserIdsInEncryptedRooms();
if (!isLogged()) return;
trackedUserIds.add(userID!);
if (additionalUsers != null) trackedUserIds.addAll(additionalUsers);
// Remove all userIds we no longer need to track the devices of.
_userDeviceKeys
.removeWhere((String userId, v) => !trackedUserIds.contains(userId));
// Check if there are outdated device key lists. Add it to the set.
final outdatedLists = <String, List<String>>{};
for (final userId in (additionalUsers ?? <String>[])) {
outdatedLists[userId] = [];
}
for (final userId in trackedUserIds) {
final deviceKeysList =
_userDeviceKeys[userId] ??= DeviceKeysList(userId, this);
final failure = _keyQueryFailures[userId.domain];
// deviceKeysList.outdated is not nullable but we have seen this error
// in production: `Failed assertion: boolean expression must not be null`
// So this could either be a null safety bug in Dart or a result of
// using unsound null safety. The extra equal check `!= false` should
// save us here.
if (deviceKeysList.outdated != false &&
(failure == null ||
DateTime.now()
.subtract(Duration(minutes: 5))
.isAfter(failure))) {
outdatedLists[userId] = [];
}
}
if (outdatedLists.isNotEmpty) {
// Request the missing device key lists from the server.
final response = await queryKeys(outdatedLists, timeout: 10000);
if (!isLogged()) return;
final deviceKeys = response.deviceKeys;
if (deviceKeys != null) {
for (final rawDeviceKeyListEntry in deviceKeys.entries) {
final userId = rawDeviceKeyListEntry.key;
final userKeys =
_userDeviceKeys[userId] ??= DeviceKeysList(userId, this);
final oldKeys = Map<String, DeviceKeys>.from(userKeys.deviceKeys);
userKeys.deviceKeys = {};
for (final rawDeviceKeyEntry
in rawDeviceKeyListEntry.value.entries) {
final deviceId = rawDeviceKeyEntry.key;
// Set the new device key for this device
final entry = DeviceKeys.fromSDNDeviceKeys(
rawDeviceKeyEntry.value, this, oldKeys[deviceId]?.lastActive);
final ed25519Key = entry.ed25519Key;
final curve25519Key = entry.curve25519Key;
if (entry.isValid &&
deviceId == entry.deviceId &&
ed25519Key != null &&
curve25519Key != null) {
// Check if deviceId or deviceKeys are known
if (!oldKeys.containsKey(deviceId)) {
final oldPublicKeys =
await database.deviceIdSeen(userId, deviceId);
if (oldPublicKeys != null &&
oldPublicKeys != curve25519Key + ed25519Key) {
Logs().w(
'Already seen Device ID has been added again. This might be an attack!');
continue;
}
final oldDeviceId = await database.publicKeySeen(ed25519Key);
if (oldDeviceId != null && oldDeviceId != deviceId) {
Logs().w(
'Already seen ED25519 has been added again. This might be an attack!');
continue;
}
final oldDeviceId2 =
await database.publicKeySeen(curve25519Key);
if (oldDeviceId2 != null && oldDeviceId2 != deviceId) {
Logs().w(
'Already seen Curve25519 has been added again. This might be an attack!');
continue;
}
await database.addSeenDeviceId(
userId, deviceId, curve25519Key + ed25519Key);
await database.addSeenPublicKey(ed25519Key, deviceId);
await database.addSeenPublicKey(curve25519Key, deviceId);
}
// is this a new key or the same one as an old one?
// better store an update - the signatures might have changed!
final oldKey = oldKeys[deviceId];
if (oldKey == null ||
(oldKey.ed25519Key == entry.ed25519Key &&
oldKey.curve25519Key == entry.curve25519Key)) {
if (oldKey != null) {
// be sure to save the verified status
entry.setDirectVerified(oldKey.directVerified);
entry.blocked = oldKey.blocked;
entry.validSignatures = oldKey.validSignatures;
}
userKeys.deviceKeys[deviceId] = entry;
if (deviceId == deviceID &&
entry.ed25519Key == fingerprintKey) {
// Always trust the own device
entry.setDirectVerified(true);
}
dbActions.add(() => database.storeUserDeviceKey(
userId,
deviceId,
json.encode(entry.toJson()),
entry.directVerified,
entry.blocked,
entry.lastActive.millisecondsSinceEpoch,
));
} else if (oldKeys.containsKey(deviceId)) {
// This shouldn't ever happen. The same device ID has gotten
// a new public key. So we ignore the update. TODO: ask krille
// if we should instead use the new key with unknown verified / blocked status
userKeys.deviceKeys[deviceId] = oldKeys[deviceId]!;
}
} else {
Logs().w('Invalid device ${entry.userId}:${entry.deviceId}');
}
}
// delete old/unused entries
for (final oldDeviceKeyEntry in oldKeys.entries) {
final deviceId = oldDeviceKeyEntry.key;
if (!userKeys.deviceKeys.containsKey(deviceId)) {
// we need to remove an old key
dbActions
.add(() => database.removeUserDeviceKey(userId, deviceId));
}
}
userKeys.outdated = false;
dbActions
.add(() => database.storeUserDeviceKeysInfo(userId, false));
}
}
// next we parse and persist the cross signing keys
final crossSigningTypes = {
'master': response.masterKeys,
'self_signing': response.selfSigningKeys,
'user_signing': response.userSigningKeys,
};
for (final crossSigningKeysEntry in crossSigningTypes.entries) {
final keyType = crossSigningKeysEntry.key;
final keys = crossSigningKeysEntry.value;
if (keys == null) {
continue;
}
for (final crossSigningKeyListEntry in keys.entries) {
final userId = crossSigningKeyListEntry.key;
final userKeys =
_userDeviceKeys[userId] ??= DeviceKeysList(userId, this);
final oldKeys =
Map<String, CrossSigningKey>.from(userKeys.crossSigningKeys);
userKeys.crossSigningKeys = {};
// add the types we aren't handling atm back
for (final oldEntry in oldKeys.entries) {
if (!oldEntry.value.usage.contains(keyType)) {
userKeys.crossSigningKeys[oldEntry.key] = oldEntry.value;
} else {
// There is a previous cross-signing key with this usage, that we no
// longer need/use. Clear it from the database.
dbActions.add(() =>
database.removeUserCrossSigningKey(userId, oldEntry.key));
}
}
final entry = CrossSigningKey.fromSDNCrossSigningKey(
crossSigningKeyListEntry.value, this);
final publicKey = entry.publicKey;
if (entry.isValid && publicKey != null) {
final oldKey = oldKeys[publicKey];
if (oldKey == null || oldKey.ed25519Key == entry.ed25519Key) {
if (oldKey != null) {
// be sure to save the verification status
entry.setDirectVerified(oldKey.directVerified);
entry.blocked = oldKey.blocked;
entry.validSignatures = oldKey.validSignatures;
}
userKeys.crossSigningKeys[publicKey] = entry;
} else {
// This shouldn't ever happen. The same device ID has gotten
// a new public key. So we ignore the update. TODO: ask krille
// if we should instead use the new key with unknown verified / blocked status
userKeys.crossSigningKeys[publicKey] = oldKey;
}
dbActions.add(() => database.storeUserCrossSigningKey(
userId,
publicKey,
json.encode(entry.toJson()),
entry.directVerified,
entry.blocked,
));
}
_userDeviceKeys[userId]?.outdated = false;
dbActions
.add(() => database.storeUserDeviceKeysInfo(userId, false));
}
}
// now process all the failures
if (response.failures != null) {
for (final failureDomain in response.failures?.keys ?? <String>[]) {
_keyQueryFailures[failureDomain] = DateTime.now();
}
}
}
if (dbActions.isNotEmpty) {
if (!isLogged()) return;
await database.transaction(() async {
for (final f in dbActions) {
await f();
}
});
}
} catch (e, s) {
Logs().e('[LibOlm] Unable to update user device keys', e, s);
}
}