Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Send state event to timeline on creating new megolm session #2012

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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