diff --git a/lib/src/client.dart b/lib/src/client.dart index ccea37bb..ed877c76 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -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()); } diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index 710c36e5..864e2960 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -92,24 +92,27 @@ class Database extends _$Database { return await dbGetInboundGroupSessionKeys(clientId, roomId).get(); } + Future 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> 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 = []; 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); } diff --git a/lib/src/database/database.g.dart b/lib/src/database/database.g.dart index b87edcf8..deb23869 100644 --- a/lib/src/database/database.g.dart +++ b/lib/src/database/database.g.dart @@ -4831,6 +4831,20 @@ abstract class _$Database extends GeneratedDatabase { ); } + Selectable 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 dbGetInboundGroupSessionKeys( int client_id, String room_id) { return customSelect( diff --git a/lib/src/database/database.moor b/lib/src/database/database.moor index ecd185ca..5b430563 100644 --- a/lib/src/database/database.moor +++ b/lib/src/database/database.moor @@ -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); diff --git a/lib/src/event.dart b/lib/src/event.dart index 16708a52..30847e84 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -414,6 +414,10 @@ class Event { return await timeline.getEventById(replyEventId); } + Future 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); diff --git a/lib/src/room.dart b/lib/src/room.dart index 2d76ee2f..7df43523 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -201,7 +201,7 @@ class Room { /// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..." /// } Map get inboundGroupSessions => _inboundGroupSessions; - Map _inboundGroupSessions = {}; + final _inboundGroupSessions = {}; /// Add a new session key to the [sessionKeys]. void setInboundGroupSession(String sessionId, Map 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({}), - ); - } + 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 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.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 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, @@ -1741,6 +1687,30 @@ class Room { } return deviceKeys; } + + bool _restoredOutboundGroupSession = false; + + Future 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.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. @@ -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 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 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. diff --git a/lib/src/sync/event_update.dart b/lib/src/sync/event_update.dart index 841b290b..c9075944 100644 --- a/lib/src/sync/event_update.dart +++ b/lib/src/sync/event_update.dart @@ -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(), diff --git a/lib/src/timeline.dart b/lib/src/timeline.dart index 9b765f83..2762a8b2 100644 --- a/lib/src/timeline.dart +++ b/lib/src/timeline.dart @@ -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') { diff --git a/lib/src/utils/room_key_request.dart b/lib/src/utils/room_key_request.dart index 501691d5..d839daa8 100644 --- a/lib/src/utils/room_key_request.dart +++ b/lib/src/utils/room_key_request.dart @@ -16,6 +16,7 @@ class RoomKeyRequest extends ToDeviceEvent { Future forwardKey() async { var room = this.room; + await room.loadInboundGroupSessionKey(content['body']['session_id']); final session = room.inboundGroupSessions[content['body']['session_id']]; var forwardedKeys = [client.identityKey]; for (final key in session.forwardingCurve25519KeyChain) { diff --git a/test/client_test.dart b/test/client_test.dart index c59ac4af..9b6303e1 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -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]));