diff --git a/lib/src/client.dart b/lib/src/client.dart index aec5293..ee4b966 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1347,7 +1347,8 @@ class Client { // man-in-the-middle attacks! final room = getRoomById(roomID); if (room == null || - (event['type'] == 'm.room.encryption' && room.encrypted)) { + (event['type'] == 'm.room.encryption' && room.encrypted && + event['content']['algorithm'] != room.getState('m.room.encryption')?.content['algorithm'])) { return; } diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index 864e296..e4af62a 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -13,7 +13,7 @@ class Database extends _$Database { Database(QueryExecutor e) : super(e); @override - int get schemaVersion => 2; + int get schemaVersion => 3; int get maxFileSize => 1 * 1024 * 1024; @@ -36,6 +36,11 @@ class Database extends _$Database { await m.createIndex(accountDataIndex); await m.createIndex(roomAccountDataIndex); await m.createIndex(presencesIndex); + from++; + } + if (from == 2) { + await m.deleteTable('outbound_group_sessions'); + await m.createTable(outboundGroupSessions); } }, ); diff --git a/lib/src/database/database.g.dart b/lib/src/database/database.g.dart index deb2386..c5c20a2 100644 --- a/lib/src/database/database.g.dart +++ b/lib/src/database/database.g.dart @@ -1313,17 +1313,22 @@ class DbOutboundGroupSession extends DataClass final String roomId; final String pickle; final String deviceIds; + final DateTime creationTime; + final int sentMessages; DbOutboundGroupSession( {@required this.clientId, @required this.roomId, @required this.pickle, - @required this.deviceIds}); + @required this.deviceIds, + @required this.creationTime, + @required this.sentMessages}); factory DbOutboundGroupSession.fromData( Map data, GeneratedDatabase db, {String prefix}) { final effectivePrefix = prefix ?? ''; final intType = db.typeSystem.forDartType(); final stringType = db.typeSystem.forDartType(); + final dateTimeType = db.typeSystem.forDartType(); return DbOutboundGroupSession( clientId: intType.mapFromDatabaseResponse(data['${effectivePrefix}client_id']), @@ -1333,6 +1338,10 @@ class DbOutboundGroupSession extends DataClass stringType.mapFromDatabaseResponse(data['${effectivePrefix}pickle']), deviceIds: stringType .mapFromDatabaseResponse(data['${effectivePrefix}device_ids']), + creationTime: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}creation_time']), + sentMessages: intType + .mapFromDatabaseResponse(data['${effectivePrefix}sent_messages']), ); } @override @@ -1350,6 +1359,12 @@ class DbOutboundGroupSession extends DataClass if (!nullToAbsent || deviceIds != null) { map['device_ids'] = Variable(deviceIds); } + if (!nullToAbsent || creationTime != null) { + map['creation_time'] = Variable(creationTime); + } + if (!nullToAbsent || sentMessages != null) { + map['sent_messages'] = Variable(sentMessages); + } return map; } @@ -1361,6 +1376,8 @@ class DbOutboundGroupSession extends DataClass roomId: serializer.fromJson(json['room_id']), pickle: serializer.fromJson(json['pickle']), deviceIds: serializer.fromJson(json['device_ids']), + creationTime: serializer.fromJson(json['creation_time']), + sentMessages: serializer.fromJson(json['sent_messages']), ); } @override @@ -1371,16 +1388,25 @@ class DbOutboundGroupSession extends DataClass 'room_id': serializer.toJson(roomId), 'pickle': serializer.toJson(pickle), 'device_ids': serializer.toJson(deviceIds), + 'creation_time': serializer.toJson(creationTime), + 'sent_messages': serializer.toJson(sentMessages), }; } DbOutboundGroupSession copyWith( - {int clientId, String roomId, String pickle, String deviceIds}) => + {int clientId, + String roomId, + String pickle, + String deviceIds, + DateTime creationTime, + int sentMessages}) => DbOutboundGroupSession( clientId: clientId ?? this.clientId, roomId: roomId ?? this.roomId, pickle: pickle ?? this.pickle, deviceIds: deviceIds ?? this.deviceIds, + creationTime: creationTime ?? this.creationTime, + sentMessages: sentMessages ?? this.sentMessages, ); @override String toString() { @@ -1388,14 +1414,22 @@ class DbOutboundGroupSession extends DataClass ..write('clientId: $clientId, ') ..write('roomId: $roomId, ') ..write('pickle: $pickle, ') - ..write('deviceIds: $deviceIds') + ..write('deviceIds: $deviceIds, ') + ..write('creationTime: $creationTime, ') + ..write('sentMessages: $sentMessages') ..write(')')) .toString(); } @override - int get hashCode => $mrjf($mrjc(clientId.hashCode, - $mrjc(roomId.hashCode, $mrjc(pickle.hashCode, deviceIds.hashCode)))); + int get hashCode => $mrjf($mrjc( + clientId.hashCode, + $mrjc( + roomId.hashCode, + $mrjc( + pickle.hashCode, + $mrjc(deviceIds.hashCode, + $mrjc(creationTime.hashCode, sentMessages.hashCode)))))); @override bool operator ==(dynamic other) => identical(this, other) || @@ -1403,7 +1437,9 @@ class DbOutboundGroupSession extends DataClass other.clientId == this.clientId && other.roomId == this.roomId && other.pickle == this.pickle && - other.deviceIds == this.deviceIds); + other.deviceIds == this.deviceIds && + other.creationTime == this.creationTime && + other.sentMessages == this.sentMessages); } class OutboundGroupSessionsCompanion @@ -1412,32 +1448,43 @@ class OutboundGroupSessionsCompanion final Value roomId; final Value pickle; final Value deviceIds; + final Value creationTime; + final Value sentMessages; const OutboundGroupSessionsCompanion({ this.clientId = const Value.absent(), this.roomId = const Value.absent(), this.pickle = const Value.absent(), this.deviceIds = const Value.absent(), + this.creationTime = const Value.absent(), + this.sentMessages = const Value.absent(), }); OutboundGroupSessionsCompanion.insert({ @required int clientId, @required String roomId, @required String pickle, @required String deviceIds, + @required DateTime creationTime, + this.sentMessages = const Value.absent(), }) : clientId = Value(clientId), roomId = Value(roomId), pickle = Value(pickle), - deviceIds = Value(deviceIds); + deviceIds = Value(deviceIds), + creationTime = Value(creationTime); static Insertable custom({ Expression clientId, Expression roomId, Expression pickle, Expression deviceIds, + Expression creationTime, + Expression sentMessages, }) { return RawValuesInsertable({ if (clientId != null) 'client_id': clientId, if (roomId != null) 'room_id': roomId, if (pickle != null) 'pickle': pickle, if (deviceIds != null) 'device_ids': deviceIds, + if (creationTime != null) 'creation_time': creationTime, + if (sentMessages != null) 'sent_messages': sentMessages, }); } @@ -1445,12 +1492,16 @@ class OutboundGroupSessionsCompanion {Value clientId, Value roomId, Value pickle, - Value deviceIds}) { + Value deviceIds, + Value creationTime, + Value sentMessages}) { return OutboundGroupSessionsCompanion( clientId: clientId ?? this.clientId, roomId: roomId ?? this.roomId, pickle: pickle ?? this.pickle, deviceIds: deviceIds ?? this.deviceIds, + creationTime: creationTime ?? this.creationTime, + sentMessages: sentMessages ?? this.sentMessages, ); } @@ -1469,6 +1520,12 @@ class OutboundGroupSessionsCompanion if (deviceIds.present) { map['device_ids'] = Variable(deviceIds.value); } + if (creationTime.present) { + map['creation_time'] = Variable(creationTime.value); + } + if (sentMessages.present) { + map['sent_messages'] = Variable(sentMessages.value); + } return map; } } @@ -1510,8 +1567,30 @@ class OutboundGroupSessions extends Table $customConstraints: 'NOT NULL'); } + final VerificationMeta _creationTimeMeta = + const VerificationMeta('creationTime'); + GeneratedDateTimeColumn _creationTime; + GeneratedDateTimeColumn get creationTime => + _creationTime ??= _constructCreationTime(); + GeneratedDateTimeColumn _constructCreationTime() { + return GeneratedDateTimeColumn('creation_time', $tableName, false, + $customConstraints: 'NOT NULL'); + } + + final VerificationMeta _sentMessagesMeta = + const VerificationMeta('sentMessages'); + GeneratedIntColumn _sentMessages; + GeneratedIntColumn get sentMessages => + _sentMessages ??= _constructSentMessages(); + GeneratedIntColumn _constructSentMessages() { + return GeneratedIntColumn('sent_messages', $tableName, false, + $customConstraints: 'NOT NULL DEFAULT \'0\'', + defaultValue: const CustomExpression('\'0\'')); + } + @override - List get $columns => [clientId, roomId, pickle, deviceIds]; + List get $columns => + [clientId, roomId, pickle, deviceIds, creationTime, sentMessages]; @override OutboundGroupSessions get asDslTable => this; @override @@ -1548,6 +1627,20 @@ class OutboundGroupSessions extends Table } else if (isInserting) { context.missing(_deviceIdsMeta); } + if (data.containsKey('creation_time')) { + context.handle( + _creationTimeMeta, + creationTime.isAcceptableOrUnknown( + data['creation_time'], _creationTimeMeta)); + } else if (isInserting) { + context.missing(_creationTimeMeta); + } + if (data.containsKey('sent_messages')) { + context.handle( + _sentMessagesMeta, + sentMessages.isAcceptableOrUnknown( + data['sent_messages'], _sentMessagesMeta)); + } return context; } @@ -4778,6 +4871,8 @@ abstract class _$Database extends GeneratedDatabase { roomId: row.readString('room_id'), pickle: row.readString('pickle'), deviceIds: row.readString('device_ids'), + creationTime: row.readDateTime('creation_time'), + sentMessages: row.readInt('sent_messages'), ); } @@ -4798,14 +4893,21 @@ abstract class _$Database extends GeneratedDatabase { } Future storeOutboundGroupSession( - int client_id, String room_id, String pickle, String device_ids) { + int client_id, + String room_id, + String pickle, + String device_ids, + DateTime creation_time, + int sent_messages) { return customInsert( - 'INSERT OR REPLACE INTO outbound_group_sessions (client_id, room_id, pickle, device_ids) VALUES (:client_id, :room_id, :pickle, :device_ids)', + 'INSERT OR REPLACE INTO outbound_group_sessions (client_id, room_id, pickle, device_ids, creation_time, sent_messages) VALUES (:client_id, :room_id, :pickle, :device_ids, :creation_time, :sent_messages)', variables: [ Variable.withInt(client_id), Variable.withString(room_id), Variable.withString(pickle), - Variable.withString(device_ids) + Variable.withString(device_ids), + Variable.withDateTime(creation_time), + Variable.withInt(sent_messages) ], updates: {outboundGroupSessions}, ); diff --git a/lib/src/database/database.moor b/lib/src/database/database.moor index 5b43056..97cf8e9 100644 --- a/lib/src/database/database.moor +++ b/lib/src/database/database.moor @@ -46,6 +46,8 @@ CREATE TABLE outbound_group_sessions ( room_id TEXT NOT NULL, pickle TEXT NOT NULL, device_ids TEXT NOT NULL, + creation_time DATETIME NOT NULL, + sent_messages INTEGER NOT NULL DEFAULT '0', UNIQUE(client_id, room_id) ) AS DbOutboundGroupSession; CREATE INDEX outbound_group_sessions_index ON outbound_group_sessions(client_id); @@ -156,7 +158,7 @@ getAllOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id; storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle); getAllOutboundGroupSessions: SELECT * FROM outbound_group_sessions WHERE client_id = :client_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, creation_time, sent_messages) VALUES (:client_id, :room_id, :pickle, :device_ids, :creation_time, :sent_messages); 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; diff --git a/lib/src/room.dart b/lib/src/room.dart index 52cb3d7..d1b8154 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -90,6 +90,8 @@ class Room { olm.OutboundGroupSession _outboundGroupSession; List _outboundGroupSessionDevices; + DateTime _outboundGroupSessionCreationTime; + int _outboundGroupSessionSentMessages; double _newestSortOrder; double _oldestSortOrder; @@ -147,6 +149,8 @@ class Room { await client.sendToDevice(deviceKeys, 'm.room_key', rawSession); _outboundGroupSession = outboundGroupSession; _outboundGroupSessionDevices = outboundGroupSessionDevices; + _outboundGroupSessionCreationTime = DateTime.now(); + _outboundGroupSessionSentMessages = 0; await _storeOutboundGroupSession(); } catch (e) { print( @@ -161,7 +165,8 @@ class Room { if (_outboundGroupSession == null) return; await client.database?.storeOutboundGroupSession( client.id, id, _outboundGroupSession.pickle(client.userID), - json.encode(_outboundGroupSessionDevices), + json.encode(_outboundGroupSessionDevices), _outboundGroupSessionCreationTime, + _outboundGroupSessionSentMessages ); return; } @@ -171,14 +176,30 @@ class Room { /// it wasn't necessary. Future clearOutboundGroupSession({bool wipe = false}) async { if (!wipe && _outboundGroupSessionDevices != null) { + // first check if the devices in the room changed var deviceKeys = await getUserDeviceKeys(); var outboundGroupSessionDevices = []; for (var keys in deviceKeys) { if (!keys.blocked) outboundGroupSessionDevices.add(keys.deviceId); } outboundGroupSessionDevices.sort(); - if (outboundGroupSessionDevices.toString() == + if (outboundGroupSessionDevices.toString() != _outboundGroupSessionDevices.toString()) { + wipe = true; + } + // next check if it needs to be rotated + final encryptionContent = getState('m.room.encryption')?.content; + final maxMessages = encryptionContent != null && encryptionContent['rotation_period_msgs'] is int + ? encryptionContent['rotation_period_msgs'] + : 100; + final maxAge = encryptionContent != null && encryptionContent['rotation_period_ms'] is int + ? encryptionContent['rotation_period_ms'] + : 604800000; // default of one week + if (_outboundGroupSessionSentMessages >= maxMessages || + _outboundGroupSessionCreationTime.add(Duration(milliseconds: maxAge)).isBefore(DateTime.now())) { + wipe = true; + } + if (!wipe) { return false; } } @@ -1705,6 +1726,8 @@ class Room { client.userID, outboundSession.pickle); _outboundGroupSessionDevices = List.from(json.decode(outboundSession.deviceIds)); + _outboundGroupSessionCreationTime = outboundSession.creationTime; + _outboundGroupSessionSentMessages = outboundSession.sentMessages; } catch (e) { _outboundGroupSession = null; _outboundGroupSessionDevices = null; @@ -1748,6 +1771,7 @@ class Room { 'session_id': _outboundGroupSession.session_id(), if (mRelatesTo != null) 'm.relates_to': mRelatesTo, }; + _outboundGroupSessionSentMessages++; await _storeOutboundGroupSession(); return encryptedPayload; }