Merge branch 'soru/fix-lazy-session-keys' into 'master'

lazy-load group session keys

See merge request famedly/famedlysdk!293
This commit is contained in:
Christian Pauly 2020-05-17 07:54:34 +00:00
commit 81b9d79518
10 changed files with 105 additions and 72 deletions

View file

@ -1315,7 +1315,6 @@ class Client {
roomAccountData: {}, roomAccountData: {},
client: this, client: this,
); );
newRoom.restoreGroupSessionKeys();
rooms.insert(position, newRoom); rooms.insert(position, newRoom);
} }
// If the membership is "leave" then remove the item and stop here // If the membership is "leave" then remove the item and stop here
@ -1450,6 +1449,7 @@ class Client {
.containsKey(toDeviceEvent.content['requesting_device_id'])) { .containsKey(toDeviceEvent.content['requesting_device_id'])) {
deviceKeys = userDeviceKeys[toDeviceEvent.sender] deviceKeys = userDeviceKeys[toDeviceEvent.sender]
.deviceKeys[toDeviceEvent.content['requesting_device_id']]; .deviceKeys[toDeviceEvent.content['requesting_device_id']];
await room.loadInboundGroupSessionKey(sessionId);
if (room.inboundGroupSessions.containsKey(sessionId)) { if (room.inboundGroupSessions.containsKey(sessionId)) {
final roomKeyRequest = final roomKeyRequest =
RoomKeyRequest.fromToDeviceEvent(toDeviceEvent, this); RoomKeyRequest.fromToDeviceEvent(toDeviceEvent, this);
@ -1600,11 +1600,6 @@ class Client {
await f(); await f();
} }
}); });
rooms.forEach((Room room) {
if (room.encrypted) {
room.clearOutboundGroupSession();
}
});
} catch (e) { } catch (e) {
print('[LibOlm] Unable to update user device keys: ' + e.toString()); 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(); 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 { Future<List<sdk.Room>> getRoomList(sdk.Client client, {bool onlyLeft = false}) async {
final res = await (select(rooms)..where((t) => onlyLeft final res = await (select(rooms)..where((t) => onlyLeft
? t.membership.equals('leave') ? t.membership.equals('leave')
: t.membership.equals('leave').not())).get(); : t.membership.equals('leave').not())).get();
final resStates = await getAllRoomStates(client.id).get(); final resStates = await getAllRoomStates(client.id).get();
final resAccountData = await getAllRoomAccountData(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>[]; final roomList = <sdk.Room>[];
for (final r in res) { for (final r in res) {
final outboundGroupSession = resOutboundGroupSessions.where((rs) => rs.roomId == r.roomId);
final room = await sdk.Room.getRoomFromTableRow( final room = await sdk.Room.getRoomFromTableRow(
r, r,
client, client,
states: resStates.where((rs) => rs.roomId == r.roomId), states: resStates.where((rs) => rs.roomId == r.roomId),
roomAccountData: resAccountData.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); 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( Selectable<DbInboundGroupSession> dbGetInboundGroupSessionKeys(
int client_id, String room_id) { int client_id, String room_id) {
return customSelect( 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; 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); 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; 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; 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; 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); 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

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

View file

@ -201,7 +201,7 @@ class Room {
/// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..." /// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
/// } /// }
Map<String, SessionKey> get inboundGroupSessions => _inboundGroupSessions; Map<String, SessionKey> get inboundGroupSessions => _inboundGroupSessions;
Map<String, SessionKey> _inboundGroupSessions = {}; final _inboundGroupSessions = <String, SessionKey>{};
/// Add a new session key to the [sessionKeys]. /// Add a new session key to the [sessionKeys].
void setInboundGroupSession(String sessionId, Map<String, dynamic> content, void setInboundGroupSession(String sessionId, Map<String, dynamic> content,
@ -228,12 +228,10 @@ class Room {
indexes: {}, indexes: {},
key: client.userID, key: client.userID,
); );
if (_fullyRestored) { client.database?.storeInboundGroupSession(client.id, id, sessionId,
client.database?.storeInboundGroupSession(client.id, id, sessionId, inboundGroupSession.pickle(client.userID), json.encode(content),
inboundGroupSession.pickle(client.userID), json.encode(content), json.encode({}),
json.encode({}), );
);
}
_tryAgainDecryptLastMessage(); _tryAgainDecryptLastMessage();
onSessionKeyReceived.add(sessionId); onSessionKeyReceived.add(sessionId);
} }
@ -1040,51 +1038,6 @@ class Room {
return; 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 /// Returns a Room from a json String which comes normally from the store. If the
/// state are also given, the method will await them. /// state are also given, the method will await them.
static Future<Room> getRoomFromTableRow( static Future<Room> getRoomFromTableRow(
@ -1093,8 +1046,6 @@ class Room {
{ {
dynamic states, // DbRoomState, as iterator and optionally as future dynamic states, // DbRoomState, as iterator and optionally as future
dynamic roomAccountData, // DbRoomAccountData, 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 { }) async {
final newRoom = Room( final newRoom = Room(
id: row.roomId, id: row.roomId,
@ -1141,12 +1092,6 @@ class Room {
} }
newRoom.roomAccountData = newRoomAccountData; newRoom.roomAccountData = newRoomAccountData;
// Restore the inbound and outbound session keys
await newRoom.restoreGroupSessionKeys(
outboundGroupSession: outboundGroupSession,
inboundGroupSessions: inboundGroupSessions,
);
return newRoom; return newRoom;
} }
@ -1167,6 +1112,7 @@ class Room {
for (var i = 0; i < events.length; i++) { for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted && if (events[i].type == EventTypes.Encrypted &&
events[i].content['body'] == DecryptError.UNKNOWN_SESSION) { events[i].content['body'] == DecryptError.UNKNOWN_SESSION) {
await events[i].loadSession();
events[i] = events[i].decrypted; events[i] = events[i].decrypted;
if (events[i].type != EventTypes.Encrypted) { if (events[i].type != EventTypes.Encrypted) {
await client.database.storeEventUpdate(client.id, await client.database.storeEventUpdate(client.id,
@ -1745,6 +1691,30 @@ class Room {
} }
return deviceKeys; 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 /// Encrypts the given json payload and creates a send-ready m.room.encrypted
/// payload. This will create a new outgoingGroupSession if necessary. /// payload. This will create a new outgoingGroupSession if necessary.
@ -1755,6 +1725,13 @@ class Room {
if (encryptionAlgorithm != 'm.megolm.v1.aes-sha2') { if (encryptionAlgorithm != 'm.megolm.v1.aes-sha2') {
throw ('Unknown encryption algorithm'); 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) { if (_outboundGroupSession == null) {
await createOutboundGroupSession(); await createOutboundGroupSession();
} }
@ -1776,6 +1753,30 @@ class Room {
return encryptedPayload; 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. /// 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 /// Returns a m.bad.encrypted event if it fails and does nothing if the event
/// was not encrypted. /// was not encrypted.

View file

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

View file

@ -116,6 +116,19 @@ class Timeline {
try { try {
if (eventUpdate.roomID != room.id) return; 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') { if (eventUpdate.type == 'timeline' || eventUpdate.type == 'history') {
// Redaction events are handled as modification for existing events. // Redaction events are handled as modification for existing events.
if (eventUpdate.eventType == 'm.room.redaction') { if (eventUpdate.eventType == 'm.room.redaction') {

View file

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

View file

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