diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index ade2762d..65451a0d 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -292,6 +292,43 @@ class KeyManager { return roomInboundGroupSessions[sessionId] = dbSess; } + void _sendEncryptionInfoEvent({ + required String roomId, + required List userIds, + List? deviceIds, + }) async { + await client.database?.transaction(() async { + await client.handleSync( + SyncUpdate( + nextBatch: '', + rooms: RoomsUpdate( + join: { + roomId: JoinedRoomUpdate( + timeline: TimelineUpdate( + events: [ + MatrixEvent( + eventId: + 'fake_event_${client.generateUniqueTransactionId()}', + content: { + 'body': + '${userIds.join(', ')} can now read along${deviceIds != null ? ' on ${deviceIds.length} new device(s)' : ''}', + if (deviceIds != null) 'devices': deviceIds, + 'users': userIds, + }, + type: EventTypes.encryptionInfo, + senderId: client.userID!, + originServerTs: DateTime.now(), + ), + ], + ), + ), + }, + ), + ), + ); + }); + } + Map> _getDeviceKeyIdMap( List deviceKeys, ) { @@ -327,21 +364,6 @@ class KeyManager { return true; } - if (!wipe) { - // first check if it needs to be rotated - final encryptionContent = - room.getState(EventTypes.Encryption)?.parsedRoomEncryptionContent; - final maxMessages = encryptionContent?.rotationPeriodMsgs ?? 100; - final maxAge = encryptionContent?.rotationPeriodMs ?? - 604800000; // default of one week - if ((sess.sentMessages ?? maxMessages) >= maxMessages || - sess.creationTime - .add(Duration(milliseconds: maxAge)) - .isBefore(DateTime.now())) { - wipe = true; - } - } - final inboundSess = await loadInboundGroupSession( room.id, sess.outboundGroupSession!.session_id(), @@ -352,81 +374,100 @@ class KeyManager { wipe = true; } - if (!wipe) { - // next check if the devices in the room changed - final devicesToReceive = []; - final newDeviceKeys = await room.getUserDeviceKeys(); - final newDeviceKeyIds = _getDeviceKeyIdMap(newDeviceKeys); - // first check for user differences - final oldUserIds = sess.devices.keys.toSet(); - final newUserIds = newDeviceKeyIds.keys.toSet(); - if (oldUserIds.difference(newUserIds).isNotEmpty) { - // a user left the room, we must wipe the session - wipe = true; - } else { - final newUsers = newUserIds.difference(oldUserIds); - if (newUsers.isNotEmpty) { - // new user! Gotta send the megolm session to them - devicesToReceive - .addAll(newDeviceKeys.where((d) => newUsers.contains(d.userId))); + // next check if the devices in the room changed + final devicesToReceive = []; + final newDeviceKeys = await room.getUserDeviceKeys(); + final newDeviceKeyIds = _getDeviceKeyIdMap(newDeviceKeys); + // first check for user differences + final oldUserIds = sess.devices.keys.toSet(); + final newUserIds = newDeviceKeyIds.keys.toSet(); + if (oldUserIds.difference(newUserIds).isNotEmpty) { + // a user left the room, we must wipe the session + wipe = true; + } else { + final newUsers = newUserIds.difference(oldUserIds); + if (newUsers.isNotEmpty) { + // new user! Gotta send the megolm session to them + devicesToReceive + .addAll(newDeviceKeys.where((d) => newUsers.contains(d.userId))); + _sendEncryptionInfoEvent(roomId: roomId, userIds: newUsers.toList()); + } + // okay, now we must test all the individual user devices, if anything new got blocked + // or if we need to send to any new devices. + // for this it is enough if we iterate over the old user Ids, as the new ones already have the needed keys in the list. + // we also know that all the old user IDs appear in the old one, else we have already wiped the session + for (final userId in oldUserIds) { + final oldBlockedDevices = sess.devices.containsKey(userId) + ? sess.devices[userId]!.entries + .where((e) => e.value) + .map((e) => e.key) + .toSet() + : {}; + final newBlockedDevices = newDeviceKeyIds.containsKey(userId) + ? newDeviceKeyIds[userId]! + .entries + .where((e) => e.value) + .map((e) => e.key) + .toSet() + : {}; + // we don't really care about old devices that got dropped (deleted), we only care if new ones got added and if new ones got blocked + // check if new devices got blocked + if (newBlockedDevices.difference(oldBlockedDevices).isNotEmpty) { + wipe = true; + } + // and now add all the new devices! + final oldDeviceIds = sess.devices.containsKey(userId) + ? sess.devices[userId]!.entries + .where((e) => !e.value) + .map((e) => e.key) + .toSet() + : {}; + final newDeviceIds = newDeviceKeyIds.containsKey(userId) + ? newDeviceKeyIds[userId]! + .entries + .where((e) => !e.value) + .map((e) => e.key) + .toSet() + : {}; + + // check if a device got removed + if (oldDeviceIds.difference(newDeviceIds).isNotEmpty) { + wipe = true; } - // okay, now we must test all the individual user devices, if anything new got blocked - // or if we need to send to any new devices. - // for this it is enough if we iterate over the old user Ids, as the new ones already have the needed keys in the list. - // we also know that all the old user IDs appear in the old one, else we have already wiped the session - for (final userId in oldUserIds) { - final oldBlockedDevices = sess.devices.containsKey(userId) - ? sess.devices[userId]!.entries - .where((e) => e.value) - .map((e) => e.key) - .toSet() - : {}; - final newBlockedDevices = newDeviceKeyIds.containsKey(userId) - ? newDeviceKeyIds[userId]! - .entries - .where((e) => e.value) - .map((e) => e.key) - .toSet() - : {}; - // we don't really care about old devices that got dropped (deleted), we only care if new ones got added and if new ones got blocked - // check if new devices got blocked - if (newBlockedDevices.difference(oldBlockedDevices).isNotEmpty) { - wipe = true; - break; - } - // and now add all the new devices! - final oldDeviceIds = sess.devices.containsKey(userId) - ? sess.devices[userId]!.entries - .where((e) => !e.value) - .map((e) => e.key) - .toSet() - : {}; - final newDeviceIds = newDeviceKeyIds.containsKey(userId) - ? newDeviceKeyIds[userId]! - .entries - .where((e) => !e.value) - .map((e) => e.key) - .toSet() - : {}; - - // check if a device got removed - if (oldDeviceIds.difference(newDeviceIds).isNotEmpty) { - wipe = true; - break; - } - // check if any new devices need keys - final newDevices = newDeviceIds.difference(oldDeviceIds); - if (newDeviceIds.isNotEmpty) { - devicesToReceive.addAll( - newDeviceKeys.where( - (d) => d.userId == userId && newDevices.contains(d.deviceId), - ), + // check if any new devices need keys + final newDevices = newDeviceIds.difference(oldDeviceIds); + if (newDeviceIds.isNotEmpty) { + devicesToReceive.addAll( + newDeviceKeys.where( + (d) => d.userId == userId && newDevices.contains(d.deviceId), + ), + ); + if (userId != client.userID && newDevices.isNotEmpty) { + _sendEncryptionInfoEvent( + roomId: roomId, + userIds: [userId], + deviceIds: newDevices.toList(), ); } } } + if (!wipe) { + // first check if it needs to be rotated + final encryptionContent = + room.getState(EventTypes.Encryption)?.parsedRoomEncryptionContent; + final maxMessages = encryptionContent?.rotationPeriodMsgs ?? 100; + final maxAge = encryptionContent?.rotationPeriodMs ?? + 604800000; // default of one week + if ((sess.sentMessages ?? maxMessages) >= maxMessages || + sess.creationTime + .add(Duration(milliseconds: maxAge)) + .isBefore(DateTime.now())) { + wipe = true; + } + } + if (!wipe) { if (!use) { return false; diff --git a/lib/matrix_api_lite/model/event_types.dart b/lib/matrix_api_lite/model/event_types.dart index 0e39bb5d..a296cd67 100644 --- a/lib/matrix_api_lite/model/event_types.dart +++ b/lib/matrix_api_lite/model/event_types.dart @@ -114,4 +114,7 @@ abstract class EventTypes { static const String GroupCallMemberReplaces = '$GroupCallMember.replaces'; static const String GroupCallMemberAssertedIdentity = '$GroupCallMember.asserted_identity'; + + // Internal + static const String encryptionInfo = 'sdk.dart.matrix.new_megolm_session'; } diff --git a/lib/src/utils/event_localizations.dart b/lib/src/utils/event_localizations.dart index 1ae3b98a..5909699e 100644 --- a/lib/src/utils/event_localizations.dart +++ b/lib/src/utils/event_localizations.dart @@ -119,6 +119,8 @@ abstract class EventLocalizations { EventTypes.Sticker: (event, i18n, body) => i18n.sentASticker( event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n), ), + 'sdk.dart.matrix.new_megolm_session': (event, i18n, body) => + i18n.userCanNowReadAlong(event), EventTypes.Redaction: (event, i18n, body) => i18n.redactedAnEvent(event), EventTypes.RoomAliases: (event, i18n, body) => i18n.changedTheRoomAliases( event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n), diff --git a/lib/src/utils/matrix_default_localizations.dart b/lib/src/utils/matrix_default_localizations.dart index ecdb7585..1a00c60b 100644 --- a/lib/src/utils/matrix_default_localizations.dart +++ b/lib/src/utils/matrix_default_localizations.dart @@ -310,4 +310,11 @@ class MatrixDefaultLocalizations extends MatrixLocalizations { @override String startedKeyVerification(String senderName) => '$senderName started key verification'; + + @override + String userCanNowReadAlong(Event event) { + final users = event.content.tryGetList('users') ?? [unknownUser]; + final deviceCount = event.content.tryGetList('devices')?.length; + return '${users.join(', ')} can now read along${deviceCount == null ? '' : ' on $deviceCount new device(s)'}'; + } } diff --git a/lib/src/utils/matrix_localizations.dart b/lib/src/utils/matrix_localizations.dart index b36e2ad3..e870f4ce 100644 --- a/lib/src/utils/matrix_localizations.dart +++ b/lib/src/utils/matrix_localizations.dart @@ -86,6 +86,8 @@ abstract class MatrixLocalizations { String redactedAnEvent(Event redactedEvent); + String userCanNowReadAlong(Event event); + String changedTheRoomAliases(String senderName); String changedTheRoomInvitationLink(String senderName);