Skip to content

Commit

Permalink
feat: Send state event to timeline when sharing megolm session
Browse files Browse the repository at this point in the history
  • Loading branch information
krille-chan committed Feb 24, 2025
1 parent 352b3fa commit e57a5da
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 83 deletions.
207 changes: 124 additions & 83 deletions lib/encryption/key_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,43 @@ class KeyManager {
return roomInboundGroupSessions[sessionId] = dbSess;
}

void _sendEncryptionInfoEvent({
required String roomId,
required List<String> userIds,
List<String>? 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<String, Map<String, bool>> _getDeviceKeyIdMap(
List<DeviceKeys> deviceKeys,
) {
Expand Down Expand Up @@ -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(),
Expand All @@ -352,81 +374,100 @@ class KeyManager {
wipe = true;
}

if (!wipe) {
// next check if the devices in the room changed
final devicesToReceive = <DeviceKeys>[];
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 = <DeviceKeys>[];
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()
: <String>{};
final newBlockedDevices = newDeviceKeyIds.containsKey(userId)
? newDeviceKeyIds[userId]!
.entries
.where((e) => e.value)
.map((e) => e.key)
.toSet()
: <String>{};
// 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()
: <String>{};
final newDeviceIds = newDeviceKeyIds.containsKey(userId)
? newDeviceKeyIds[userId]!
.entries
.where((e) => !e.value)
.map((e) => e.key)
.toSet()
: <String>{};

// 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()
: <String>{};
final newBlockedDevices = newDeviceKeyIds.containsKey(userId)
? newDeviceKeyIds[userId]!
.entries
.where((e) => e.value)
.map((e) => e.key)
.toSet()
: <String>{};
// 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()
: <String>{};
final newDeviceIds = newDeviceKeyIds.containsKey(userId)
? newDeviceKeyIds[userId]!
.entries
.where((e) => !e.value)
.map((e) => e.key)
.toSet()
: <String>{};

// 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;
Expand Down
3 changes: 3 additions & 0 deletions lib/matrix_api_lite/model/event_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
2 changes: 2 additions & 0 deletions lib/src/utils/event_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
7 changes: 7 additions & 0 deletions lib/src/utils/matrix_default_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>('users') ?? [unknownUser];
final deviceCount = event.content.tryGetList<String>('devices')?.length;
return '${users.join(', ')} can now read along${deviceCount == null ? '' : ' on $deviceCount new device(s)'}';
}
}
2 changes: 2 additions & 0 deletions lib/src/utils/matrix_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ abstract class MatrixLocalizations {

String redactedAnEvent(Event redactedEvent);

String userCanNowReadAlong(Event event);

String changedTheRoomAliases(String senderName);

String changedTheRoomInvitationLink(String senderName);
Expand Down

0 comments on commit e57a5da

Please sign in to comment.