lazy-load group session keys

This commit is contained in:
Sorunome 2020-05-17 07:54:34 +00:00 committed by Christian Pauly
parent 98d2f8d6bb
commit 06b601c41b
10 changed files with 105 additions and 72 deletions

View file

@ -1280,7 +1280,6 @@ class Client {
roomAccountData: {},
client: this,
);
newRoom.restoreGroupSessionKeys();
rooms.insert(position, newRoom);
}
// If the membership is "leave" then remove the item and stop here
@ -1414,6 +1413,7 @@ class Client {
.containsKey(toDeviceEvent.content['requesting_device_id'])) {
deviceKeys = userDeviceKeys[toDeviceEvent.sender]
.deviceKeys[toDeviceEvent.content['requesting_device_id']];
await room.loadInboundGroupSessionKey(sessionId);
if (room.inboundGroupSessions.containsKey(sessionId)) {
final roomKeyRequest =
RoomKeyRequest.fromToDeviceEvent(toDeviceEvent, this);
@ -1558,11 +1558,6 @@ class Client {
await f();
}
});
rooms.forEach((Room room) {
if (room.encrypted) {
room.clearOutboundGroupSession();
}
});
} catch (e) {
print('[LibOlm] Unable to update user device keys: ' + e.toString());
}

View file

@ -92,24 +92,27 @@ class Database extends _$Database {
return await dbGetInboundGroupSessionKeys(clientId, roomId).get();
}
Future<DbInboundGroupSession> getDbInboundGroupSession(int clientId, String roomId, String sessionId) async {
final res = await dbGetInboundGroupSessionKey(clientId, roomId, sessionId).get();
if (res.isEmpty) {
return null;
}
return res.first;
}
Future<List<sdk.Room>> getRoomList(sdk.Client client, {bool onlyLeft = false}) async {
final res = await (select(rooms)..where((t) => onlyLeft
? t.membership.equals('leave')
: t.membership.equals('leave').not())).get();
final resStates = await getAllRoomStates(client.id).get();
final resAccountData = await getAllRoomAccountData(client.id).get();
final resOutboundGroupSessions = await getAllOutboundGroupSessions(client.id).get();
final resInboundGroupSessions = await getAllInboundGroupSessions(client.id).get();
final roomList = <sdk.Room>[];
for (final r in res) {
final outboundGroupSession = resOutboundGroupSessions.where((rs) => rs.roomId == r.roomId);
final room = await sdk.Room.getRoomFromTableRow(
r,
client,
states: resStates.where((rs) => rs.roomId == r.roomId),
roomAccountData: resAccountData.where((rs) => rs.roomId == r.roomId),
outboundGroupSession: outboundGroupSession.isEmpty ? false : outboundGroupSession.first,
inboundGroupSessions: resInboundGroupSessions.where((rs) => rs.roomId == r.roomId),
);
roomList.add(room);
}

View file

@ -4831,6 +4831,20 @@ abstract class _$Database extends GeneratedDatabase {
);
}
Selectable<DbInboundGroupSession> dbGetInboundGroupSessionKey(
int client_id, String room_id, String session_id) {
return customSelect(
'SELECT * FROM inbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id',
variables: [
Variable.withInt(client_id),
Variable.withString(room_id),
Variable.withString(session_id)
],
readsFrom: {
inboundGroupSessions
}).map(_rowToDbInboundGroupSession);
}
Selectable<DbInboundGroupSession> dbGetInboundGroupSessionKeys(
int client_id, String room_id) {
return customSelect(

View file

@ -158,6 +158,7 @@ getAllOutboundGroupSessions: SELECT * FROM outbound_group_sessions WHERE client_
dbGetOutboundGroupSession: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
storeOutboundGroupSession: INSERT OR REPLACE INTO outbound_group_sessions (client_id, room_id, pickle, device_ids) VALUES (:client_id, :room_id, :pickle, :device_ids);
removeOutboundGroupSession: DELETE FROM outbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
dbGetInboundGroupSessionKey: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
dbGetInboundGroupSessionKeys: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
getAllInboundGroupSessions: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id;
storeInboundGroupSession: INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes);

View file

@ -414,6 +414,10 @@ class Event {
return await timeline.getEventById(replyEventId);
}
Future<void> loadSession() {
return room.loadInboundGroupSessionKeyForEvent(this);
}
/// Trys to decrypt this event. Returns a m.bad.encrypted event
/// if it fails and does nothing if the event was not encrypted.
Event get decrypted => room.decryptGroupMessage(this);

View file

@ -201,7 +201,7 @@ class Room {
/// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
/// }
Map<String, SessionKey> get inboundGroupSessions => _inboundGroupSessions;
Map<String, SessionKey> _inboundGroupSessions = {};
final _inboundGroupSessions = <String, SessionKey>{};
/// Add a new session key to the [sessionKeys].
void setInboundGroupSession(String sessionId, Map<String, dynamic> content,
@ -228,12 +228,10 @@ class Room {
indexes: {},
key: client.userID,
);
if (_fullyRestored) {
client.database?.storeInboundGroupSession(client.id, id, sessionId,
inboundGroupSession.pickle(client.userID), json.encode(content),
json.encode({}),
);
}
_tryAgainDecryptLastMessage();
onSessionKeyReceived.add(sessionId);
}
@ -1036,51 +1034,6 @@ class Room {
return;
}
Future<void> restoreGroupSessionKeys({
dynamic outboundGroupSession, // DbOutboundGroupSession, optionally as future
dynamic inboundGroupSessions, // DbSessionKey, as iterator and optionally as future
}) async {
// Restore the inbound and outbound session keys
if (client.encryptionEnabled && client.database != null) {
outboundGroupSession ??= client.database.getDbOutboundGroupSession(client.id, id);
inboundGroupSessions ??= client.database.getDbInboundGroupSessions(client.id, id);
if (outboundGroupSession is Future) {
outboundGroupSession = await outboundGroupSession;
}
if (inboundGroupSessions is Future) {
inboundGroupSessions = await inboundGroupSessions;
}
if (outboundGroupSession != false && outboundGroupSession != null) {
try {
_outboundGroupSession = olm.OutboundGroupSession();
_outboundGroupSession.unpickle(
client.userID, outboundGroupSession.pickle);
} catch (e) {
_outboundGroupSession = null;
print('[LibOlm] Unable to unpickle outboundGroupSession: ' +
e.toString());
}
_outboundGroupSessionDevices =
List<String>.from(json.decode(outboundGroupSession.deviceIds));
}
if (inboundGroupSessions?.isNotEmpty ?? false) {
_inboundGroupSessions ??= {};
for (final sessionKey in inboundGroupSessions) {
try {
_inboundGroupSessions[sessionKey.sessionId] = SessionKey.fromDb(sessionKey, client.userID);
} catch (e) {
print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString());
}
}
}
}
_tryAgainDecryptLastMessage();
_fullyRestored = true;
return;
}
bool _fullyRestored = false;
/// Returns a Room from a json String which comes normally from the store. If the
/// state are also given, the method will await them.
static Future<Room> getRoomFromTableRow(
@ -1089,8 +1042,6 @@ class Room {
{
dynamic states, // DbRoomState, as iterator and optionally as future
dynamic roomAccountData, // DbRoomAccountData, as iterator and optionally as future
dynamic outboundGroupSession, // DbOutboundGroupSession, optionally as future
dynamic inboundGroupSessions, // DbSessionKey, as iterator and optionally as future
}) async {
final newRoom = Room(
id: row.roomId,
@ -1137,12 +1088,6 @@ class Room {
}
newRoom.roomAccountData = newRoomAccountData;
// Restore the inbound and outbound session keys
await newRoom.restoreGroupSessionKeys(
outboundGroupSession: outboundGroupSession,
inboundGroupSessions: inboundGroupSessions,
);
return newRoom;
}
@ -1163,6 +1108,7 @@ class Room {
for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted &&
events[i].content['body'] == DecryptError.UNKNOWN_SESSION) {
await events[i].loadSession();
events[i] = events[i].decrypted;
if (events[i].type != EventTypes.Encrypted) {
await client.database.storeEventUpdate(client.id,
@ -1742,6 +1688,30 @@ class Room {
return deviceKeys;
}
bool _restoredOutboundGroupSession = false;
Future<void> restoreOutboundGroupSession() async {
if (_restoredOutboundGroupSession || client.database == null) {
return;
}
final outboundSession = await client.database.getDbOutboundGroupSession(client.id, id);
if (outboundSession != null) {
try {
_outboundGroupSession = olm.OutboundGroupSession();
_outboundGroupSession.unpickle(
client.userID, outboundSession.pickle);
_outboundGroupSessionDevices =
List<String>.from(json.decode(outboundSession.deviceIds));
} catch (e) {
_outboundGroupSession = null;
_outboundGroupSessionDevices = null;
print('[LibOlm] Unable to unpickle outboundGroupSession: ' +
e.toString());
}
}
_restoredOutboundGroupSession = true;
}
/// Encrypts the given json payload and creates a send-ready m.room.encrypted
/// payload. This will create a new outgoingGroupSession if necessary.
Future<Map<String, dynamic>> encryptGroupMessagePayload(
@ -1751,6 +1721,13 @@ class Room {
if (encryptionAlgorithm != 'm.megolm.v1.aes-sha2') {
throw ('Unknown encryption algorithm');
}
if (!_restoredOutboundGroupSession && client.database != null) {
// try to restore an outbound group session from the database
await restoreOutboundGroupSession();
}
// and clear the outbound session, if it needs clearing
await clearOutboundGroupSession();
// create a new one if none exists...
if (_outboundGroupSession == null) {
await createOutboundGroupSession();
}
@ -1772,6 +1749,30 @@ class Room {
return encryptedPayload;
}
Future<void> loadInboundGroupSessionKey(String sessionId) async {
if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) return; // nothing to do
final session = await client.database.getDbInboundGroupSession(client.id, id, sessionId);
if (session == null) return; // no session found
try {
_inboundGroupSessions[sessionId] = SessionKey.fromDb(session, client.userID);
} catch (e) {
print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString());
}
}
Future<void> loadInboundGroupSessionKeyForEvent(Event event) async {
if (client.database == null) return; // nothing to do, no database
if (event.type != EventTypes.Encrypted) return;
if (!client.encryptionEnabled) {
throw (DecryptError.NOT_ENABLED);
}
if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') {
throw (DecryptError.UNKNOWN_ALGORITHM);
}
final String sessionId = event.content['session_id'];
return loadInboundGroupSessionKey(sessionId);
}
/// Decrypts the given [event] with one of the available ingoingGroupSessions.
/// Returns a m.bad.encrypted event if it fails and does nothing if the event
/// was not encrypted.

View file

@ -53,7 +53,7 @@ class EventUpdate {
var decrpytedEvent =
room.decryptGroupMessage(Event.fromJson(content, room, sortOrder));
return EventUpdate(
eventType: eventType,
eventType: decrpytedEvent.typeKey,
roomID: roomID,
type: type,
content: decrpytedEvent.toJson(),

View file

@ -116,6 +116,19 @@ class Timeline {
try {
if (eventUpdate.roomID != room.id) return;
// try to decrypt the event first, if needed
if (eventUpdate.eventType == 'm.room.encrypted' && room.client.database != null) {
try {
await room.loadInboundGroupSessionKey(eventUpdate.content['content']['session_id']);
eventUpdate = eventUpdate.decrypt(room);
if (eventUpdate.eventType != 'm.room.encrypted') {
await room.client.database.storeEventUpdate(room.client.id, eventUpdate);
}
} catch (err) {
print('[WARNING] (_handleEventUpdate) Failed to decrypt event: ${err.toString()}');
}
}
if (eventUpdate.type == 'timeline' || eventUpdate.type == 'history') {
// Redaction events are handled as modification for existing events.
if (eventUpdate.eventType == 'm.room.redaction') {

View file

@ -16,6 +16,7 @@ class RoomKeyRequest extends ToDeviceEvent {
Future<void> forwardKey() async {
var room = this.room;
await room.loadInboundGroupSessionKey(content['body']['session_id']);
final session = room.inboundGroupSessions[content['body']['session_id']];
var forwardedKeys = <dynamic>[client.identityKey];
for (final key in session.forwardingCurve25519KeyChain) {

View file

@ -667,6 +667,7 @@ void main() {
expect(client2.deviceID, client1.deviceID);
expect(client2.deviceName, client1.deviceName);
if (client2.encryptionEnabled) {
await client2.rooms[1].restoreOutboundGroupSession();
expect(client2.pickledOlmAccount, client1.pickledOlmAccount);
expect(json.encode(client2.rooms[1].inboundGroupSessions[sessionKey]),
json.encode(client1.rooms[1].inboundGroupSessions[sessionKey]));