refactor: _updateUserDeviceKeys method

This commit is contained in:
Christian Pauly 2021-09-09 11:12:00 +02:00 committed by Krille Fear
parent 56af96c7ea
commit 00771fc209
8 changed files with 228 additions and 42 deletions

View File

@ -370,10 +370,12 @@ class KeyManager {
for (final device in devicesToReceive) {
inboundSess.allowedAtIndex[device.userId] ??= <String, int>{};
if (!inboundSess.allowedAtIndex[device.userId]
.containsKey(device.deviceId) ||
inboundSess.allowedAtIndex[device.userId][device.deviceId] >
.containsKey(device.curve25519Key) ||
inboundSess.allowedAtIndex[device.userId]
[device.curve25519Key] >
sess.outboundGroupSession.message_index()) {
inboundSess.allowedAtIndex[device.userId][device.deviceId] =
inboundSess.allowedAtIndex[device.userId]
[device.curve25519Key] =
sess.outboundGroupSession.message_index();
}
}
@ -471,7 +473,7 @@ class KeyManager {
final allowedAtIndex = <String, Map<String, int>>{};
for (final device in deviceKeys) {
allowedAtIndex[device.userId] ??= <String, int>{};
allowedAtIndex[device.userId][device.deviceId] =
allowedAtIndex[device.userId][device.curve25519Key] =
outboundGroupSession.message_index();
}
setInboundGroupSession(
@ -832,7 +834,8 @@ class KeyManager {
// if we know the user may see the message, then we can just forward the key.
// we do not need to check if the device is verified, just if it is not blocked,
// as that is the logic we already initially try to send out the room keys.
final index = session.allowedAtIndex[device.userId][device.deviceId];
final index =
session.allowedAtIndex[device.userId][device.curve25519Key];
Logs().i(
'[KeyManager] Valid foreign request, forwarding key at index $index...');
await roomKeyRequest.forwardKey(index);

View File

@ -1138,7 +1138,7 @@ class Client extends MatrixApi {
// ignore: unawaited_futures
database?.deleteOldFiles(
DateTime.now().subtract(Duration(days: 30)).millisecondsSinceEpoch);
await _updateUserDeviceKeys();
await updateUserDeviceKeys();
if (encryptionEnabled) {
encryption.onSync();
}
@ -1642,9 +1642,9 @@ class Client extends MatrixApi {
final Map<String, DateTime> _keyQueryFailures = {};
Future<void> _updateUserDeviceKeys() async {
Future<void> updateUserDeviceKeys() async {
try {
if (!isLogged()) return;
if (!isLogged() || database == null) return;
final dbActions = <Future<dynamic> Function()>[];
final trackedUserIds = await _getUserIdsInEncryptedRooms();
if (!isLogged()) return;
@ -1691,11 +1691,45 @@ class Client extends MatrixApi {
// Set the new device key for this device
final entry = DeviceKeys.fromMatrixDeviceKeys(
rawDeviceKeyEntry.value, this, oldKeys[deviceId]?.lastActive);
if (entry.isValid) {
if (entry.isValid && deviceId == entry.deviceId) {
// Check if deviceId or deviceKeys are known
if (!oldKeys.containsKey(deviceId)) {
final oldPublicKeys =
await database.deviceIdSeen(id, userId, deviceId);
if (oldPublicKeys != null &&
oldPublicKeys != entry.curve25519Key + entry.ed25519Key) {
Logs().w(
'Already seen Device ID has been added again. This might be an attack!');
continue;
}
final oldDeviceId =
await database.publicKeySeen(id, entry.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(id, entry.curve25519Key);
if (oldDeviceId2 != null && oldDeviceId2 != deviceId) {
Logs().w(
'Already seen Curve25519 has been added again. This might be an attack!');
continue;
}
await database.addSeenDeviceId(id, userId, deviceId,
entry.curve25519Key + entry.ed25519Key);
await database.addSeenPublicKey(
id, entry.ed25519Key, deviceId);
await database.addSeenPublicKey(
id, entry.curve25519Key, deviceId);
}
// is this a new key or the same one as an old one?
// better store an update - the signatures might have changed!
if (!oldKeys.containsKey(deviceId) ||
oldKeys[deviceId].ed25519Key == entry.ed25519Key) {
(oldKeys[deviceId].ed25519Key == entry.ed25519Key &&
oldKeys[deviceId].curve25519Key ==
entry.curve25519Key)) {
if (oldKeys.containsKey(deviceId)) {
// be sure to save the verified status
entry.setDirectVerified(oldKeys[deviceId].directVerified);
@ -1708,17 +1742,15 @@ class Client extends MatrixApi {
// Always trust the own device
entry.setDirectVerified(true);
}
if (database != null) {
dbActions.add(() => database.storeUserDeviceKey(
id,
userId,
deviceId,
json.encode(entry.toJson()),
entry.directVerified,
entry.blocked,
entry.lastActive.millisecondsSinceEpoch,
));
}
dbActions.add(() => database.storeUserDeviceKey(
id,
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
@ -1731,14 +1763,12 @@ class Client extends MatrixApi {
}
}
// delete old/unused entries
if (database != null) {
for (final oldDeviceKeyEntry in oldKeys.entries) {
final deviceId = oldDeviceKeyEntry.key;
if (!_userDeviceKeys[userId].deviceKeys.containsKey(deviceId)) {
// we need to remove an old key
dbActions.add(
() => database.removeUserDeviceKey(id, userId, deviceId));
}
for (final oldDeviceKeyEntry in oldKeys.entries) {
final deviceId = oldDeviceKeyEntry.key;
if (!_userDeviceKeys[userId].deviceKeys.containsKey(deviceId)) {
// we need to remove an old key
dbActions.add(
() => database.removeUserDeviceKey(id, userId, deviceId));
}
}
_userDeviceKeys[userId].outdated = false;

View File

@ -318,6 +318,16 @@ abstract class DatabaseApi {
Future<List<StoredInboundGroupSession>> getInboundGroupSessionsToUpload();
Future<void> addSeenDeviceId(
int clientId, String userId, String deviceId, String publicKeys);
Future<void> addSeenPublicKey(
int clientId, String publicKey, String deviceId);
Future<String?> deviceIdSeen(int clientId, userId, deviceId);
Future<String?> publicKeySeen(int clientId, String publicKey);
Future<dynamic> close();
Future<T> transaction<T>(Future<T> Function() action);

View File

@ -36,7 +36,7 @@ import 'package:hive/hive.dart';
///
/// This database does not support file caching!
class FamedlySdkHiveDatabase extends DatabaseApi {
static const int version = 4;
static const int version = 5;
final String name;
late Box _clientBox;
late Box _accountDataBox;
@ -74,6 +74,11 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
/// Key is a tuple as MultiKey(roomId, eventId)
late LazyBox _eventsBox;
/// Key is a tuple as MultiKey(userId, deviceId)
late LazyBox _seenDeviceIdsBox;
late LazyBox _seenDeviceKeysBox;
String get _clientBoxName => '$name.box.client';
String get _accountDataBoxName => '$name.box.account_data';
String get _roomsBoxName => '$name.box.rooms';
@ -93,6 +98,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
String get _presencesBoxName => '$name.box.presences';
String get _timelineFragmentsBoxName => '$name.box.timeline_fragments';
String get _eventsBoxName => '$name.box.events';
String get _seenDeviceIdsBoxName => '$name.box.seen_device_ids';
String get _seenDeviceKeysBoxName => '$name.box.seen_device_keys';
final HiveCipher? encryptionCipher;
@ -120,6 +127,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
action(_presencesBox),
action(_timelineFragmentsBox),
action(_eventsBox),
action(_seenDeviceIdsBox),
action(_seenDeviceKeysBox),
]);
Future<void> open() async {
@ -191,6 +200,14 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
_eventsBoxName,
encryptionCipher: encryptionCipher,
);
_seenDeviceIdsBox = await Hive.openLazyBox(
_seenDeviceIdsBoxName,
encryptionCipher: encryptionCipher,
);
_seenDeviceKeysBox = await Hive.openLazyBox(
_seenDeviceKeysBoxName,
encryptionCipher: encryptionCipher,
);
// Check version and check if we need a migration
final currentVersion = (await _clientBox.get('version') as int?);
@ -205,6 +222,25 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
Future<void> _migrateFromVersion(int currentVersion) async {
Logs().i('Migrate Hive database from version $currentVersion to $version');
if (version == 5) {
for (final key in _userDeviceKeysBox.keys) {
try {
final raw = await _userDeviceKeysBox.get(key) as Map;
if (!raw.containsKey('keys')) continue;
final deviceKeys = DeviceKeys.fromJson(
convertToJson(raw),
Client(''),
);
await addSeenDeviceId(0, deviceKeys.userId, deviceKeys.deviceId,
deviceKeys.curve25519Key + deviceKeys.ed25519Key);
await addSeenPublicKey(0, deviceKeys.ed25519Key, deviceKeys.deviceId);
await addSeenPublicKey(
0, deviceKeys.curve25519Key, deviceKeys.deviceId);
} catch (e) {
Logs().w('Can not migrate device $key', e);
}
}
}
await clearCache(0);
await _clientBox.put('version', version);
}
@ -1232,6 +1268,39 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
.map((raw) => StoredInboundGroupSession.fromJson(convertToJson(raw)))
.toList();
}
@override
Future<void> addSeenDeviceId(
int clientId,
String userId,
String deviceId,
String publicKeysHash,
) =>
_seenDeviceIdsBox.put(
MultiKey(userId, deviceId).toString(), publicKeysHash);
@override
Future<void> addSeenPublicKey(
int clientId,
String publicKey,
String deviceId,
) =>
_seenDeviceKeysBox.put(publicKey.toHiveKey, deviceId);
@override
Future<String?> deviceIdSeen(int clientId, userId, deviceId) async {
final raw =
await _seenDeviceIdsBox.get(MultiKey(userId, deviceId).toString());
if (raw == null) return null;
return raw as String;
}
@override
Future<String?> publicKeySeen(int clientId, String publicKey) async {
final raw = await _seenDeviceKeysBox.get(publicKey.toHiveKey);
if (raw == null) return null;
return raw as String;
}
}
dynamic _castValue(dynamic value) {

View File

@ -52,7 +52,11 @@ void main() {
/// Check if all Elements get created
matrix = Client('testclient', httpClient: FakeMatrixApi());
matrix = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
);
eventUpdateListFuture = matrix.onEvent.stream.toList();
toDeviceUpdateListFuture = matrix.onToDeviceEvent.stream.toList();
@ -291,7 +295,11 @@ void main() {
});
test('Login', () async {
matrix = Client('testclient', httpClient: FakeMatrixApi());
matrix = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
);
eventUpdateListFuture = matrix.onEvent.stream.toList();

View File

@ -25,6 +25,7 @@ import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import '../fake_client.dart';
import '../fake_matrix_api.dart';
void main() {
group('Key Manager', () {
@ -113,8 +114,14 @@ void main() {
var inbound = client.encryption.keyManager.getInboundGroupSession(
roomId, sess.outboundGroupSession.session_id(), client.identityKey);
expect(inbound != null, true);
expect(inbound.allowedAtIndex['@alice:example.com']['JLAFKJWSCS'], 0);
expect(inbound.allowedAtIndex['@alice:example.com']['OTHERDEVICE'], 0);
expect(
inbound.allowedAtIndex['@alice:example.com']
['L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8'],
0);
expect(
inbound.allowedAtIndex['@alice:example.com']
['wMIDhiQl5jEXQrTB03ePOSQfR8sA/KMrW0CIfFfXKEE'],
0);
// rotate after too many messages
sess.sentMessages = 300;
@ -208,9 +215,18 @@ void main() {
true);
inbound = client.encryption.keyManager.getInboundGroupSession(
roomId, sess.outboundGroupSession.session_id(), client.identityKey);
expect(inbound.allowedAtIndex['@alice:example.com']['JLAFKJWSCS'], 0);
expect(inbound.allowedAtIndex['@alice:example.com']['OTHERDEVICE'], 0);
expect(inbound.allowedAtIndex['@alice:example.com']['NEWDEVICE'], 1);
expect(
inbound.allowedAtIndex['@alice:example.com']
['L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8'],
0);
expect(
inbound.allowedAtIndex['@alice:example.com']
['wMIDhiQl5jEXQrTB03ePOSQfR8sA/KMrW0CIfFfXKEE'],
0);
expect(
inbound.allowedAtIndex['@alice:example.com']
['bnKQp6pPW0l9cGoIgHpBoK5OUi4h0gylJ7upc4asFV8'],
1);
// do not rotate if new user is added
member.content['membership'] = 'leave';
@ -500,9 +516,56 @@ void main() {
session.free();
});
test('Reused deviceID attack', () async {
if (!olmEnabled) return;
Logs().level = Level.warning;
client ??= await getClient();
// Ensure the device came from sync
expect(
client.userDeviceKeys['@alice:example.com']
.deviceKeys['JLAFKJWSCS'] !=
null,
true);
// Alice removes her device
client.userDeviceKeys['@alice:example.com'].deviceKeys
.remove('JLAFKJWSCS');
// Alice adds her device with same device ID but different keys
final oldResp = FakeMatrixApi.api['POST']['/client/r0/keys/query'](null);
FakeMatrixApi.api['POST']['/client/r0/keys/query'] = (_) {
oldResp['device_keys']['@alice:example.com']['JLAFKJWSCS'] = {
'user_id': '@alice:example.com',
'device_id': 'JLAFKJWSCS',
'algorithms': [
'm.olm.v1.curve25519-aes-sha2',
'm.megolm.v1.aes-sha2'
],
'keys': {
'curve25519:JLAFKJWSCS':
'WbwrNyD7nvtmcLQ0TTuVPFGJq6JznfjrVsjIpmBqvDw',
'ed25519:JLAFKJWSCS': 'vl0d54pTVRcvBgUzoQFa8e6TldHWG9O8bh0iuIvgd/I'
},
'signatures': {
'@alice:example.com': {
'ed25519:JLAFKJWSCS':
's/L86jLa8BTroL8GsBeqO0gRLC3ZrSA7Gch6UoLI2SefC1+1ycmnP9UGbLPh3qBJOmlhczMpBLZwelg87qNNDA'
}
}
};
return oldResp;
};
client.userDeviceKeys['@alice:example.com'].outdated = true;
await client.updateUserDeviceKeys();
expect(
client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'],
null);
});
test('dispose client', () async {
if (!olmEnabled) return;
await client.dispose(closeDatabase: true);
await client.dispose(closeDatabase: false);
});
});
}

View File

@ -26,6 +26,7 @@ import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import '../fake_client.dart';
import '../fake_database.dart';
import '../fake_matrix_api.dart';
class MockSSSS extends SSSS {
@ -84,9 +85,11 @@ void main() {
if (!olmEnabled) return;
client1 = await getClient();
client2 = Client('othertestclient',
httpClient: FakeMatrixApi(),
databaseBuilder: (_) => client1.database);
client2 = Client(
'othertestclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
);
await client2.checkHomeserver('https://fakeserver.notexisting',
checkWellKnown: false);
await client2.init(

View File

@ -119,7 +119,7 @@ void main() {
test('dispose client', () async {
if (!olmEnabled) return;
await client.dispose(closeDatabase: true);
await client.dispose(closeDatabase: false);
});
});
}