Merge branch 'master' into soru/cross-signing

This commit is contained in:
Sorunome 2020-05-22 13:18:36 +02:00
commit 29721f00a8
No known key found for this signature in database
GPG key ID: B19471D07FC9BE9C
17 changed files with 558 additions and 266 deletions

View file

@ -54,6 +54,7 @@ code_analyze:
image: cirrusci/flutter
dependencies: []
script:
- flutter format lib/ test/ test_driver/ --set-exit-if-changed
- flutter analyze
build-api-doc:

View file

@ -41,7 +41,10 @@ That means for example: "[users] add fetch users endpoint".
- Directories need to be lowercase.
## Code style:
- We recommend using Android Studio for coding. We are using the Android Studio auto styling with ctrl+alt+shift+L.
Please use code formatting. You can use VSCode or Android Studio. On other editors you need to run:
```
flutter format lib/**/*/*.dart
```
## Code quality:
- Don't repeat yourself! Use local variables, functions, classes.

View file

@ -84,7 +84,8 @@ class Client {
/// debug: Print debug output?
/// database: The database instance to use
/// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions
Client(this.clientName, {this.debug = false, this.database, this.enableE2eeRecovery = false}) {
Client(this.clientName,
{this.debug = false, this.database, this.enableE2eeRecovery = false}) {
onLoginStateChanged.stream.listen((loginState) {
print('LoginState: ${loginState.toString()}');
});
@ -1045,8 +1046,7 @@ class Client {
}
if (sync['account_data'] is Map<String, dynamic> &&
sync['account_data']['events'] is List<dynamic>) {
await _handleGlobalEvents(
sync['account_data']['events'], 'account_data');
await _handleGlobalEvents(sync['account_data']['events'], 'account_data');
}
if (sync['device_lists'] is Map<String, dynamic>) {
await _handleDeviceListsEvents(sync['device_lists']);
@ -1077,7 +1077,8 @@ class Client {
}
}
Future<void> _handleDeviceListsEvents(Map<String, dynamic> deviceLists) async {
Future<void> _handleDeviceListsEvents(
Map<String, dynamic> deviceLists) async {
if (deviceLists['changed'] is List) {
for (final userId in deviceLists['changed']) {
if (_userDeviceKeys.containsKey(userId)) {
@ -1179,7 +1180,8 @@ class Client {
}
}
Future<void> _handleRooms(Map<String, dynamic> rooms, Membership membership) async {
Future<void> _handleRooms(
Map<String, dynamic> rooms, Membership membership) async {
for (final entry in rooms.entries) {
final id = entry.key;
final room = entry.value;
@ -1252,8 +1254,7 @@ class Client {
if (room['timeline'] is Map<String, dynamic> &&
room['timeline']['events'] is List<dynamic> &&
room['timeline']['events'].isNotEmpty) {
await _handleRoomEvents(
id, room['timeline']['events'], 'timeline');
await _handleRoomEvents(id, room['timeline']['events'], 'timeline');
handledEvents = true;
}
@ -1318,7 +1319,8 @@ class Client {
}
}
Future<void> _handleRoomEvents(String chat_id, List<dynamic> events, String type) async {
Future<void> _handleRoomEvents(
String chat_id, List<dynamic> events, String type) async {
for (num i = 0; i < events.length; i++) {
await _handleEvent(events[i], chat_id, type);
}
@ -1341,14 +1343,17 @@ class Client {
}
}
Future<void> _handleEvent(Map<String, dynamic> event, String roomID, String type) async {
Future<void> _handleEvent(
Map<String, dynamic> event, String roomID, String type) async {
if (event['type'] is String && event['content'] is Map<String, dynamic>) {
// The client must ignore any new m.room.encryption event to prevent
// man-in-the-middle attacks!
final room = getRoomById(roomID);
if (room == null ||
(event['type'] == 'm.room.encryption' && room.encrypted &&
event['content']['algorithm'] != room.getState('m.room.encryption')?.content['algorithm'])) {
(event['type'] == 'm.room.encryption' &&
room.encrypted &&
event['content']['algorithm'] !=
room.getState('m.room.encryption')?.content['algorithm'])) {
return;
}
@ -1367,7 +1372,8 @@ class Client {
}
if (update.eventType == 'm.room.encrypted' && database != null) {
// the event is still encrytped....let's try fetching the keys from the database!
await room.loadInboundGroupSessionKey(event['content']['session_id'], event['content']['sender_key']);
await room.loadInboundGroupSessionKey(
event['content']['session_id'], event['content']['sender_key']);
update = update.decrypt(room);
}
if (type != 'ephemeral' && database != null) {
@ -1667,10 +1673,9 @@ class Client {
}
_userDeviceKeys[userId].deviceKeys[deviceId] = entry;
if (deviceId == deviceID &&
entry.ed25519Key ==
fingerprintKey) {
// Always trust the own device
entry.setDirectVerified(true);
entry.ed25519Key == fingerprintKey) {
// Always trust the own device
entry.setDirectVerified(true);
}
} else {
// This shouldn't ever happen. The same device ID has gotten
@ -1684,7 +1689,7 @@ class Client {
userId,
deviceId,
json.encode(entry.toJson()),
entry.verified,
entry.directVerified,
entry.blocked,
));
}
@ -1745,7 +1750,7 @@ class Client {
userId,
publicKey,
json.encode(entry.toJson()),
entry.verified,
entry.directVerified,
entry.blocked,
));
}

View file

@ -19,39 +19,39 @@ class Database extends _$Database {
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator m) {
return m.createAll();
},
onUpgrade: (Migrator m, int from, int to) async {
// this appears to be only called once, so multiple consecutive upgrades have to be handled appropriately in here
if (from == 1) {
await m.createIndex(userDeviceKeysIndex);
await m.createIndex(userDeviceKeysKeyIndex);
await m.createIndex(olmSessionsIndex);
await m.createIndex(outboundGroupSessionsIndex);
await m.createIndex(inboundGroupSessionsIndex);
await m.createIndex(roomsIndex);
await m.createIndex(eventsIndex);
await m.createIndex(roomStatesIndex);
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);
from++;
}
if (from == 3) {
await m.createTable(userCrossSigningKeys);
await m.createIndex(userCrossSigningKeysIndex);
// mark all keys as outdated so that the cross signing keys will be fetched
await m.issueCustomQuery('UPDATE user_device_keys SET outdated = true');
from++;
}
},
);
onCreate: (Migrator m) {
return m.createAll();
},
onUpgrade: (Migrator m, int from, int to) async {
// this appears to be only called once, so multiple consecutive upgrades have to be handled appropriately in here
if (from == 1) {
await m.createIndex(userDeviceKeysIndex);
await m.createIndex(userDeviceKeysKeyIndex);
await m.createIndex(olmSessionsIndex);
await m.createIndex(outboundGroupSessionsIndex);
await m.createIndex(inboundGroupSessionsIndex);
await m.createIndex(roomsIndex);
await m.createIndex(eventsIndex);
await m.createIndex(roomStatesIndex);
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);
from++;
}
if (from == 3) {
await m.createTable(userCrossSigningKeys);
await m.createIndex(userCrossSigningKeysIndex);
// mark all keys as outdated so that the cross signing keys will be fetched
await m.issueCustomQuery('UPDATE user_device_keys SET outdated = true');
from++;
}
},
);
Future<DbClient> getClient(String name) async {
final res = await dbGetClient(name).get();
@ -76,7 +76,8 @@ class Database extends _$Database {
return res;
}
Future<Map<String, List<olm.Session>>> getOlmSessions(int clientId, String userId) async {
Future<Map<String, List<olm.Session>>> getOlmSessions(
int clientId, String userId) async {
final raw = await getAllOlmSessions(clientId).get();
if (raw.isEmpty) {
return {};
@ -97,7 +98,8 @@ class Database extends _$Database {
return res;
}
Future<DbOutboundGroupSession> getDbOutboundGroupSession(int clientId, String roomId) async {
Future<DbOutboundGroupSession> getDbOutboundGroupSession(
int clientId, String roomId) async {
final res = await dbGetOutboundGroupSession(clientId, roomId).get();
if (res.isEmpty) {
return null;
@ -105,22 +107,28 @@ class Database extends _$Database {
return res.first;
}
Future<List<DbInboundGroupSession>> getDbInboundGroupSessions(int clientId, String roomId) async {
Future<List<DbInboundGroupSession>> getDbInboundGroupSessions(
int clientId, String roomId) async {
return await dbGetInboundGroupSessionKeys(clientId, roomId).get();
}
Future<DbInboundGroupSession> getDbInboundGroupSession(int clientId, String roomId, String sessionId) async {
final res = await dbGetInboundGroupSessionKey(clientId, roomId, sessionId).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();
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 roomList = <sdk.Room>[];
@ -157,11 +165,13 @@ class Database extends _$Database {
/// Stores a RoomUpdate object in the database. Must be called inside of
/// [transaction].
final Set<String> _ensuredRooms = {};
Future<void> storeRoomUpdate(int clientId, sdk.RoomUpdate roomUpdate, [sdk.Room oldRoom]) async {
Future<void> storeRoomUpdate(int clientId, sdk.RoomUpdate roomUpdate,
[sdk.Room oldRoom]) async {
final setKey = '${clientId};${roomUpdate.id}';
if (roomUpdate.membership != sdk.Membership.leave) {
if (!_ensuredRooms.contains(setKey)) {
await ensureRoomExists(clientId, roomUpdate.id, roomUpdate.membership.toString().split('.').last);
await ensureRoomExists(clientId, roomUpdate.id,
roomUpdate.membership.toString().split('.').last);
_ensuredRooms.add(setKey);
}
} else {
@ -173,24 +183,37 @@ class Database extends _$Database {
var doUpdate = oldRoom == null;
if (!doUpdate) {
doUpdate = roomUpdate.highlight_count != oldRoom.highlightCount ||
roomUpdate.notification_count != oldRoom.notificationCount ||
roomUpdate.membership.toString().split('.').last != oldRoom.membership.toString().split('.').last ||
(roomUpdate.summary?.mJoinedMemberCount != null &&
roomUpdate.summary.mJoinedMemberCount != oldRoom.mInvitedMemberCount) ||
(roomUpdate.summary?.mInvitedMemberCount != null &&
roomUpdate.summary.mJoinedMemberCount != oldRoom.mJoinedMemberCount) ||
(roomUpdate.summary?.mHeroes != null &&
roomUpdate.summary.mHeroes.join(',') != oldRoom.mHeroes.join(','));
roomUpdate.notification_count != oldRoom.notificationCount ||
roomUpdate.membership.toString().split('.').last !=
oldRoom.membership.toString().split('.').last ||
(roomUpdate.summary?.mJoinedMemberCount != null &&
roomUpdate.summary.mJoinedMemberCount !=
oldRoom.mInvitedMemberCount) ||
(roomUpdate.summary?.mInvitedMemberCount != null &&
roomUpdate.summary.mJoinedMemberCount !=
oldRoom.mJoinedMemberCount) ||
(roomUpdate.summary?.mHeroes != null &&
roomUpdate.summary.mHeroes.join(',') !=
oldRoom.mHeroes.join(','));
}
if (doUpdate) {
await (update(rooms)..where((r) => r.roomId.equals(roomUpdate.id) & r.clientId.equals(clientId))).write(RoomsCompanion(
await (update(rooms)
..where((r) =>
r.roomId.equals(roomUpdate.id) & r.clientId.equals(clientId)))
.write(RoomsCompanion(
highlightCount: Value(roomUpdate.highlight_count),
notificationCount: Value(roomUpdate.notification_count),
membership: Value(roomUpdate.membership.toString().split('.').last),
joinedMemberCount: roomUpdate.summary?.mJoinedMemberCount != null ? Value(roomUpdate.summary.mJoinedMemberCount) : Value.absent(),
invitedMemberCount: roomUpdate.summary?.mInvitedMemberCount != null ? Value(roomUpdate.summary.mInvitedMemberCount) : Value.absent(),
heroes: roomUpdate.summary?.mHeroes != null ? Value(roomUpdate.summary.mHeroes.join(',')) : Value.absent(),
joinedMemberCount: roomUpdate.summary?.mJoinedMemberCount != null
? Value(roomUpdate.summary.mJoinedMemberCount)
: Value.absent(),
invitedMemberCount: roomUpdate.summary?.mInvitedMemberCount != null
? Value(roomUpdate.summary.mInvitedMemberCount)
: Value.absent(),
heroes: roomUpdate.summary?.mHeroes != null
? Value(roomUpdate.summary.mHeroes.join(','))
: Value.absent(),
));
}
@ -205,17 +228,24 @@ class Database extends _$Database {
/// Stores an UserUpdate object in the database. Must be called inside of
/// [transaction].
Future<void> storeUserEventUpdate(int clientId, sdk.UserUpdate userUpdate) async {
Future<void> storeUserEventUpdate(
int clientId, sdk.UserUpdate userUpdate) async {
if (userUpdate.type == 'account_data') {
await storeAccountData(clientId, userUpdate.eventType, json.encode(userUpdate.content['content']));
await storeAccountData(clientId, userUpdate.eventType,
json.encode(userUpdate.content['content']));
} else if (userUpdate.type == 'presence') {
await storePresence(clientId, userUpdate.eventType, userUpdate.content['sender'], json.encode(userUpdate.content['content']));
await storePresence(
clientId,
userUpdate.eventType,
userUpdate.content['sender'],
json.encode(userUpdate.content['content']));
}
}
/// Stores an EventUpdate object in the database. Must be called inside of
/// [transaction].
Future<void> storeEventUpdate(int clientId, sdk.EventUpdate eventUpdate) async {
Future<void> storeEventUpdate(
int clientId, sdk.EventUpdate eventUpdate) async {
if (eventUpdate.type == 'ephemeral') return;
final eventContent = eventUpdate.content;
final type = eventUpdate.type;
@ -239,11 +269,13 @@ class Database extends _$Database {
eventContent['unsigned'] is Map<String, dynamic> &&
eventContent['unsigned']['transaction_id'] is String) {
// status changed and we have an old transaction id --> update event id and stuffs
await updateEventStatus(status, eventContent['event_id'], clientId, eventContent['unsigned']['transaction_id'], chatId);
await updateEventStatus(status, eventContent['event_id'], clientId,
eventContent['unsigned']['transaction_id'], chatId);
} else {
DbEvent oldEvent;
if (type == 'history') {
final allOldEvents = await getEvent(clientId, eventContent['event_id'], chatId).get();
final allOldEvents =
await getEvent(clientId, eventContent['event_id'], chatId).get();
if (allOldEvents.isNotEmpty) {
oldEvent = allOldEvents.first;
}
@ -253,7 +285,10 @@ class Database extends _$Database {
eventContent['event_id'],
chatId,
oldEvent?.sortOrder ?? eventUpdate.sortOrder,
eventContent['origin_server_ts'] != null ? DateTime.fromMillisecondsSinceEpoch(eventContent['origin_server_ts']) : DateTime.now(),
eventContent['origin_server_ts'] != null
? DateTime.fromMillisecondsSinceEpoch(
eventContent['origin_server_ts'])
: DateTime.now(),
eventContent['sender'],
eventContent['type'],
json.encode(eventContent['unsigned'] ?? ''),
@ -268,7 +303,8 @@ class Database extends _$Database {
if (status != -1 &&
eventUpdate.content.containsKey('unsigned') &&
eventUpdate.content['unsigned']['transaction_id'] is String) {
await removeEvent(clientId, eventUpdate.content['unsigned']['transaction_id'], chatId);
await removeEvent(clientId,
eventUpdate.content['unsigned']['transaction_id'], chatId);
}
}
@ -281,7 +317,10 @@ class Database extends _$Database {
eventContent['event_id'] ?? now.millisecondsSinceEpoch.toString(),
chatId,
eventUpdate.sortOrder ?? 0.0,
eventContent['origin_server_ts'] != null ? DateTime.fromMillisecondsSinceEpoch(eventContent['origin_server_ts']) : now,
eventContent['origin_server_ts'] != null
? DateTime.fromMillisecondsSinceEpoch(
eventContent['origin_server_ts'])
: now,
eventContent['sender'],
eventContent['type'],
json.encode(eventContent['unsigned'] ?? ''),
@ -299,7 +338,8 @@ class Database extends _$Database {
}
}
Future<sdk.Event> getEventById(int clientId, String eventId, sdk.Room room) async {
Future<sdk.Event> getEventById(
int clientId, String eventId, sdk.Room room) async {
final event = await getEvent(clientId, eventId, room.id).get();
if (event.isEmpty) {
return null;
@ -308,7 +348,9 @@ class Database extends _$Database {
}
Future<bool> redactMessage(int clientId, sdk.EventUpdate eventUpdate) async {
final events = await getEvent(clientId, eventUpdate.content['redacts'], eventUpdate.roomID).get();
final events = await getEvent(
clientId, eventUpdate.content['redacts'], eventUpdate.roomID)
.get();
var success = false;
for (final dbEvent in events) {
final event = sdk.Event.fromDb(dbEvent, null);
@ -337,29 +379,44 @@ class Database extends _$Database {
Future<void> forgetRoom(int clientId, String roomId) async {
final setKey = '${clientId};${roomId}';
_ensuredRooms.remove(setKey);
await (delete(rooms)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
await (delete(events)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
await (delete(roomStates)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
await (delete(roomAccountData)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
await (delete(rooms)
..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId)))
.go();
await (delete(events)
..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId)))
.go();
await (delete(roomStates)
..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId)))
.go();
await (delete(roomAccountData)
..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId)))
.go();
}
Future<void> clearCache(int clientId) async {
await (delete(presences)..where((r) => r.clientId.equals(clientId))).go();
await (delete(roomAccountData)..where((r) => r.clientId.equals(clientId))).go();
await (delete(roomAccountData)..where((r) => r.clientId.equals(clientId)))
.go();
await (delete(accountData)..where((r) => r.clientId.equals(clientId))).go();
await (delete(roomStates)..where((r) => r.clientId.equals(clientId))).go();
await (delete(events)..where((r) => r.clientId.equals(clientId))).go();
await (delete(rooms)..where((r) => r.clientId.equals(clientId))).go();
await (delete(outboundGroupSessions)..where((r) => r.clientId.equals(clientId))).go();
await (delete(outboundGroupSessions)
..where((r) => r.clientId.equals(clientId)))
.go();
await storePrevBatch(null, clientId);
}
Future<void> clear(int clientId) async {
await clearCache(clientId);
await (delete(inboundGroupSessions)..where((r) => r.clientId.equals(clientId))).go();
await (delete(inboundGroupSessions)
..where((r) => r.clientId.equals(clientId)))
.go();
await (delete(olmSessions)..where((r) => r.clientId.equals(clientId))).go();
await (delete(userDeviceKeysKey)..where((r) => r.clientId.equals(clientId))).go();
await (delete(userDeviceKeys)..where((r) => r.clientId.equals(clientId))).go();
await (delete(userDeviceKeysKey)..where((r) => r.clientId.equals(clientId)))
.go();
await (delete(userDeviceKeys)..where((r) => r.clientId.equals(clientId)))
.go();
await (delete(clients)..where((r) => r.clientId.equals(clientId))).go();
}

View file

@ -42,7 +42,15 @@ class Presence {
final String statusMsg;
final DateTime time;
Presence({this.sender, this.displayname, this.avatarUrl, this.currentlyActive, this.lastActiveAgo, this.presence, this.statusMsg, this.time});
Presence(
{this.sender,
this.displayname,
this.avatarUrl,
this.currentlyActive,
this.lastActiveAgo,
this.presence,
this.statusMsg,
this.time});
Presence.fromJson(Map<String, dynamic> json)
: sender = json['sender'],
@ -66,15 +74,16 @@ class Presence {
return Presence(
sender: dbEntry.sender,
displayname: content['displayname'],
avatarUrl: content['avatar_url'] != null ? Uri.parse(content['avatar_url']) : null,
avatarUrl: content['avatar_url'] != null
? Uri.parse(content['avatar_url'])
: null,
currentlyActive: content['currently_active'],
lastActiveAgo: content['last_active_ago'],
time: DateTime.fromMillisecondsSinceEpoch(
DateTime.now().millisecondsSinceEpoch -
(content['last_active_ago'] ?? 0)),
presence: PresenceType.values.firstWhere(
(e) =>
e.toString() == "PresenceType.${content['presence']}",
(e) => e.toString() == "PresenceType.${content['presence']}",
orElse: () => null),
statusMsg: content['status_msg'],
);

View file

@ -112,7 +112,8 @@ class Room {
}
Future<void> updateSortOrder() async {
await client.database?.updateRoomSortOrder(_oldestSortOrder, _newestSortOrder, client.id, id);
await client.database?.updateRoomSortOrder(
_oldestSortOrder, _newestSortOrder, client.id, id);
}
/// Clears the existing outboundGroupSession, tries to create a new one and
@ -165,10 +166,12 @@ class Room {
Future<void> _storeOutboundGroupSession() async {
if (_outboundGroupSession == null) return;
await client.database?.storeOutboundGroupSession(
client.id, id, _outboundGroupSession.pickle(client.userID),
json.encode(_outboundGroupSessionDevices), _outboundGroupSessionCreationTime,
_outboundGroupSessionSentMessages
);
client.id,
id,
_outboundGroupSession.pickle(client.userID),
json.encode(_outboundGroupSessionDevices),
_outboundGroupSessionCreationTime,
_outboundGroupSessionSentMessages);
return;
}
@ -190,21 +193,27 @@ class Room {
}
// 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
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())) {
_outboundGroupSessionCreationTime
.add(Duration(milliseconds: maxAge))
.isBefore(DateTime.now())) {
wipe = true;
}
if (!wipe) {
return false;
}
}
if (!wipe && _outboundGroupSessionDevices == null && _outboundGroupSession == null) {
if (!wipe &&
_outboundGroupSessionDevices == null &&
_outboundGroupSession == null) {
return true; // let's just short-circuit out of here, no need to do DB stuff
}
_outboundGroupSessionDevices = null;
@ -250,8 +259,12 @@ class Room {
indexes: {},
key: client.userID,
);
client.database?.storeInboundGroupSession(client.id, id, sessionId,
inboundGroupSession.pickle(client.userID), json.encode(content),
client.database?.storeInboundGroupSession(
client.id,
id,
sessionId,
inboundGroupSession.pickle(client.userID),
json.encode(content),
json.encode({}),
);
_tryAgainDecryptLastMessage();
@ -398,8 +411,7 @@ class Room {
states.forEach((final String key, final entry) {
if (!entry.containsKey('')) return;
final Event state = entry[''];
if (state.sortOrder != null &&
state.sortOrder > lastSortOrder) {
if (state.sortOrder != null && state.sortOrder > lastSortOrder) {
lastSortOrder = state.sortOrder;
lastEvent = state;
}
@ -436,7 +448,8 @@ class Room {
this.roomAccountData = const {},
double newestSortOrder = 0.0,
double oldestSortOrder = 0.0,
}) : _newestSortOrder = newestSortOrder, _oldestSortOrder = oldestSortOrder;
}) : _newestSortOrder = newestSortOrder,
_oldestSortOrder = oldestSortOrder;
/// The default count of how much events should be requested when requesting the
/// history of this room.
@ -519,28 +532,79 @@ class Room {
/// return all current emote packs for this room
Map<String, Map<String, String>> get emotePacks {
final packs = <String, Map<String, String>>{};
final addEmotePack = (String packName, Map<String, dynamic> content) {
packs[packName] = <String, String>{};
content.forEach((key, value) {
final normalizeEmotePackName = (String name) {
name = name.replaceAll(' ', '-');
name = name.replaceAll(RegExp(r'[^\w-]'), '');
return name.toLowerCase();
};
final addEmotePack = (String packName, Map<String, dynamic> content,
[String packNameOverride]) {
if (!(content['short'] is Map)) {
return;
}
if (content['pack'] is Map && content['pack']['name'] is String) {
packName = content['pack']['name'];
}
if (packNameOverride != null && packNameOverride.isNotEmpty) {
packName = packNameOverride;
}
packName = normalizeEmotePackName(packName);
if (!packs.containsKey(packName)) {
packs[packName] = <String, String>{};
}
content['short'].forEach((key, value) {
if (key is String && value is String && value.startsWith('mxc://')) {
packs[packName][key] = value;
}
});
};
final roomEmotes = getState('im.ponies.room_emotes');
final userEmotes = client.accountData['im.ponies.user_emotes'];
if (roomEmotes != null && roomEmotes.content['short'] is Map) {
addEmotePack('room', roomEmotes.content['short']);
// first add all the room emotes
final allRoomEmotes = states.states['im.ponies.room_emotes'];
if (allRoomEmotes != null) {
for (final entry in allRoomEmotes.entries) {
final stateKey = entry.key;
final event = entry.value;
addEmotePack(stateKey.isEmpty ? 'room' : stateKey, event.content);
}
}
if (userEmotes != null && userEmotes.content['short'] is Map) {
addEmotePack('user', userEmotes.content['short']);
// next add all the user emotes
final userEmotes = client.accountData['im.ponies.user_emotes'];
if (userEmotes != null) {
addEmotePack('user', userEmotes.content);
}
// finally add all the external emote rooms
final emoteRooms = client.accountData['im.ponies.emote_rooms'];
if (emoteRooms != null && emoteRooms.content['rooms'] is Map) {
for (final roomEntry in emoteRooms.content['rooms'].entries) {
final roomId = roomEntry.key;
if (roomId == id) {
continue;
}
final room = client.getRoomById(roomId);
if (room != null && roomEntry.value is Map) {
for (final stateKeyEntry in roomEntry.value.entries) {
final stateKey = stateKeyEntry.key;
final event = room.getState('im.ponies.room_emotes', stateKey);
if (event != null && stateKeyEntry.value is Map) {
addEmotePack(
room.canonicalAlias.isEmpty ? room.id : canonicalAlias,
event.content,
stateKeyEntry.value['name']);
}
}
}
}
}
return packs;
}
/// Sends a normal text message to this room. Returns the event ID generated
/// by the server for this message.
Future<String> sendTextEvent(String message, {String txid, Event inReplyTo, bool parseMarkdown = true, Map<String, Map<String, String>> emotePacks}) {
Future<String> sendTextEvent(String message,
{String txid,
Event inReplyTo,
bool parseMarkdown = true,
Map<String, Map<String, String>> emotePacks}) {
final event = <String, dynamic>{
'msgtype': 'm.text',
'body': message,
@ -793,7 +857,11 @@ class Room {
final sortOrder = newSortOrder;
// Display a *sending* event and store it.
var eventUpdate = EventUpdate(type: 'timeline', roomID: id, eventType: type, sortOrder: sortOrder,
var eventUpdate = EventUpdate(
type: 'timeline',
roomID: id,
eventType: type,
sortOrder: sortOrder,
content: {
'type': type,
'event_id': messageID,
@ -951,7 +1019,8 @@ class Room {
final dbActions = <Future<dynamic> Function()>[];
if (client.database != null) {
dbActions.add(() => client.database.setRoomPrevBatch(prev_batch, client.id, id));
dbActions.add(
() => client.database.setRoomPrevBatch(prev_batch, client.id, id));
}
if (!(resp['chunk'] is List<dynamic> &&
@ -969,7 +1038,8 @@ class Room {
).decrypt(this);
client.onEvent.add(eventUpdate);
if (client.database != null) {
dbActions.add(() => client.database.storeEventUpdate(client.id, eventUpdate));
dbActions.add(
() => client.database.storeEventUpdate(client.id, eventUpdate));
}
}
}
@ -985,11 +1055,13 @@ class Room {
).decrypt(this);
client.onEvent.add(eventUpdate);
if (client.database != null) {
dbActions.add(() => client.database.storeEventUpdate(client.id, eventUpdate));
dbActions.add(
() => client.database.storeEventUpdate(client.id, eventUpdate));
}
}
if (client.database != null) {
dbActions.add(() => client.database.setRoomPrevBatch(resp['end'], client.id, id));
dbActions.add(
() => client.database.setRoomPrevBatch(resp['end'], client.id, id));
}
await client.database?.transaction(() async {
for (final f in dbActions) {
@ -1062,12 +1134,12 @@ class Room {
/// 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(
DbRoom row, // either Map<String, dynamic> or DbRoom
Client matrix,
{
dynamic states, // DbRoomState, as iterator and optionally as future
dynamic roomAccountData, // DbRoomAccountData, as iterator and optionally as future
}) async {
DbRoom row, // either Map<String, dynamic> or DbRoom
Client matrix, {
dynamic states, // DbRoomState, as iterator and optionally as future
dynamic
roomAccountData, // DbRoomAccountData, as iterator and optionally as future
}) async {
final newRoom = Room(
id: row.roomId,
membership: Membership.values
@ -1093,7 +1165,8 @@ class Room {
rawStates = states;
}
for (final rawState in rawStates) {
final newState = Event.fromDb(rawState, newRoom);;
final newState = Event.fromDb(rawState, newRoom);
;
newRoom.setState(newState);
}
}
@ -1136,7 +1209,8 @@ class Room {
await events[i].loadSession();
events[i] = events[i].decrypted;
if (events[i].type != EventTypes.Encrypted) {
await client.database.storeEventUpdate(client.id,
await client.database.storeEventUpdate(
client.id,
EventUpdate(
eventType: events[i].typeKey,
content: events[i].toJson(),
@ -1258,7 +1332,8 @@ class Room {
'content': resp,
'state_key': mxID,
};
await client.database.storeEventUpdate(client.id,
await client.database.storeEventUpdate(
client.id,
EventUpdate(
content: content,
roomID: id,
@ -1705,7 +1780,8 @@ class Room {
var users = await requestParticipants();
for (final user in users) {
if (client.userDeviceKeys.containsKey(user.id)) {
for (var deviceKeyEntry in client.userDeviceKeys[user.id].deviceKeys.values) {
for (var deviceKeyEntry
in client.userDeviceKeys[user.id].deviceKeys.values) {
deviceKeys.add(deviceKeyEntry);
}
}
@ -1719,14 +1795,14 @@ class Room {
if (_restoredOutboundGroupSession || client.database == null) {
return;
}
final outboundSession = await client.database.getDbOutboundGroupSession(client.id, id);
final outboundSession =
await client.database.getDbOutboundGroupSession(client.id, id);
if (outboundSession != null) {
try {
_outboundGroupSession = olm.OutboundGroupSession();
_outboundGroupSession.unpickle(
client.userID, outboundSession.pickle);
_outboundGroupSession.unpickle(client.userID, outboundSession.pickle);
_outboundGroupSessionDevices =
List<String>.from(json.decode(outboundSession.deviceIds));
List<String>.from(json.decode(outboundSession.deviceIds));
_outboundGroupSessionCreationTime = outboundSession.creationTime;
_outboundGroupSessionSentMessages = outboundSession.sentMessages;
} catch (e) {
@ -1782,46 +1858,53 @@ class Room {
Future<void> requestSessionKey(String sessionId, String senderKey) async {
final users = await requestParticipants();
await client.sendToDevice(
[],
'm.room_key_request',
{
'action': 'request_cancellation',
'request_id': base64.encode(utf8.encode(sessionId)),
'requesting_device_id': client.deviceID,
},
encrypted: false,
toUsers: users);
await client.sendToDevice(
[],
'm.room_key_request',
{
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': id,
'sender_key': senderKey,
'session_id': sessionId,
[],
'm.room_key_request',
{
'action': 'request_cancellation',
'request_id': base64.encode(utf8.encode(sessionId)),
'requesting_device_id': client.deviceID,
},
'request_id': base64.encode(utf8.encode(sessionId)),
'requesting_device_id': client.deviceID,
},
encrypted: false,
toUsers: users);
encrypted: false,
toUsers: users);
await client.sendToDevice(
[],
'm.room_key_request',
{
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': id,
'sender_key': senderKey,
'session_id': sessionId,
},
'request_id': base64.encode(utf8.encode(sessionId)),
'requesting_device_id': client.deviceID,
},
encrypted: false,
toUsers: users);
}
Future<void> loadInboundGroupSessionKey(String sessionId, [String senderKey]) async {
if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) return; // nothing to do
final session = await client.database.getDbInboundGroupSession(client.id, id, sessionId);
Future<void> loadInboundGroupSessionKey(String sessionId,
[String senderKey]) async {
if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) {
return;
} // nothing to do
final session = await client.database
.getDbInboundGroupSession(client.id, id, sessionId);
if (session == null) {
// no session found, let's request it!
if (client.enableE2eeRecovery && !_requestedSessionIds.contains(sessionId) && senderKey != null) {
if (client.enableE2eeRecovery &&
!_requestedSessionIds.contains(sessionId) &&
senderKey != null) {
unawaited(requestSessionKey(sessionId, senderKey));
_requestedSessionIds.add(sessionId);
}
return;
}
try {
_inboundGroupSessions[sessionId] = SessionKey.fromDb(session, client.userID);
_inboundGroupSessions[sessionId] =
SessionKey.fromDb(session, client.userID);
} catch (e) {
print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString());
}
@ -1844,7 +1927,8 @@ class Room {
/// Returns a m.bad.encrypted event if it fails and does nothing if the event
/// was not encrypted.
Event decryptGroupMessage(Event event) {
if (event.type != EventTypes.Encrypted || event.content['ciphertext'] == null) return event;
if (event.type != EventTypes.Encrypted ||
event.content['ciphertext'] == null) return event;
Map<String, dynamic> decryptedPayload;
try {
if (!client.encryptionEnabled) {
@ -1862,7 +1946,9 @@ class Room {
.decrypt(event.content['ciphertext']);
final messageIndexKey =
event.eventId + event.time.millisecondsSinceEpoch.toString();
if (inboundGroupSessions[sessionId].indexes.containsKey(messageIndexKey) &&
if (inboundGroupSessions[sessionId]
.indexes
.containsKey(messageIndexKey) &&
inboundGroupSessions[sessionId].indexes[messageIndexKey] !=
decryptResult.message_index) {
if ((_outboundGroupSession?.session_id() ?? '') == sessionId) {
@ -1876,11 +1962,17 @@ class Room {
// the entry should always exist. In the case it doesn't, the following
// line *could* throw an error. As that is a future, though, and we call
// it un-awaited here, nothing happens, which is exactly the result we want
client.database?.updateInboundGroupSessionIndexes(json.encode(inboundGroupSessions[sessionId].indexes), client.id, id, sessionId);
client.database?.updateInboundGroupSessionIndexes(
json.encode(inboundGroupSessions[sessionId].indexes),
client.id,
id,
sessionId);
decryptedPayload = json.decode(decryptResult.plaintext);
} catch (exception) {
// alright, if this was actually by our own outbound group session, we might as well clear it
if (client.enableE2eeRecovery && (_outboundGroupSession?.session_id() ?? '') == event.content['session_id']) {
if (client.enableE2eeRecovery &&
(_outboundGroupSession?.session_id() ?? '') ==
event.content['session_id']) {
clearOutboundGroupSession(wipe: true);
}
if (exception.toString() == DecryptError.UNKNOWN_SESSION) {

View file

@ -43,7 +43,8 @@ class EventUpdate {
// the order where to stort this event
final double sortOrder;
EventUpdate({this.eventType, this.roomID, this.type, this.content, this.sortOrder});
EventUpdate(
{this.eventType, this.roomID, this.type, this.content, this.sortOrder});
EventUpdate decrypt(Room room) {
if (eventType != 'm.room.encrypted') {

View file

@ -26,6 +26,7 @@ import 'dart:async';
import 'event.dart';
import 'room.dart';
import 'sync/event_update.dart';
import 'sync/room_update.dart';
typedef onTimelineUpdateCallback = void Function();
typedef onTimelineInsertCallback = void Function(int insertID);
@ -41,6 +42,7 @@ class Timeline {
final onTimelineInsertCallback onInsert;
StreamSubscription<EventUpdate> sub;
StreamSubscription<RoomUpdate> roomSub;
StreamSubscription<String> sessionIdReceivedSub;
bool _requestingHistoryLock = false;
@ -77,6 +79,13 @@ class Timeline {
Timeline({this.room, this.events, this.onUpdate, this.onInsert}) {
sub ??= room.client.onEvent.stream.listen(_handleEventUpdate);
// if the timeline is limited we want to clear our events cache
// as r.limitedTimeline can be "null" sometimes, we need to check for == true
// as after receiving a limited timeline room update new events are expected
// to be received via the onEvent stream, it is unneeded to call sortAndUpdate
roomSub ??= room.client.onRoomUpdate.stream
.where((r) => r.id == room.id && r.limitedTimeline == true)
.listen((r) => events.clear());
sessionIdReceivedSub ??=
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
}
@ -84,6 +93,7 @@ class Timeline {
/// Don't forget to call this before you dismiss this object!
void cancelSubscriptions() {
sub?.cancel();
roomSub?.cancel();
sessionIdReceivedSub?.cancel();
}
@ -121,8 +131,8 @@ class Timeline {
if (eventUpdate.eventType == 'm.room.redaction') {
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
if (eventId != null) {
events[eventId]
.setRedactionEvent(Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder));
events[eventId].setRedactionEvent(Event.fromJson(
eventUpdate.content, room, eventUpdate.sortOrder));
}
} else if (eventUpdate.content['status'] == -2) {
var i = _findEvent(event_id: eventUpdate.content['event_id']);
@ -138,18 +148,23 @@ class Timeline {
: null);
if (i < events.length) {
events[i] = Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
events[i] = Event.fromJson(
eventUpdate.content, room, eventUpdate.sortOrder);
}
} else {
Event newEvent;
var senderUser = room.getState('m.room.member', eventUpdate.content['sender'])?.asUser ?? await room.client.database
?.getUser(room.client.id, eventUpdate.content['sender'], room);
var senderUser = room
.getState('m.room.member', eventUpdate.content['sender'])
?.asUser ??
await room.client.database?.getUser(
room.client.id, eventUpdate.content['sender'], room);
if (senderUser != null) {
eventUpdate.content['displayname'] = senderUser.displayName;
eventUpdate.content['avatar_url'] = senderUser.avatarUrl.toString();
}
newEvent = Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
newEvent =
Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
if (eventUpdate.type == 'history' &&
events.indexWhere(

View file

@ -258,11 +258,17 @@ class DeviceKeys extends _SignedKey {
String get curve25519Key => keys['curve25519:$deviceId'];
bool get isValid => userId != null && deviceId != null && keys != null && curve25519Key != null && ed25519Key != null;
bool get isValid =>
userId != null &&
deviceId != null &&
keys != null &&
curve25519Key != null &&
ed25519Key != null;
Future<void> setVerified(bool newVerified) {
Future<void> setVerified(bool newVerified, Client client) {
_verified = newVerified;
return client.database?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
return client.database
?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
}
Future<void> setBlocked(bool newBlocked) {
@ -273,7 +279,8 @@ class DeviceKeys extends _SignedKey {
room.clearOutboundGroupSession();
}
}
return client.database?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
return client.database
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
}
DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry, Client cl) {
@ -282,10 +289,12 @@ class DeviceKeys extends _SignedKey {
content = Map<String, dynamic>.from(json);
userId = dbEntry.userId;
identifier = dbEntry.deviceId;
algorithms = json['algorithms'].cast<String>();
keys = json['keys'] != null ? Map<String, String>.from(json['keys']) : null;
signatures = json['signatures'] != null
? Map<String, dynamic>.from(json['signatures'])
algorithms = content['algorithms'].cast<String>();
keys = content['keys'] != null
? Map<String, String>.from(content['keys'])
: null;
signatures = content['signatures'] != null
? Map<String, dynamic>.from(content['signatures'])
: null;
unsigned = json['unsigned'] != null
? Map<String, dynamic>.from(json['unsigned'])
@ -311,8 +320,9 @@ class DeviceKeys extends _SignedKey {
blocked = json['blocked'] ?? false;
}
KeyVerification startVerification() {
final request = KeyVerification(client: client, userId: userId, deviceId: deviceId);
KeyVerification startVerification(Client client) {
final request =
KeyVerification(client: client, userId: userId, deviceId: deviceId);
request.start();
client.addKeyVerificationRequest(request);
return request;

View file

@ -42,7 +42,14 @@ import '../room.dart';
| |
*/
enum KeyVerificationState { askAccept, waitingAccept, askSas, waitingSas, done, error }
enum KeyVerificationState {
askAccept,
waitingAccept,
askSas,
waitingSas,
done,
error
}
List<String> _intersect(List<String> a, List<dynamic> b) {
final res = <String>[];
@ -76,7 +83,8 @@ List<int> _bytesToInt(Uint8List bytes, int totalBits) {
final VERIFICATION_METHODS = [_KeyVerificationMethodSas.type];
_KeyVerificationMethod _makeVerificationMethod(String type, KeyVerification request) {
_KeyVerificationMethod _makeVerificationMethod(
String type, KeyVerification request) {
if (type == _KeyVerificationMethodSas.type) {
return _KeyVerificationMethodSas(request: request);
}
@ -89,7 +97,7 @@ class KeyVerification {
final Room room;
final String userId;
void Function() onUpdate;
String get deviceId => _deviceId;
String get deviceId => _deviceId;
String _deviceId;
bool startedVerification = false;
_KeyVerificationMethod method;
@ -104,7 +112,8 @@ class KeyVerification {
String canceledCode;
String canceledReason;
KeyVerification({this.client, this.room, this.userId, String deviceId, this.onUpdate}) {
KeyVerification(
{this.client, this.room, this.userId, String deviceId, this.onUpdate}) {
lastActivity = DateTime.now();
_deviceId ??= deviceId;
}
@ -115,9 +124,10 @@ class KeyVerification {
}
static String getTransactionId(Map<String, dynamic> payload) {
return payload['transaction_id'] ?? (
payload['m.relates_to'] is Map ? payload['m.relates_to']['event_id'] : null
);
return payload['transaction_id'] ??
(payload['m.relates_to'] is Map
? payload['m.relates_to']['event_id']
: null);
}
Future<void> start() async {
@ -132,7 +142,8 @@ class KeyVerification {
setState(KeyVerificationState.waitingAccept);
}
Future<void> handlePayload(String type, Map<String, dynamic> payload, [String eventId]) async {
Future<void> handlePayload(String type, Map<String, dynamic> payload,
[String eventId]) async {
print('[Key Verification] Received type ${type}: ' + payload.toString());
try {
switch (type) {
@ -141,13 +152,16 @@ class KeyVerification {
transactionId ??= eventId ?? payload['transaction_id'];
// verify the timestamp
final now = DateTime.now();
final verifyTime = DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) || now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
final verifyTime =
DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) ||
now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
await cancel('m.timeout');
return;
}
// verify it has a method we can use
possibleMethods = _intersect(VERIFICATION_METHODS, payload['methods']);
possibleMethods =
_intersect(VERIFICATION_METHODS, payload['methods']);
if (possibleMethods.isEmpty) {
// reject it outright
await cancel('m.unknown_method');
@ -156,7 +170,8 @@ class KeyVerification {
setState(KeyVerificationState.askAccept);
break;
case 'm.key.verification.ready':
possibleMethods = _intersect(VERIFICATION_METHODS, payload['methods']);
possibleMethods =
_intersect(VERIFICATION_METHODS, payload['methods']);
if (possibleMethods.isEmpty) {
// reject it outright
await cancel('m.unknown_method');
@ -214,7 +229,8 @@ class KeyVerification {
/// called when the user accepts an incoming verification
Future<void> acceptVerification() async {
if (!(await verifyLastStep(['m.key.verification.request', 'm.key.verification.start']))) {
if (!(await verifyLastStep(
['m.key.verification.request', 'm.key.verification.start']))) {
return;
}
setState(KeyVerificationState.waitingAccept);
@ -231,7 +247,8 @@ class KeyVerification {
/// called when the user rejects an incoming verification
Future<void> rejectVerification() async {
if (!(await verifyLastStep(['m.key.verification.request', 'm.key.verification.start']))) {
if (!(await verifyLastStep(
['m.key.verification.request', 'm.key.verification.start']))) {
return;
}
await cancel('m.user');
@ -251,7 +268,9 @@ class KeyVerification {
List<int> get sasNumbers {
if (method is _KeyVerificationMethodSas) {
return _bytesToInt((method as _KeyVerificationMethodSas).makeSas(5), 13).map((n) => n + 1000).toList();
return _bytesToInt((method as _KeyVerificationMethodSas).makeSas(5), 13)
.map((n) => n + 1000)
.toList();
}
return [];
}
@ -265,7 +284,8 @@ class KeyVerification {
List<KeyVerificationEmoji> get sasEmojis {
if (method is _KeyVerificationMethodSas) {
final numbers = _bytesToInt((method as _KeyVerificationMethodSas).makeSas(6), 6);
final numbers =
_bytesToInt((method as _KeyVerificationMethodSas).makeSas(6), 6);
return numbers.map((n) => KeyVerificationEmoji(n)).toList().sublist(0, 7);
}
return [];
@ -282,8 +302,10 @@ class KeyVerification {
final keyId = entry.key;
final verifyDeviceId = keyId.substring('ed25519:'.length);
final keyInfo = entry.value;
if (client.userDeviceKeys[userId].deviceKeys.containsKey(verifyDeviceId)) {
if (!(await verifier(keyInfo, client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]))) {
if (client.userDeviceKeys[userId].deviceKeys
.containsKey(verifyDeviceId)) {
if (!(await verifier(keyInfo,
client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]))) {
await cancel('m.key_mismatch');
return;
}
@ -309,7 +331,8 @@ class KeyVerification {
}
Future<bool> verifyActivity() async {
if (lastActivity != null && lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
if (lastActivity != null &&
lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
lastActivity = DateTime.now();
return true;
}
@ -360,7 +383,8 @@ class KeyVerification {
if (['m.key.verification.request'].contains(type)) {
payload['msgtype'] = type;
payload['to'] = userId;
payload['body'] = 'Attempting verification request. (${type}) Apparently your client doesn\'t support this';
payload['body'] =
'Attempting verification request. (${type}) Apparently your client doesn\'t support this';
type = 'm.room.message';
}
final newTransactionId = await room.sendEvent(payload, type: type);
@ -369,7 +393,8 @@ class KeyVerification {
client.addKeyVerificationRequest(this);
}
} else {
await client.sendToDevice([client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload);
await client.sendToDevice(
[client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload);
}
}
@ -394,6 +419,7 @@ abstract class _KeyVerificationMethod {
bool validateStart(Map<String, dynamic> payload) {
return false;
}
Future<void> sendStart();
void dispose() {}
}
@ -404,7 +430,8 @@ const KNOWN_MESSAGE_AUTHENTIFICATION_CODES = ['hkdf-hmac-sha256'];
const KNOWN_AUTHENTICATION_TYPES = ['emoji', 'decimal'];
class _KeyVerificationMethodSas extends _KeyVerificationMethod {
_KeyVerificationMethodSas({KeyVerification request}) : super(request: request);
_KeyVerificationMethodSas({KeyVerification request})
: super(request: request);
static String type = 'm.sas.v1';
@ -428,7 +455,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
try {
switch (type) {
case 'm.key.verification.start':
if (!(await request.verifyLastStep(['m.key.verification.request', 'm.key.verification.start']))) {
if (!(await request.verifyLastStep(
['m.key.verification.request', 'm.key.verification.start']))) {
return; // abort
}
if (!validateStart(payload)) {
@ -448,7 +476,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
await _sendKey();
break;
case 'm.key.verification.key':
if (!(await request.verifyLastStep(['m.key.verification.accept', 'm.key.verification.start']))) {
if (!(await request.verifyLastStep(
['m.key.verification.accept', 'm.key.verification.start']))) {
return;
}
_handleKey(payload);
@ -515,7 +544,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
if (payload['method'] != type) {
return false;
}
final possibleKeyAgreementProtocols = _intersect(KNOWN_KEY_AGREEMENT_PROTOCOLS, payload['key_agreement_protocols']);
final possibleKeyAgreementProtocols = _intersect(
KNOWN_KEY_AGREEMENT_PROTOCOLS, payload['key_agreement_protocols']);
if (possibleKeyAgreementProtocols.isEmpty) {
return false;
}
@ -525,12 +555,15 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
return false;
}
hash = possibleHashes.first;
final possibleMessageAuthenticationCodes = _intersect(KNOWN_MESSAGE_AUTHENTIFICATION_CODES, payload['message_authentication_codes']);
final possibleMessageAuthenticationCodes = _intersect(
KNOWN_MESSAGE_AUTHENTIFICATION_CODES,
payload['message_authentication_codes']);
if (possibleMessageAuthenticationCodes.isEmpty) {
return false;
}
messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
final possibleAuthenticationTypes = _intersect(KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
final possibleAuthenticationTypes = _intersect(
KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
if (possibleAuthenticationTypes.isEmpty) {
return false;
}
@ -553,7 +586,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
}
bool _handleAccept(Map<String, dynamic> payload) {
if (!KNOWN_KEY_AGREEMENT_PROTOCOLS.contains(payload['key_agreement_protocol'])) {
if (!KNOWN_KEY_AGREEMENT_PROTOCOLS
.contains(payload['key_agreement_protocol'])) {
return false;
}
keyAgreementProtocol = payload['key_agreement_protocol'];
@ -561,11 +595,13 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
return false;
}
hash = payload['hash'];
if (!KNOWN_MESSAGE_AUTHENTIFICATION_CODES.contains(payload['message_authentication_code'])) {
if (!KNOWN_MESSAGE_AUTHENTIFICATION_CODES
.contains(payload['message_authentication_code'])) {
return false;
}
messageAuthenticationCode = payload['message_authentication_code'];
final possibleAuthenticationTypes = _intersect(KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
final possibleAuthenticationTypes = _intersect(
KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
if (possibleAuthenticationTypes.isEmpty) {
return false;
}
@ -594,13 +630,23 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
Uint8List makeSas(int bytes) {
var sasInfo = '';
if (keyAgreementProtocol == 'curve25519-hkdf-sha256') {
final ourInfo = '${client.userID}|${client.deviceID}|${sas.get_pubkey()}|';
final theirInfo = '${request.userId}|${request.deviceId}|${theirPublicKey}|';
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' + (request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + request.transactionId;
final ourInfo =
'${client.userID}|${client.deviceID}|${sas.get_pubkey()}|';
final theirInfo =
'${request.userId}|${request.deviceId}|${theirPublicKey}|';
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' +
(request.startedVerification
? ourInfo + theirInfo
: theirInfo + ourInfo) +
request.transactionId;
} else if (keyAgreementProtocol == 'curve25519') {
final ourInfo = client.userID + client.deviceID;
final theirInfo = request.userId + request.deviceId;
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' + (request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + request.transactionId;
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' +
(request.startedVerification
? ourInfo + theirInfo
: theirInfo + ourInfo) +
request.transactionId;
} else {
throw 'Unknown key agreement protocol';
}
@ -609,9 +655,11 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
Future<void> _sendMac() async {
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
client.userID + client.deviceID +
request.userId + request.deviceId +
request.transactionId;
client.userID +
client.deviceID +
request.userId +
request.deviceId +
request.transactionId;
final mac = <String, String>{};
final keyList = <String>[];
@ -619,7 +667,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
// for now it is just our device key, once we have cross-signing
// we would also add the cross signing key here
final deviceKeyId = 'ed25519:${client.deviceID}';
mac[deviceKeyId] = _calculateMac(client.fingerprintKey, baseInfo + deviceKeyId);
mac[deviceKeyId] =
_calculateMac(client.fingerprintKey, baseInfo + deviceKeyId);
keyList.add(deviceKeyId);
keyList.sort();
@ -633,13 +682,16 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
Future<void> _processMac() async {
final payload = macPayload;
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
request.userId + request.deviceId +
client.userID + client.deviceID +
request.transactionId;
request.userId +
request.deviceId +
client.userID +
client.deviceID +
request.transactionId;
final keyList = payload['mac'].keys.toList();
keyList.sort();
if (payload['keys'] != _calculateMac(keyList.join(','), baseInfo + 'KEY_IDS')) {
if (payload['keys'] !=
_calculateMac(keyList.join(','), baseInfo + 'KEY_IDS')) {
await request.cancel('m.key_mismatch');
return;
}

View file

@ -13,11 +13,12 @@ class LinebreakSyntax extends InlineSyntax {
class SpoilerSyntax extends TagSyntax {
Map<String, String> reasonMap = <String, String>{};
SpoilerSyntax() : super(
r'\|\|(?:([^\|]+)\|(?!\|))?',
requiresDelimiterRun: true,
end: r'\|\|',
);
SpoilerSyntax()
: super(
r'\|\|(?:([^\|]+)\|(?!\|))?',
requiresDelimiterRun: true,
end: r'\|\|',
);
@override
bool onMatch(InlineParser parser, Match match) {
@ -31,7 +32,8 @@ class SpoilerSyntax extends TagSyntax {
@override
bool onMatchEnd(InlineParser parser, Match match, TagState state) {
final element = Element('span', state.children);
element.attributes['data-mx-spoiler'] = htmlEscape.convert(reasonMap[match.input] ?? '');
element.attributes['data-mx-spoiler'] =
htmlEscape.convert(reasonMap[match.input] ?? '');
parser.addNode(element);
return true;
}
@ -88,14 +90,33 @@ class PillSyntax extends InlineSyntax {
String markdown(String text, [Map<String, Map<String, String>> emotePacks]) {
emotePacks ??= <String, Map<String, String>>{};
var ret = markdownToHtml(text,
var ret = markdownToHtml(
text,
extensionSet: ExtensionSet.commonMark,
inlineSyntaxes: [StrikethroughSyntax(), LinebreakSyntax(), SpoilerSyntax(), EmoteSyntax(emotePacks), PillSyntax()],
inlineSyntaxes: [
StrikethroughSyntax(),
LinebreakSyntax(),
SpoilerSyntax(),
EmoteSyntax(emotePacks),
PillSyntax()
],
);
var stripPTags = '<p>'.allMatches(ret).length <= 1;
if (stripPTags) {
final otherBlockTags = ['table', 'pre', 'ol', 'ul', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'];
final otherBlockTags = [
'table',
'pre',
'ol',
'ul',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'blockquote'
];
for (final tag in otherBlockTags) {
// we check for the close tag as the opening one might have attributes
if (ret.contains('</${tag}>')) {

View file

@ -26,7 +26,8 @@ class RoomKeyRequest extends ToDeviceEvent {
var message = session.content;
message['forwarding_curve25519_key_chain'] = forwardedKeys;
message['session_key'] = session.inboundGroupSession.export_session(session.inboundGroupSession.first_known_index());
message['session_key'] = session.inboundGroupSession
.export_session(session.inboundGroupSession.first_known_index());
await client.sendToDevice(
[requestingDevice],
'm.forwarded_room_key',

View file

@ -20,9 +20,8 @@ class SessionKey {
SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key {
final parsedContent = Event.getMapFromPayload(dbEntry.content);
final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes);
content = parsedContent != null
? Map<String, dynamic>.from(parsedContent)
: null;
content =
parsedContent != null ? Map<String, dynamic>.from(parsedContent) : null;
indexes = parsedIndexes != null
? Map<String, int>.from(parsedIndexes)
: <String, int>{};

View file

@ -15,12 +15,15 @@ void main() {
},
};
test('simple markdown', () {
expect(markdown('hey *there* how are **you** doing?'), 'hey <em>there</em> how are <strong>you</strong> doing?');
expect(markdown('hey *there* how are **you** doing?'),
'hey <em>there</em> how are <strong>you</strong> doing?');
expect(markdown('wha ~~strike~~ works!'), 'wha <del>strike</del> works!');
});
test('spoilers', () {
expect(markdown('Snape killed ||Dumbledoor||'), 'Snape killed <span data-mx-spoiler="">Dumbledoor</span>');
expect(markdown('Snape killed ||Story|Dumbledoor||'), 'Snape killed <span data-mx-spoiler="Story">Dumbledoor</span>');
expect(markdown('Snape killed ||Dumbledoor||'),
'Snape killed <span data-mx-spoiler="">Dumbledoor</span>');
expect(markdown('Snape killed ||Story|Dumbledoor||'),
'Snape killed <span data-mx-spoiler="Story">Dumbledoor</span>');
});
test('multiple paragraphs', () {
expect(markdown('Heya!\n\nBeep'), '<p>Heya!</p>\n<p>Beep</p>');
@ -32,16 +35,22 @@ void main() {
expect(markdown('foxies\ncute'), 'foxies<br />\ncute');
});
test('emotes', () {
expect(markdown(':fox:', emotePacks), '<img src="mxc:&#47;&#47;roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':user~fox:', emotePacks), '<img src="mxc:&#47;&#47;userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':raccoon:', emotePacks), '<img src="mxc:&#47;&#47;raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
expect(markdown(':fox:', emotePacks),
'<img src="mxc:&#47;&#47;roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':user~fox:', emotePacks),
'<img src="mxc:&#47;&#47;userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':raccoon:', emotePacks),
'<img src="mxc:&#47;&#47;raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
expect(markdown(':invalid:', emotePacks), ':invalid:');
expect(markdown(':room~invalid:', emotePacks), ':room~invalid:');
});
test('pills', () {
expect(markdown('Hey @sorunome:sorunome.de!'), 'Hey <a href="https://matrix.to/#/@sorunome:sorunome.de">@sorunome:sorunome.de</a>!');
expect(markdown('#fox:sorunome.de: you all are awesome'), '<a href="https://matrix.to/#/#fox:sorunome.de">#fox:sorunome.de</a>: you all are awesome');
expect(markdown('!blah:example.org'), '<a href="https://matrix.to/#/!blah:example.org">!blah:example.org</a>');
expect(markdown('Hey @sorunome:sorunome.de!'),
'Hey <a href="https://matrix.to/#/@sorunome:sorunome.de">@sorunome:sorunome.de</a>!');
expect(markdown('#fox:sorunome.de: you all are awesome'),
'<a href="https://matrix.to/#/#fox:sorunome.de">#fox:sorunome.de</a>: you all are awesome');
expect(markdown('!blah:example.org'),
'<a href="https://matrix.to/#/!blah:example.org">!blah:example.org</a>');
});
});
}

View file

@ -26,7 +26,9 @@ import 'package:test/test.dart';
import 'package:famedlysdk/src/client.dart';
import 'package:famedlysdk/src/room.dart';
import 'package:famedlysdk/src/timeline.dart';
import 'package:famedlysdk/src/user.dart';
import 'package:famedlysdk/src/sync/event_update.dart';
import 'package:famedlysdk/src/sync/room_update.dart';
import 'fake_matrix_api.dart';
void main() {
@ -240,5 +242,18 @@ void main() {
expect(timeline.events[8].eventId, '1143273582443PhrSn:example.org');
expect(room.prev_batch, 't47409-4357353_219380_26003_2265');
});
test('Clear cache on limited timeline', () async {
client.onRoomUpdate.add(RoomUpdate(
id: roomID,
membership: Membership.join,
notification_count: 0,
highlight_count: 0,
limitedTimeline: true,
prev_batch: 'blah',
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events.isEmpty, true);
});
});
}

View file

@ -128,7 +128,8 @@ void test() async {
await Future.delayed(Duration(seconds: 5));
assert(room.outboundGroupSession != null);
var currentSessionIdA = room.outboundGroupSession.session_id();
assert(room.inboundGroupSessions.containsKey(room.outboundGroupSession.session_id()));
assert(room.inboundGroupSessions
.containsKey(room.outboundGroupSession.session_id()));
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
@ -236,7 +237,8 @@ void test() async {
assert(restoredRoom.outboundGroupSession.session_id() ==
room.outboundGroupSession.session_id());
assert(restoredRoom.inboundGroupSessions.length == 4);
assert(restoredRoom.inboundGroupSessions.length == room.inboundGroupSessions.length);
assert(restoredRoom.inboundGroupSessions.length ==
room.inboundGroupSessions.length);
for (var i = 0; i < restoredRoom.inboundGroupSessions.length; i++) {
assert(restoredRoom.inboundGroupSessions.keys.toList()[i] ==
room.inboundGroupSessions.keys.toList()[i]);