fix: Cleanup nullsafe encryption a bit

This commit is contained in:
Nicolas Werner 2021-09-27 12:17:35 +02:00
parent da80658c09
commit 1c838e3be8
9 changed files with 132 additions and 162 deletions

View File

@ -149,8 +149,7 @@ class Encryption {
runInRoot(() => keyVerificationManager.handleEventUpdate(update)); runInRoot(() => keyVerificationManager.handleEventUpdate(update));
} }
if (update.content['sender'] == client.userID && if (update.content['sender'] == client.userID &&
(!update.content.containsKey('unsigned') || update.content['unsigned']?['transaction_id'] == null) {
!update.content['unsigned'].containsKey('transaction_id'))) {
// maybe we need to re-try SSSS secrets // maybe we need to re-try SSSS secrets
// ignore: unawaited_futures // ignore: unawaited_futures
runInRoot(() => ssss.periodicallyRequestMissingCache()); runInRoot(() => ssss.periodicallyRequestMissingCache());
@ -375,15 +374,13 @@ class Encryption {
Future<void> autovalidateMasterOwnKey() async { Future<void> autovalidateMasterOwnKey() async {
// check if we can set our own master key as verified, if it isn't yet // check if we can set our own master key as verified, if it isn't yet
final masterKey = client.userDeviceKeys[client.userID]?.masterKey;
if (client.database != null && if (client.database != null &&
client.userDeviceKeys.containsKey(client.userID)) { masterKey != null &&
final masterKey = client.userDeviceKeys[client.userID]!.masterKey; !masterKey.directVerified &&
if (masterKey != null && masterKey
!masterKey.directVerified && .hasValidSignatureChain(onlyValidateUserIds: {client.userID})) {
masterKey await masterKey.setVerified(true);
.hasValidSignatureChain(onlyValidateUserIds: {client.userID})) {
await masterKey.setVerified(true);
}
} }
} }

View File

@ -146,10 +146,10 @@ class KeyManager {
newSession.dispose(); newSession.dispose();
return; return;
} }
if (!_inboundGroupSessions.containsKey(roomId)) {
_inboundGroupSessions[roomId] = <String, SessionKey>{}; final roomInboundGroupSessions =
} _inboundGroupSessions[roomId] ??= <String, SessionKey>{};
_inboundGroupSessions[roomId]![sessionId] = newSession; roomInboundGroupSessions[sessionId] = newSession;
if (!client.isLogged() || client.encryption == null) { if (!client.isLogged() || client.encryption == null) {
return; return;
} }
@ -189,9 +189,8 @@ class KeyManager {
SessionKey? getInboundGroupSession( SessionKey? getInboundGroupSession(
String roomId, String sessionId, String senderKey, String roomId, String sessionId, String senderKey,
{bool otherRooms = true}) { {bool otherRooms = true}) {
if (_inboundGroupSessions.containsKey(roomId) && final sess = _inboundGroupSessions[roomId]?[sessionId];
_inboundGroupSessions[roomId]!.containsKey(sessionId)) { if (sess != null) {
final sess = _inboundGroupSessions[roomId]![sessionId]!;
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) { if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
return null; return null;
} }
@ -202,8 +201,8 @@ class KeyManager {
} }
// search if this session id is *somehow* found in another room // search if this session id is *somehow* found in another room
for (final val in _inboundGroupSessions.values) { for (final val in _inboundGroupSessions.values) {
if (val.containsKey(sessionId)) { final sess = val[sessionId];
final sess = val[sessionId]!; if (sess != null) {
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) { if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
return null; return null;
} }
@ -231,9 +230,8 @@ class KeyManager {
/// Loads an inbound group session /// Loads an inbound group session
Future<SessionKey?> loadInboundGroupSession( Future<SessionKey?> loadInboundGroupSession(
String roomId, String sessionId, String senderKey) async { String roomId, String sessionId, String senderKey) async {
if (_inboundGroupSessions.containsKey(roomId) && final sess = _inboundGroupSessions[roomId]?[sessionId];
_inboundGroupSessions[roomId]!.containsKey(sessionId)) { if (sess != null) {
final sess = _inboundGroupSessions[roomId]![sessionId]!;
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) { if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
return null; // sender keys do not match....better not do anything return null; // sender keys do not match....better not do anything
} }
@ -244,16 +242,15 @@ class KeyManager {
if (session == null) { if (session == null) {
return null; return null;
} }
final sess = SessionKey.fromDb(session, client.userID); final dbSess = SessionKey.fromDb(session, client.userID);
if (!_inboundGroupSessions.containsKey(roomId)) { final roomInboundGroupSessions =
_inboundGroupSessions[roomId] = <String, SessionKey>{}; _inboundGroupSessions[roomId] ??= <String, SessionKey>{};
} if (!dbSess.isValid ||
if (!sess.isValid || dbSess.senderKey.isEmpty ||
sess.senderKey.isEmpty || dbSess.senderKey != senderKey) {
sess.senderKey != senderKey) {
return null; return null;
} }
_inboundGroupSessions[roomId]![sessionId] = sess; roomInboundGroupSessions[sessionId] = dbSess;
return sess; return sess;
} }
@ -261,14 +258,13 @@ class KeyManager {
List<DeviceKeys> deviceKeys) { List<DeviceKeys> deviceKeys) {
final deviceKeyIds = <String, Map<String, bool>>{}; final deviceKeyIds = <String, Map<String, bool>>{};
for (final device in deviceKeys) { for (final device in deviceKeys) {
if (device.deviceId == null) { final deviceId = device.deviceId;
if (deviceId == null) {
Logs().w('[KeyManager] ignoring device without deviceid'); Logs().w('[KeyManager] ignoring device without deviceid');
continue; continue;
} }
if (!deviceKeyIds.containsKey(device.userId)) { final userDeviceKeyIds = deviceKeyIds[device.userId] ??= <String, bool>{};
deviceKeyIds[device.userId] = <String, bool>{}; userDeviceKeyIds[deviceId] = !device.encryptToDevice;
}
deviceKeyIds[device.userId]![device.deviceId!] = !device.encryptToDevice;
} }
return deviceKeyIds; return deviceKeyIds;
} }
@ -440,8 +436,9 @@ class KeyManager {
/// Creates an outbound group session for a given room id /// Creates an outbound group session for a given room id
Future<OutboundGroupSession> createOutboundGroupSession(String roomId) async { Future<OutboundGroupSession> createOutboundGroupSession(String roomId) async {
if (_pendingNewOutboundGroupSessions.containsKey(roomId)) { final sess = _pendingNewOutboundGroupSessions[roomId];
return _pendingNewOutboundGroupSessions[roomId]!; if (sess != null) {
return sess;
} }
_pendingNewOutboundGroupSessions[roomId] = _pendingNewOutboundGroupSessions[roomId] =
_createOutboundGroupSession(roomId); _createOutboundGroupSession(roomId);
@ -791,14 +788,12 @@ class KeyManager {
Logs().i('[KeyManager] No body, doing nothing'); Logs().i('[KeyManager] No body, doing nothing');
return; // no body return; // no body
} }
if (!client.userDeviceKeys.containsKey(event.sender) || final device = client.userDeviceKeys[event.sender]
!client.userDeviceKeys[event.sender]!.deviceKeys ?.deviceKeys[event.content['requesting_device_id']];
.containsKey(event.content['requesting_device_id'])) { if (device == null) {
Logs().i('[KeyManager] Device not found, doing nothing'); Logs().i('[KeyManager] Device not found, doing nothing');
return; // device not found return; // device not found
} }
final device = client.userDeviceKeys[event.sender]!
.deviceKeys[event.content['requesting_device_id']]!;
if (device.userId == client.userID && if (device.userId == client.userID &&
device.deviceId == client.deviceID) { device.deviceId == client.deviceID) {
Logs().i('[KeyManager] Request is by ourself, ignoring'); Logs().i('[KeyManager] Request is by ourself, ignoring');
@ -914,10 +909,8 @@ class KeyManager {
}; };
final data = <String, Map<String, Map<String, dynamic>>>{}; final data = <String, Map<String, Map<String, dynamic>>>{};
for (final device in request.devices) { for (final device in request.devices) {
if (!data.containsKey(device.userId)) { final userData = data[device.userId] ??= {};
data[device.userId] = {}; userData[device.deviceId!] = sendToDeviceMessage;
}
data[device.userId]![device.deviceId!] = sendToDeviceMessage;
} }
await client.sendToDevice( await client.sendToDevice(
EventTypes.RoomKeyRequest, EventTypes.RoomKeyRequest,
@ -933,13 +926,10 @@ class KeyManager {
} }
final String roomId = event.content['room_id']; final String roomId = event.content['room_id'];
final String sessionId = event.content['session_id']; final String sessionId = event.content['session_id'];
if (client.userDeviceKeys.containsKey(event.sender) && final sender_ed25519 = client.userDeviceKeys[event.sender]
client.userDeviceKeys[event.sender]!.deviceKeys ?.deviceKeys[event.content['requesting_device_id']]?.ed25519Key;
.containsKey(event.content['requesting_device_id'])) { if (sender_ed25519 != null) {
event.content['sender_claimed_ed25519_key'] = client event.content['sender_claimed_ed25519_key'] = sender_ed25519;
.userDeviceKeys[event.sender]!
.deviceKeys[event.content['requesting_device_id']]!
.ed25519Key;
} }
Logs().v('[KeyManager] Keeping room key'); Logs().v('[KeyManager] Keeping room key');
setInboundGroupSession(roomId, sessionId, setInboundGroupSession(roomId, sessionId,
@ -1044,9 +1034,8 @@ RoomKeys _generateUploadKeys(_GenerateUploadKeysArgs args) {
continue; continue;
} }
// create the room if it doesn't exist // create the room if it doesn't exist
if (!roomKeys.rooms.containsKey(sess.roomId)) { final roomKeyBackup =
roomKeys.rooms[sess.roomId] = RoomKeyBackup(sessions: {}); roomKeys.rooms[sess.roomId] ??= RoomKeyBackup(sessions: {});
}
// generate the encrypted content // generate the encrypted content
final payload = <String, dynamic>{ final payload = <String, dynamic>{
'algorithm': AlgorithmTypes.megolmV1AesSha2, 'algorithm': AlgorithmTypes.megolmV1AesSha2,
@ -1061,7 +1050,7 @@ RoomKeys _generateUploadKeys(_GenerateUploadKeysArgs args) {
// fetch the device, if available... // fetch the device, if available...
//final device = args.client.getUserDeviceKeysByCurve25519Key(sess.senderKey); //final device = args.client.getUserDeviceKeysByCurve25519Key(sess.senderKey);
// aaaand finally add the session key to our payload // aaaand finally add the session key to our payload
roomKeys.rooms[sess.roomId]!.sessions[sess.sessionId] = KeyBackupData( roomKeyBackup.sessions[sess.sessionId] = KeyBackupData(
firstMessageIndex: sess.inboundGroupSession!.first_known_index(), firstMessageIndex: sess.inboundGroupSession!.first_known_index(),
forwardedCount: sess.forwardingCurve25519KeyChain.length, forwardedCount: sess.forwardingCurve25519KeyChain.length,
isVerified: dbSession.verified, //device?.verified ?? false, isVerified: dbSession.verified, //device?.verified ?? false,

View File

@ -411,12 +411,10 @@ class OlmManager {
} }
} }
final Map<String, dynamic> plainContent = json.decode(plaintext); final Map<String, dynamic> plainContent = json.decode(plaintext);
if (plainContent.containsKey('sender') && if (plainContent['sender'] != event.sender) {
plainContent['sender'] != event.sender) {
throw DecryptException(DecryptException.senderDoesntMatch); throw DecryptException(DecryptException.senderDoesntMatch);
} }
if (plainContent.containsKey('recipient') && if (plainContent['recipient'] != client.userID) {
plainContent['recipient'] != client.userID) {
throw DecryptException(DecryptException.recipientDoesntMatch); throw DecryptException(DecryptException.recipientDoesntMatch);
} }
if (plainContent['recipient_keys'] is Map && if (plainContent['recipient_keys'] is Map &&
@ -637,19 +635,16 @@ class OlmManager {
} }
final deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys); final deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) => deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) =>
olmSessions.containsKey(deviceKeys.curve25519Key) && olmSessions[deviceKeys.curve25519Key]?.isNotEmpty ?? false);
olmSessions[deviceKeys.curve25519Key]!.isNotEmpty);
if (deviceKeysWithoutSession.isNotEmpty) { if (deviceKeysWithoutSession.isNotEmpty) {
await startOutgoingOlmSessions(deviceKeysWithoutSession); await startOutgoingOlmSessions(deviceKeysWithoutSession);
} }
for (final device in deviceKeys) { for (final device in deviceKeys) {
if (!data.containsKey(device.userId)) { final userData = data[device.userId] ??= {};
data[device.userId] = {};
}
try { try {
data[device.userId]![device.deviceId!] = userData[device.deviceId!] = await encryptToDeviceMessagePayload(
await encryptToDeviceMessagePayload(device, type, payload, device, type, payload,
getFromDb: false); getFromDb: false);
} catch (e, s) { } catch (e, s) {
Logs().w('[LibOlm] Error encrypting to-device event', e, s); Logs().w('[LibOlm] Error encrypting to-device event', e, s);
continue; continue;

View File

@ -90,10 +90,11 @@ class Bootstrap {
// cache the secret analyzing so that we don't drop stuff a different client sets during bootstrapping // cache the secret analyzing so that we don't drop stuff a different client sets during bootstrapping
Map<String, Set<String>>? _secretsCache; Map<String, Set<String>>? _secretsCache;
Map<String, Set<String>> analyzeSecrets() { Map<String, Set<String>> analyzeSecrets() {
if (_secretsCache != null) { final secretsCache = _secretsCache;
if (secretsCache != null) {
// deep-copy so that we can do modifications // deep-copy so that we can do modifications
final newSecrets = <String, Set<String>>{}; final newSecrets = <String, Set<String>>{};
for (final s in _secretsCache!.entries) { for (final s in secretsCache.entries) {
newSecrets[s.key] = Set<String>.from(s.value); newSecrets[s.key] = Set<String>.from(s.value);
} }
return newSecrets; return newSecrets;
@ -147,11 +148,7 @@ class Bootstrap {
final usage = <String, int>{}; final usage = <String, int>{};
for (final keys in secrets.values) { for (final keys in secrets.values) {
for (final key in keys) { for (final key in keys) {
if (!usage.containsKey(key)) { usage.update(key, (i) => i++, ifAbsent: () => 1);
usage[key] = 1;
} else {
usage[key] = usage[key]! + 1;
}
} }
} }
final entriesList = usage.entries.toList(); final entriesList = usage.entries.toList();
@ -315,14 +312,15 @@ class Bootstrap {
} }
Future<void> openExistingSsss() async { Future<void> openExistingSsss() async {
if (state != BootstrapState.openExistingSsss || newSsssKey == null) { final newSsssKey_ = newSsssKey;
if (state != BootstrapState.openExistingSsss || newSsssKey_ == null) {
throw BootstrapBadStateException(); throw BootstrapBadStateException();
} }
if (!newSsssKey!.isUnlocked) { if (!newSsssKey_.isUnlocked) {
throw BootstrapBadStateException('Key not unlocked'); throw BootstrapBadStateException('Key not unlocked');
} }
Logs().v('Maybe cache all...'); Logs().v('Maybe cache all...');
await newSsssKey!.maybeCacheAll(); await newSsssKey_.maybeCacheAll();
checkCrossSigning(); checkCrossSigning();
} }
@ -477,12 +475,9 @@ class Bootstrap {
futures.add( futures.add(
client.onSync.stream client.onSync.stream
.firstWhere((syncUpdate) => .firstWhere((syncUpdate) =>
client.userDeviceKeys.containsKey(client.userID) && masterKey?.publicKey != null &&
client.userDeviceKeys[client.userID]!.masterKey != null && client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key ==
client.userDeviceKeys[client.userID]!.masterKey!.ed25519Key != masterKey?.publicKey)
null &&
client.userDeviceKeys[client.userID]!.masterKey!.ed25519Key ==
masterKey!.publicKey)
.then((_) => Logs().v('New Master Key was created')), .then((_) => Logs().v('New Master Key was created')),
); );
} }
@ -595,8 +590,10 @@ class Bootstrap {
if (state != BootstrapState.error) { if (state != BootstrapState.error) {
_state = newState; _state = newState;
} }
if (onUpdate != null) {
onUpdate!(); final onUpdate_ = onUpdate;
if (onUpdate_ != null) {
onUpdate_();
} }
} }
} }

View File

@ -227,10 +227,9 @@ class KeyVerification {
if (deviceId == '*') { if (deviceId == '*') {
_deviceId = payload['from_device']; // gotta set the real device id _deviceId = payload['from_device']; // gotta set the real device id
// and broadcast the cancel to the other devices // and broadcast the cancel to the other devices
final devices = client.userDeviceKeys.containsKey(userId) final devices = List<DeviceKeys>.from(
? List<DeviceKeys>.from( client.userDeviceKeys[userId]?.deviceKeys.values ??
client.userDeviceKeys[userId]!.deviceKeys.values) Iterable.empty());
: List<DeviceKeys>.from([]);
devices.removeWhere( devices.removeWhere(
(d) => {deviceId, client.deviceID}.contains(d.deviceId)); (d) => {deviceId, client.deviceID}.contains(d.deviceId));
final cancelPayload = <String, dynamic>{ final cancelPayload = <String, dynamic>{
@ -472,7 +471,8 @@ class KeyVerification {
Future<bool> Function(String, SignableKey) verifier) async { Future<bool> Function(String, SignableKey) verifier) async {
_verifiedDevices = <SignableKey>[]; _verifiedDevices = <SignableKey>[];
if (!client.userDeviceKeys.containsKey(userId)) { final userDeviceKey = client.userDeviceKeys[userId];
if (userDeviceKey == null) {
await cancel('m.key_mismatch'); await cancel('m.key_mismatch');
return; return;
} }
@ -480,7 +480,7 @@ class KeyVerification {
final keyId = entry.key; final keyId = entry.key;
final verifyDeviceId = keyId.substring('ed25519:'.length); final verifyDeviceId = keyId.substring('ed25519:'.length);
final keyInfo = entry.value; final keyInfo = entry.value;
final key = client.userDeviceKeys[userId]!.getKey(verifyDeviceId); final key = userDeviceKey.getKey(verifyDeviceId);
if (key != null) { if (key != null) {
if (!(await verifier(keyInfo, key))) { if (!(await verifier(keyInfo, key))) {
await cancel('m.key_mismatch'); await cancel('m.key_mismatch');
@ -619,8 +619,10 @@ class KeyVerification {
if (state != KeyVerificationState.error) { if (state != KeyVerificationState.error) {
state = newState; state = newState;
} }
final onUpdate = this.onUpdate;
if (onUpdate != null) { if (onUpdate != null) {
onUpdate!(); onUpdate();
} }
} }
} }
@ -808,8 +810,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
} }
Future<void> _sendAccept() async { Future<void> _sendAccept() async {
sas = olm.SAS(); final sas = this.sas ??= olm.SAS();
commitment = _makeCommitment(sas!.get_pubkey(), startCanonicalJson); commitment = _makeCommitment(sas.get_pubkey(), startCanonicalJson);
await request.send(EventTypes.KeyVerificationAccept, { await request.send(EventTypes.KeyVerificationAccept, {
'method': type, 'method': type,
'key_agreement_protocol': keyAgreementProtocol, 'key_agreement_protocol': keyAgreementProtocol,
@ -906,9 +908,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
_calculateMac(encryption.fingerprintKey!, baseInfo + deviceKeyId); _calculateMac(encryption.fingerprintKey!, baseInfo + deviceKeyId);
keyList.add(deviceKeyId); keyList.add(deviceKeyId);
final masterKey = client.userDeviceKeys.containsKey(client.userID) final masterKey = client.userDeviceKeys[client.userID]?.masterKey;
? client.userDeviceKeys[client.userID]!.masterKey
: null;
if (masterKey != null && masterKey.verified) { if (masterKey != null && masterKey.verified) {
// we have our own master key verified, let's send it! // we have our own master key verified, let's send it!
final masterKeyId = 'ed25519:${masterKey.publicKey}'; final masterKeyId = 'ed25519:${masterKey.publicKey}';

View File

@ -49,7 +49,7 @@ class SessionKey {
late Map<String, String> senderClaimedKeys; late Map<String, String> senderClaimedKeys;
/// Sender curve25519 key /// Sender curve25519 key
late String senderKey; String senderKey;
/// Is this session valid? /// Is this session valid?
bool get isValid => inboundGroupSession != null; bool get isValid => inboundGroupSession != null;

View File

@ -98,10 +98,10 @@ class Event extends MatrixEvent {
this.roomId = roomId ?? room?.id; this.roomId = roomId ?? room?.id;
this.senderId = senderId; this.senderId = senderId;
this.unsigned = unsigned; this.unsigned = unsigned;
// synapse unfortunatley isn't following the spec and tosses the prev_content // synapse unfortunatley isn't following the spec and tosses the prev_content
// into the unsigned block. // into the unsigned block.
// Currently we are facing a very strange bug in web which is impossible to debug. // Currently we are facing a very strange bug in web which is impossible to debug.
// It may be because of this line so we put this in try-catch until we can fix it. // It may be because of this line so we put this in try-catch until we can fix it.
try { try {
this.prevContent = (prevContent != null && prevContent.isNotEmpty) this.prevContent = (prevContent != null && prevContent.isNotEmpty)
? prevContent ? prevContent
@ -111,21 +111,21 @@ class Event extends MatrixEvent {
? unsigned['prev_content'] ? unsigned['prev_content']
: null; : null;
} catch (_) { } catch (_) {
// A strange bug in dart web makes this crash // A strange bug in dart web makes this crash
} }
this.stateKey = stateKey; this.stateKey = stateKey;
this.originServerTs = originServerTs; this.originServerTs = originServerTs;
// Mark event as failed to send if status is `sending` and event is older // Mark event as failed to send if status is `sending` and event is older
// than the timeout. This should not happen with the deprecated Moor // than the timeout. This should not happen with the deprecated Moor
// database! // database!
if (status == 0 && room?.client?.database != null) { if (status == 0 && room?.client?.database != null) {
// Age of this event in milliseconds // Age of this event in milliseconds
final age = DateTime.now().millisecondsSinceEpoch - final age = DateTime.now().millisecondsSinceEpoch -
originServerTs.millisecondsSinceEpoch; originServerTs.millisecondsSinceEpoch;
if (age > room.client.sendMessageTimeoutSeconds * 1000) { if (age > room.client.sendMessageTimeoutSeconds * 1000) {
// Update this event in database and open timelines // Update this event in database and open timelines
final json = toJson(); final json = toJson();
json['unsigned'] ??= <String, dynamic>{}; json['unsigned'] ??= <String, dynamic>{};
json['unsigned'][messageSendingStatusKey] = -1; json['unsigned'][messageSendingStatusKey] = -1;
@ -324,8 +324,8 @@ class Event extends MatrixEvent {
/// Try to send this event again. Only works with events of status -1. /// Try to send this event again. Only works with events of status -1.
Future<String> sendAgain({String txid}) async { Future<String> sendAgain({String txid}) async {
if (status != -1) return null; if (status != -1) return null;
// we do not remove the event here. It will automatically be updated // we do not remove the event here. It will automatically be updated
// in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2 // in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2
final newEventId = await room.sendEvent( final newEventId = await room.sendEvent(
content, content,
txid: txid ?? unsigned['transaction_id'] ?? eventId, txid: txid ?? unsigned['transaction_id'] ?? eventId,
@ -420,7 +420,7 @@ class Event extends MatrixEvent {
return getThumbnail ? thumbnailMxcUrl : attachmentMxcUrl; return getThumbnail ? thumbnailMxcUrl : attachmentMxcUrl;
} }
// size determined from an approximate 800x800 jpeg thumbnail with method=scale // size determined from an approximate 800x800 jpeg thumbnail with method=scale
static const _minNoThumbSize = 80 * 1024; static const _minNoThumbSize = 80 * 1024;
/// Gets the attachment https URL to display in the timeline, taking into account if the original image is tiny. /// Gets the attachment https URL to display in the timeline, taking into account if the original image is tiny.
@ -449,14 +449,14 @@ class Event extends MatrixEvent {
final thisInfoMap = useThumbnailMxcUrl ? thumbnailInfoMap : infoMap; final thisInfoMap = useThumbnailMxcUrl ? thumbnailInfoMap : infoMap;
final thisMxcUrl = final thisMxcUrl =
useThumbnailMxcUrl ? infoMap['thumbnail_url'] : content['url']; useThumbnailMxcUrl ? infoMap['thumbnail_url'] : content['url'];
// if we have as method scale, we can return safely the original image, should it be small enough // if we have as method scale, we can return safely the original image, should it be small enough
if (getThumbnail && if (getThumbnail &&
method == ThumbnailMethod.scale && method == ThumbnailMethod.scale &&
thisInfoMap['size'] is int && thisInfoMap['size'] is int &&
thisInfoMap['size'] < minNoThumbSize) { thisInfoMap['size'] < minNoThumbSize) {
getThumbnail = false; getThumbnail = false;
} }
// now generate the actual URLs // now generate the actual URLs
if (getThumbnail) { if (getThumbnail) {
return Uri.parse(thisMxcUrl).getThumbnail( return Uri.parse(thisMxcUrl).getThumbnail(
room.client, room.client,
@ -480,7 +480,7 @@ class Event extends MatrixEvent {
throw "This event hasn't any attachment or thumbnail."; throw "This event hasn't any attachment or thumbnail.";
} }
getThumbnail = mxcUrl != attachmentMxcUrl; getThumbnail = mxcUrl != attachmentMxcUrl;
// Is this file storeable? // Is this file storeable?
final thisInfoMap = getThumbnail ? thumbnailInfoMap : infoMap; final thisInfoMap = getThumbnail ? thumbnailInfoMap : infoMap;
final storeable = room.client.database != null && final storeable = room.client.database != null &&
thisInfoMap['size'] is int && thisInfoMap['size'] is int &&
@ -517,7 +517,7 @@ class Event extends MatrixEvent {
Uint8List uint8list; Uint8List uint8list;
// Is this file storeable? // Is this file storeable?
final thisInfoMap = getThumbnail ? thumbnailInfoMap : infoMap; final thisInfoMap = getThumbnail ? thumbnailInfoMap : infoMap;
var storeable = room.client.database != null && var storeable = room.client.database != null &&
thisInfoMap['size'] is int && thisInfoMap['size'] is int &&
@ -527,7 +527,7 @@ class Event extends MatrixEvent {
uint8list = await room.client.database.getFile(mxcUrl); uint8list = await room.client.database.getFile(mxcUrl);
} }
// Download the file // Download the file
if (uint8list == null) { if (uint8list == null) {
downloadCallback ??= (Uri url) async { downloadCallback ??= (Uri url) async {
return (await http.get(url)).bodyBytes; return (await http.get(url)).bodyBytes;
@ -541,7 +541,7 @@ class Event extends MatrixEvent {
} }
} }
// Decrypt the file // Decrypt the file
if (isEncrypted) { if (isEncrypted) {
final fileMap = final fileMap =
getThumbnail ? infoMap['thumbnail_file'] : content['file']; getThumbnail ? infoMap['thumbnail_file'] : content['file'];
@ -579,10 +579,10 @@ class Event extends MatrixEvent {
} }
var body = plaintextBody ? this.plaintextBody : this.body; var body = plaintextBody ? this.plaintextBody : this.body;
// we need to know if the message is an html message to be able to determine // we need to know if the message is an html message to be able to determine
// if we need to strip the reply fallback. // if we need to strip the reply fallback.
var htmlMessage = content['format'] != 'org.matrix.custom.html'; var htmlMessage = content['format'] != 'org.matrix.custom.html';
// If we have an edit, we want to operate on the new content // If we have an edit, we want to operate on the new content
if (hideEdit && if (hideEdit &&
relationshipType == RelationshipTypes.edit && relationshipType == RelationshipTypes.edit &&
content.tryGet<Map<String, dynamic>>('m.new_content') != null) { content.tryGet<Map<String, dynamic>>('m.new_content') != null) {
@ -600,9 +600,9 @@ class Event extends MatrixEvent {
body; body;
} }
} }
// Hide reply fallback // Hide reply fallback
// Be sure that the plaintextBody already stripped teh reply fallback, // Be sure that the plaintextBody already stripped teh reply fallback,
// if the message is formatted // if the message is formatted
if (hideReply && (!plaintextBody || htmlMessage)) { if (hideReply && (!plaintextBody || htmlMessage)) {
body = body.replaceFirst( body = body.replaceFirst(
RegExp(r'^>( \*)? <[^>]+>[^\n\r]+\r?\n(> [^\n]*\r?\n)*\r?\n'), ''); RegExp(r'^>( \*)? <[^>]+>[^\n\r]+\r?\n(> [^\n]*\r?\n)*\r?\n'), '');
@ -613,7 +613,7 @@ class Event extends MatrixEvent {
localizedBody = callback(this, i18n, body); localizedBody = callback(this, i18n, body);
} }
// Add the sender name prefix // Add the sender name prefix
if (withSenderNamePrefix && if (withSenderNamePrefix &&
type == EventTypes.Message && type == EventTypes.Message &&
textOnlyMessageTypes.contains(messageType)) { textOnlyMessageTypes.contains(messageType)) {
@ -691,13 +691,13 @@ class Event extends MatrixEvent {
return this; return this;
} }
if (hasAggregatedEvents(timeline, RelationshipTypes.edit)) { if (hasAggregatedEvents(timeline, RelationshipTypes.edit)) {
// alright, we have an edit // alright, we have an edit
final allEditEvents = aggregatedEvents(timeline, RelationshipTypes.edit) final allEditEvents = aggregatedEvents(timeline, RelationshipTypes.edit)
// we only allow edits made by the original author themself // we only allow edits made by the original author themself
.where((e) => e.senderId == senderId && e.type == EventTypes.Message) .where((e) => e.senderId == senderId && e.type == EventTypes.Message)
.toList(); .toList();
// we need to check again if it isn't empty, as we potentially removed all // we need to check again if it isn't empty, as we potentially removed all
// aggregated edits // aggregated edits
if (allEditEvents.isNotEmpty) { if (allEditEvents.isNotEmpty) {
allEditEvents.sort((a, b) => a.originServerTs.millisecondsSinceEpoch - allEditEvents.sort((a, b) => a.originServerTs.millisecondsSinceEpoch -
b.originServerTs.millisecondsSinceEpoch > b.originServerTs.millisecondsSinceEpoch >
@ -705,7 +705,7 @@ class Event extends MatrixEvent {
? 1 ? 1
: -1); : -1);
final rawEvent = allEditEvents.last.toJson(); final rawEvent = allEditEvents.last.toJson();
// update the content of the new event to render // update the content of the new event to render
if (rawEvent['content']['m.new_content'] is Map) { if (rawEvent['content']['m.new_content'] is Map) {
rawEvent['content'] = rawEvent['content']['m.new_content']; rawEvent['content'] = rawEvent['content']['m.new_content'];
} }
@ -720,16 +720,16 @@ class Event extends MatrixEvent {
content['format'] == 'org.matrix.custom.html' && content['format'] == 'org.matrix.custom.html' &&
content['formatted_body'] is String; content['formatted_body'] is String;
// regexes to fetch the number of emotes, including emoji, and if the message consists of only those // regexes to fetch the number of emotes, including emoji, and if the message consists of only those
// to match an emoji we can use the following regex: // to match an emoji we can use the following regex:
// (?:\x{00a9}|\x{00ae}|[\x{2600}-\x{27bf}]|[\x{2b00}-\x{2bff}]|\x{d83c}[\x{d000}-\x{dfff}]|\x{d83d}[\x{d000}-\x{dfff}]|\x{d83e}[\x{d000}-\x{dfff}])[\x{fe00}-\x{fe0f}]? // (?:\x{00a9}|\x{00ae}|[\x{2600}-\x{27bf}]|[\x{2b00}-\x{2bff}]|\x{d83c}[\x{d000}-\x{dfff}]|\x{d83d}[\x{d000}-\x{dfff}]|\x{d83e}[\x{d000}-\x{dfff}])[\x{fe00}-\x{fe0f}]?
// we need to replace \x{0000} with \u0000, the comment is left in the other format to be able to paste into regex101.com // we need to replace \x{0000} with \u0000, the comment is left in the other format to be able to paste into regex101.com
// to see if there is a custom emote, we use the following regex: <img[^>]+data-mx-(?:emote|emoticon)(?==|>|\s)[^>]*> // to see if there is a custom emote, we use the following regex: <img[^>]+data-mx-(?:emote|emoticon)(?==|>|\s)[^>]*>
// now we combind the two to have four regexes: // now we combind the two to have four regexes:
// 1. are there only emoji, or whitespace // 1. are there only emoji, or whitespace
// 2. are there only emoji, emotes, or whitespace // 2. are there only emoji, emotes, or whitespace
// 3. count number of emoji // 3. count number of emoji
// 4- count number of emoji or emotes // 4- count number of emoji or emotes
static final RegExp _onlyEmojiRegex = RegExp( static final RegExp _onlyEmojiRegex = RegExp(
r'^((?:\u00a9|\u00ae|[\u2600-\u27bf]|[\u2b00-\u2bff]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])[\ufe00-\ufe0f]?|\s)*$', r'^((?:\u00a9|\u00ae|[\u2600-\u27bf]|[\u2b00-\u2bff]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])[\ufe00-\ufe0f]?|\s)*$',
caseSensitive: false, caseSensitive: false,

View File

@ -249,14 +249,9 @@ abstract class SignableKey extends MatrixSignableKey {
if (otherUserId == userId && keyId == identifier) { if (otherUserId == userId && keyId == identifier) {
continue; continue;
} }
SignableKey? key;
if (client.userDeviceKeys[otherUserId]!.deviceKeys.containsKey(keyId)) {
key = client.userDeviceKeys[otherUserId]!.deviceKeys[keyId];
} else if (client.userDeviceKeys[otherUserId]!.crossSigningKeys
.containsKey(keyId)) {
key = client.userDeviceKeys[otherUserId]!.crossSigningKeys[keyId];
}
final key = client.userDeviceKeys[otherUserId]?.deviceKeys[keyId] ??
client.userDeviceKeys[otherUserId]?.crossSigningKeys[keyId];
if (key == null) { if (key == null) {
continue; continue;
} }
@ -272,25 +267,22 @@ abstract class SignableKey extends MatrixSignableKey {
} }
var haveValidSignature = false; var haveValidSignature = false;
var gotSignatureFromCache = false; var gotSignatureFromCache = false;
if (validSignatures != null && if (validSignatures?[otherUserId][fullKeyId] == true) {
validSignatures!.containsKey(otherUserId) && haveValidSignature = true;
validSignatures![otherUserId].containsKey(fullKeyId)) { gotSignatureFromCache = true;
if (validSignatures![otherUserId][fullKeyId] == true) { } else if (validSignatures?[otherUserId][fullKeyId] == false) {
haveValidSignature = true; haveValidSignature = false;
gotSignatureFromCache = true; gotSignatureFromCache = true;
} else if (validSignatures![otherUserId][fullKeyId] == false) {
haveValidSignature = false;
gotSignatureFromCache = true;
}
} }
if (!gotSignatureFromCache && key.ed25519Key != null) { if (!gotSignatureFromCache && key.ed25519Key != null) {
// validate the signature manually // validate the signature manually
haveValidSignature = _verifySignature(key.ed25519Key!, signature); haveValidSignature = _verifySignature(key.ed25519Key!, signature);
validSignatures ??= <String, dynamic>{}; final validSignatures = this.validSignatures ??= <String, dynamic>{};
if (!validSignatures!.containsKey(otherUserId)) { if (!validSignatures.containsKey(otherUserId)) {
validSignatures![otherUserId] = <String, dynamic>{}; validSignatures[otherUserId] = <String, dynamic>{};
} }
validSignatures![otherUserId][fullKeyId] = haveValidSignature; validSignatures[otherUserId][fullKeyId] = haveValidSignature;
} }
if (!haveValidSignature) { if (!haveValidSignature) {
// no valid signature, this key is useless // no valid signature, this key is useless

View File

@ -22,7 +22,7 @@ dependencies:
js: ^0.6.3 js: ^0.6.3
slugify: ^2.0.0 slugify: ^2.0.0
html: ^0.15.0 html: ^0.15.0
collection: ^1.15.0-nullsafety.4 collection: ^1.15.0
dev_dependencies: dev_dependencies:
pedantic: ^1.11.0 pedantic: ^1.11.0