From af961b99dc2705c2937a18d1e08ae8ab1b84f77c Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 21 May 2020 15:20:33 +0200 Subject: [PATCH 01/64] get device verification status based on cross signing --- lib/src/client.dart | 96 ++++- lib/src/database/database.dart | 23 +- lib/src/database/database.g.dart | 551 +++++++++++++++++++++++++++- lib/src/database/database.moor | 20 +- lib/src/utils/device_keys_list.dart | 260 +++++++++++-- lib/src/utils/key_verification.dart | 28 +- lib/src/utils/room_key_request.dart | 2 +- 7 files changed, 912 insertions(+), 68 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 2841e69..3e817a7 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -796,7 +796,7 @@ class Client { pickledOlmAccount, ); } - _userDeviceKeys = await database.getUserDeviceKeys(id); + _userDeviceKeys = await database.getUserDeviceKeys(this); _olmSessions = await database.getOlmSessions(id, _userID); _rooms = await database.getRoomList(this, onlyLeft: false); _sortRooms(); @@ -1644,6 +1644,7 @@ class Client { action: '/client/r0/keys/query', data: {'timeout': 10000, 'device_keys': outdatedLists}); + // first we parse and persist the device keys for (final rawDeviceKeyListEntry in response['device_keys'].entries) { final String userId = rawDeviceKeyListEntry.key; final oldKeys = @@ -1653,10 +1654,17 @@ class Client { final String deviceId = rawDeviceKeyEntry.key; // Set the new device key for this device - - if (!oldKeys.containsKey(deviceId)) { - final entry = DeviceKeys.fromJson(rawDeviceKeyEntry.value); - if (entry.isValid) { + final entry = DeviceKeys.fromJson(rawDeviceKeyEntry.value, this); + if (entry.isValid) { + // is this a new key or the same one as an old one? + // better store an update - the signatures might have changed! + if (!oldKeys.containsKey(deviceId) || oldKeys[deviceId].ed25519Key == entry.ed25519Key) { + if (oldKeys.containsKey(deviceId)) { + // be sure to save the verified status + entry.verified = oldKeys[deviceId].verified; + entry.blocked = oldKeys[deviceId].blocked; + entry.validSignatures = oldKeys[deviceId].validSignatures; + } _userDeviceKeys[userId].deviceKeys[deviceId] = entry; if (deviceId == deviceID && entry.ed25519Key == @@ -1664,22 +1672,26 @@ class Client { // Always trust the own device entry.verified = true; } + } else { + // This shouldn't ever happen. The same device ID has gotten + // a new public key. So we ignore the update. TODO: ask krille + // if we should instead use the new key with unknown verified / blocked status + _userDeviceKeys[userId].deviceKeys[deviceId] = oldKeys[deviceId]; } if (database != null) { dbActions.add(() => database.storeUserDeviceKey( id, userId, deviceId, - json.encode( - _userDeviceKeys[userId].deviceKeys[deviceId].toJson()), - _userDeviceKeys[userId].deviceKeys[deviceId].verified, - _userDeviceKeys[userId].deviceKeys[deviceId].blocked, + json.encode(entry.toJson()), + json.encode(entry.validSignatures), + entry.verified, + entry.blocked, )); } - } else { - _userDeviceKeys[userId].deviceKeys[deviceId] = oldKeys[deviceId]; } } + // delete old/unused entries if (database != null) { for (final oldDeviceKeyEntry in oldKeys.entries) { final deviceId = oldDeviceKeyEntry.key; @@ -1696,6 +1708,68 @@ class Client { .add(() => database.storeUserDeviceKeysInfo(id, userId, false)); } } + // next we parse and persist the cross signing keys + for (final keyType in ['master_keys', 'self_signing_keys', 'user_signing_keys']) { + if (!(response[keyType] is Map)) { + continue; + } + for (final rawDeviceKeyListEntry in response[keyType].entries) { + final String userId = rawDeviceKeyListEntry.key; + final oldKeys = Map.from(_userDeviceKeys[userId].crossSigningKeys); + _userDeviceKeys[userId].crossSigningKeys = {}; + // add the types we arne't handling atm back + for (final oldEntry in oldKeys.entries) { + if (!oldEntry.value.usage.contains(keyType.substring(0, keyType.length - '_keys'.length))) { + _userDeviceKeys[userId].crossSigningKeys[oldEntry.key] = oldEntry.value; + } + } + final entry = CrossSigningKey.fromJson(rawDeviceKeyListEntry.value, this); + if (entry.isValid) { + final publicKey = entry.publicKey; + if (!oldKeys.containsKey(publicKey) || oldKeys[publicKey].ed25519Key == entry.ed25519Key) { + if (oldKeys.containsKey(publicKey)) { + // be sure to save the verification status + entry.verified = oldKeys[publicKey].verified; + entry.blocked = oldKeys[publicKey].blocked; + entry.validSignatures = oldKeys[publicKey].validSignatures; + } + _userDeviceKeys[userId].crossSigningKeys[publicKey] = entry; + } else { + // This shouldn't ever happen. The same device ID has gotten + // a new public key. So we ignore the update. TODO: ask krille + // if we should instead use the new key with unknown verified / blocked status + _userDeviceKeys[userId].crossSigningKeys[publicKey] = oldKeys[publicKey]; + } + if (database != null) { + dbActions.add(() => database.storeUserCrossSigningKey( + id, + userId, + publicKey, + json.encode(entry.toJson()), + json.encode(entry.validSignatures), + entry.verified, + entry.blocked, + )); + } + } + // delete old/unused entries + if (database != null) { + for (final oldCrossSigningKeyEntry in oldKeys.entries) { + final publicKey = oldCrossSigningKeyEntry.key; + if (!_userDeviceKeys[userId].crossSigningKeys.containsKey(publicKey)) { + // we need to remove an old key + dbActions.add( + () => database.removeUserCrossSigningKey(id, userId, publicKey)); + } + } + } + _userDeviceKeys[userId].outdated = false; + if (database != null) { + dbActions + .add(() => database.storeUserDeviceKeysInfo(id, userId, false)); + } + } + } } await database?.transaction(() async { for (final f in dbActions) { diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index e4af62a..f402068 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -13,7 +13,7 @@ class Database extends _$Database { Database(QueryExecutor e) : super(e); @override - int get schemaVersion => 3; + int get schemaVersion => 4; int get maxFileSize => 1 * 1024 * 1024; @@ -41,6 +41,15 @@ class Database extends _$Database { 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); + await m.addColumn(userDeviceKeysKey, userDeviceKeysKey.validSignatures); + // 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++; } }, ); @@ -51,15 +60,19 @@ class Database extends _$Database { return res.first; } - Future> getUserDeviceKeys(int clientId) async { - final deviceKeys = await getAllUserDeviceKeys(clientId).get(); + Future> getUserDeviceKeys(sdk.Client client) async { + final deviceKeys = await getAllUserDeviceKeys(client.id).get(); if (deviceKeys.isEmpty) { return {}; } - final deviceKeysKeys = await getAllUserDeviceKeysKeys(clientId).get(); + final deviceKeysKeys = await getAllUserDeviceKeysKeys(client.id).get(); + final crossSigningKeys = await getAllUserCrossSigningKeys(client.id).get(); final res = {}; for (final entry in deviceKeys) { - res[entry.userId] = sdk.DeviceKeysList.fromDb(entry, deviceKeysKeys.where((k) => k.userId == entry.userId).toList()); + res[entry.userId] = sdk.DeviceKeysList.fromDb(entry, + deviceKeysKeys.where((k) => k.userId == entry.userId).toList(), + crossSigningKeys.where((k) => k.userId == entry.userId).toList(), + client); } return res; } diff --git a/lib/src/database/database.g.dart b/lib/src/database/database.g.dart index 61bc1c7..0feeca1 100644 --- a/lib/src/database/database.g.dart +++ b/lib/src/database/database.g.dart @@ -699,6 +699,7 @@ class DbUserDeviceKeysKey extends DataClass final String userId; final String deviceId; final String content; + final String validSignatures; final bool verified; final bool blocked; DbUserDeviceKeysKey( @@ -706,6 +707,7 @@ class DbUserDeviceKeysKey extends DataClass @required this.userId, @required this.deviceId, @required this.content, + this.validSignatures, this.verified, this.blocked}); factory DbUserDeviceKeysKey.fromData( @@ -724,6 +726,8 @@ class DbUserDeviceKeysKey extends DataClass .mapFromDatabaseResponse(data['${effectivePrefix}device_id']), content: stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']), + validSignatures: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}valid_signatures']), verified: boolType.mapFromDatabaseResponse(data['${effectivePrefix}verified']), blocked: @@ -745,6 +749,9 @@ class DbUserDeviceKeysKey extends DataClass if (!nullToAbsent || content != null) { map['content'] = Variable(content); } + if (!nullToAbsent || validSignatures != null) { + map['valid_signatures'] = Variable(validSignatures); + } if (!nullToAbsent || verified != null) { map['verified'] = Variable(verified); } @@ -762,6 +769,7 @@ class DbUserDeviceKeysKey extends DataClass userId: serializer.fromJson(json['user_id']), deviceId: serializer.fromJson(json['device_id']), content: serializer.fromJson(json['content']), + validSignatures: serializer.fromJson(json['valid_signatures']), verified: serializer.fromJson(json['verified']), blocked: serializer.fromJson(json['blocked']), ); @@ -774,6 +782,7 @@ class DbUserDeviceKeysKey extends DataClass 'user_id': serializer.toJson(userId), 'device_id': serializer.toJson(deviceId), 'content': serializer.toJson(content), + 'valid_signatures': serializer.toJson(validSignatures), 'verified': serializer.toJson(verified), 'blocked': serializer.toJson(blocked), }; @@ -784,6 +793,7 @@ class DbUserDeviceKeysKey extends DataClass String userId, String deviceId, String content, + String validSignatures, bool verified, bool blocked}) => DbUserDeviceKeysKey( @@ -791,6 +801,7 @@ class DbUserDeviceKeysKey extends DataClass userId: userId ?? this.userId, deviceId: deviceId ?? this.deviceId, content: content ?? this.content, + validSignatures: validSignatures ?? this.validSignatures, verified: verified ?? this.verified, blocked: blocked ?? this.blocked, ); @@ -801,6 +812,7 @@ class DbUserDeviceKeysKey extends DataClass ..write('userId: $userId, ') ..write('deviceId: $deviceId, ') ..write('content: $content, ') + ..write('validSignatures: $validSignatures, ') ..write('verified: $verified, ') ..write('blocked: $blocked') ..write(')')) @@ -814,8 +826,10 @@ class DbUserDeviceKeysKey extends DataClass userId.hashCode, $mrjc( deviceId.hashCode, - $mrjc(content.hashCode, - $mrjc(verified.hashCode, blocked.hashCode)))))); + $mrjc( + content.hashCode, + $mrjc(validSignatures.hashCode, + $mrjc(verified.hashCode, blocked.hashCode))))))); @override bool operator ==(dynamic other) => identical(this, other) || @@ -824,6 +838,7 @@ class DbUserDeviceKeysKey extends DataClass other.userId == this.userId && other.deviceId == this.deviceId && other.content == this.content && + other.validSignatures == this.validSignatures && other.verified == this.verified && other.blocked == this.blocked); } @@ -833,6 +848,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { final Value userId; final Value deviceId; final Value content; + final Value validSignatures; final Value verified; final Value blocked; const UserDeviceKeysKeyCompanion({ @@ -840,6 +856,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { this.userId = const Value.absent(), this.deviceId = const Value.absent(), this.content = const Value.absent(), + this.validSignatures = const Value.absent(), this.verified = const Value.absent(), this.blocked = const Value.absent(), }); @@ -848,6 +865,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { @required String userId, @required String deviceId, @required String content, + this.validSignatures = const Value.absent(), this.verified = const Value.absent(), this.blocked = const Value.absent(), }) : clientId = Value(clientId), @@ -859,6 +877,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { Expression userId, Expression deviceId, Expression content, + Expression validSignatures, Expression verified, Expression blocked, }) { @@ -867,6 +886,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { if (userId != null) 'user_id': userId, if (deviceId != null) 'device_id': deviceId, if (content != null) 'content': content, + if (validSignatures != null) 'valid_signatures': validSignatures, if (verified != null) 'verified': verified, if (blocked != null) 'blocked': blocked, }); @@ -877,6 +897,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { Value userId, Value deviceId, Value content, + Value validSignatures, Value verified, Value blocked}) { return UserDeviceKeysKeyCompanion( @@ -884,6 +905,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { userId: userId ?? this.userId, deviceId: deviceId ?? this.deviceId, content: content ?? this.content, + validSignatures: validSignatures ?? this.validSignatures, verified: verified ?? this.verified, blocked: blocked ?? this.blocked, ); @@ -904,6 +926,9 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { if (content.present) { map['content'] = Variable(content.value); } + if (validSignatures.present) { + map['valid_signatures'] = Variable(validSignatures.value); + } if (verified.present) { map['verified'] = Variable(verified.value); } @@ -951,6 +976,16 @@ class UserDeviceKeysKey extends Table $customConstraints: 'NOT NULL'); } + final VerificationMeta _validSignaturesMeta = + const VerificationMeta('validSignatures'); + GeneratedTextColumn _validSignatures; + GeneratedTextColumn get validSignatures => + _validSignatures ??= _constructValidSignatures(); + GeneratedTextColumn _constructValidSignatures() { + return GeneratedTextColumn('valid_signatures', $tableName, true, + $customConstraints: ''); + } + final VerificationMeta _verifiedMeta = const VerificationMeta('verified'); GeneratedBoolColumn _verified; GeneratedBoolColumn get verified => _verified ??= _constructVerified(); @@ -971,7 +1006,7 @@ class UserDeviceKeysKey extends Table @override List get $columns => - [clientId, userId, deviceId, content, verified, blocked]; + [clientId, userId, deviceId, content, validSignatures, verified, blocked]; @override UserDeviceKeysKey get asDslTable => this; @override @@ -1008,6 +1043,12 @@ class UserDeviceKeysKey extends Table } else if (isInserting) { context.missing(_contentMeta); } + if (data.containsKey('valid_signatures')) { + context.handle( + _validSignaturesMeta, + validSignatures.isAcceptableOrUnknown( + data['valid_signatures'], _validSignaturesMeta)); + } if (data.containsKey('verified')) { context.handle(_verifiedMeta, verified.isAcceptableOrUnknown(data['verified'], _verifiedMeta)); @@ -1039,6 +1080,401 @@ class UserDeviceKeysKey extends Table bool get dontWriteConstraints => true; } +class DbUserCrossSigningKey extends DataClass + implements Insertable { + final int clientId; + final String userId; + final String publicKey; + final String content; + final String validSignatures; + final bool verified; + final bool blocked; + DbUserCrossSigningKey( + {@required this.clientId, + @required this.userId, + @required this.publicKey, + @required this.content, + this.validSignatures, + this.verified, + this.blocked}); + factory DbUserCrossSigningKey.fromData( + Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final intType = db.typeSystem.forDartType(); + final stringType = db.typeSystem.forDartType(); + final boolType = db.typeSystem.forDartType(); + return DbUserCrossSigningKey( + clientId: + intType.mapFromDatabaseResponse(data['${effectivePrefix}client_id']), + userId: + stringType.mapFromDatabaseResponse(data['${effectivePrefix}user_id']), + publicKey: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}public_key']), + content: + stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']), + validSignatures: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}valid_signatures']), + verified: + boolType.mapFromDatabaseResponse(data['${effectivePrefix}verified']), + blocked: + boolType.mapFromDatabaseResponse(data['${effectivePrefix}blocked']), + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || clientId != null) { + map['client_id'] = Variable(clientId); + } + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); + } + if (!nullToAbsent || publicKey != null) { + map['public_key'] = Variable(publicKey); + } + if (!nullToAbsent || content != null) { + map['content'] = Variable(content); + } + if (!nullToAbsent || validSignatures != null) { + map['valid_signatures'] = Variable(validSignatures); + } + if (!nullToAbsent || verified != null) { + map['verified'] = Variable(verified); + } + if (!nullToAbsent || blocked != null) { + map['blocked'] = Variable(blocked); + } + return map; + } + + factory DbUserCrossSigningKey.fromJson(Map json, + {ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return DbUserCrossSigningKey( + clientId: serializer.fromJson(json['client_id']), + userId: serializer.fromJson(json['user_id']), + publicKey: serializer.fromJson(json['public_key']), + content: serializer.fromJson(json['content']), + validSignatures: serializer.fromJson(json['valid_signatures']), + verified: serializer.fromJson(json['verified']), + blocked: serializer.fromJson(json['blocked']), + ); + } + @override + Map toJson({ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'client_id': serializer.toJson(clientId), + 'user_id': serializer.toJson(userId), + 'public_key': serializer.toJson(publicKey), + 'content': serializer.toJson(content), + 'valid_signatures': serializer.toJson(validSignatures), + 'verified': serializer.toJson(verified), + 'blocked': serializer.toJson(blocked), + }; + } + + DbUserCrossSigningKey copyWith( + {int clientId, + String userId, + String publicKey, + String content, + String validSignatures, + bool verified, + bool blocked}) => + DbUserCrossSigningKey( + clientId: clientId ?? this.clientId, + userId: userId ?? this.userId, + publicKey: publicKey ?? this.publicKey, + content: content ?? this.content, + validSignatures: validSignatures ?? this.validSignatures, + verified: verified ?? this.verified, + blocked: blocked ?? this.blocked, + ); + @override + String toString() { + return (StringBuffer('DbUserCrossSigningKey(') + ..write('clientId: $clientId, ') + ..write('userId: $userId, ') + ..write('publicKey: $publicKey, ') + ..write('content: $content, ') + ..write('validSignatures: $validSignatures, ') + ..write('verified: $verified, ') + ..write('blocked: $blocked') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc( + clientId.hashCode, + $mrjc( + userId.hashCode, + $mrjc( + publicKey.hashCode, + $mrjc( + content.hashCode, + $mrjc(validSignatures.hashCode, + $mrjc(verified.hashCode, blocked.hashCode))))))); + @override + bool operator ==(dynamic other) => + identical(this, other) || + (other is DbUserCrossSigningKey && + other.clientId == this.clientId && + other.userId == this.userId && + other.publicKey == this.publicKey && + other.content == this.content && + other.validSignatures == this.validSignatures && + other.verified == this.verified && + other.blocked == this.blocked); +} + +class UserCrossSigningKeysCompanion + extends UpdateCompanion { + final Value clientId; + final Value userId; + final Value publicKey; + final Value content; + final Value validSignatures; + final Value verified; + final Value blocked; + const UserCrossSigningKeysCompanion({ + this.clientId = const Value.absent(), + this.userId = const Value.absent(), + this.publicKey = const Value.absent(), + this.content = const Value.absent(), + this.validSignatures = const Value.absent(), + this.verified = const Value.absent(), + this.blocked = const Value.absent(), + }); + UserCrossSigningKeysCompanion.insert({ + @required int clientId, + @required String userId, + @required String publicKey, + @required String content, + this.validSignatures = const Value.absent(), + this.verified = const Value.absent(), + this.blocked = const Value.absent(), + }) : clientId = Value(clientId), + userId = Value(userId), + publicKey = Value(publicKey), + content = Value(content); + static Insertable custom({ + Expression clientId, + Expression userId, + Expression publicKey, + Expression content, + Expression validSignatures, + Expression verified, + Expression blocked, + }) { + return RawValuesInsertable({ + if (clientId != null) 'client_id': clientId, + if (userId != null) 'user_id': userId, + if (publicKey != null) 'public_key': publicKey, + if (content != null) 'content': content, + if (validSignatures != null) 'valid_signatures': validSignatures, + if (verified != null) 'verified': verified, + if (blocked != null) 'blocked': blocked, + }); + } + + UserCrossSigningKeysCompanion copyWith( + {Value clientId, + Value userId, + Value publicKey, + Value content, + Value validSignatures, + Value verified, + Value blocked}) { + return UserCrossSigningKeysCompanion( + clientId: clientId ?? this.clientId, + userId: userId ?? this.userId, + publicKey: publicKey ?? this.publicKey, + content: content ?? this.content, + validSignatures: validSignatures ?? this.validSignatures, + verified: verified ?? this.verified, + blocked: blocked ?? this.blocked, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (publicKey.present) { + map['public_key'] = Variable(publicKey.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (validSignatures.present) { + map['valid_signatures'] = Variable(validSignatures.value); + } + if (verified.present) { + map['verified'] = Variable(verified.value); + } + if (blocked.present) { + map['blocked'] = Variable(blocked.value); + } + return map; + } +} + +class UserCrossSigningKeys extends Table + with TableInfo { + final GeneratedDatabase _db; + final String _alias; + UserCrossSigningKeys(this._db, [this._alias]); + final VerificationMeta _clientIdMeta = const VerificationMeta('clientId'); + GeneratedIntColumn _clientId; + GeneratedIntColumn get clientId => _clientId ??= _constructClientId(); + GeneratedIntColumn _constructClientId() { + return GeneratedIntColumn('client_id', $tableName, false, + $customConstraints: 'NOT NULL REFERENCES clients(client_id)'); + } + + final VerificationMeta _userIdMeta = const VerificationMeta('userId'); + GeneratedTextColumn _userId; + GeneratedTextColumn get userId => _userId ??= _constructUserId(); + GeneratedTextColumn _constructUserId() { + return GeneratedTextColumn('user_id', $tableName, false, + $customConstraints: 'NOT NULL'); + } + + final VerificationMeta _publicKeyMeta = const VerificationMeta('publicKey'); + GeneratedTextColumn _publicKey; + GeneratedTextColumn get publicKey => _publicKey ??= _constructPublicKey(); + GeneratedTextColumn _constructPublicKey() { + return GeneratedTextColumn('public_key', $tableName, false, + $customConstraints: 'NOT NULL'); + } + + final VerificationMeta _contentMeta = const VerificationMeta('content'); + GeneratedTextColumn _content; + GeneratedTextColumn get content => _content ??= _constructContent(); + GeneratedTextColumn _constructContent() { + return GeneratedTextColumn('content', $tableName, false, + $customConstraints: 'NOT NULL'); + } + + final VerificationMeta _validSignaturesMeta = + const VerificationMeta('validSignatures'); + GeneratedTextColumn _validSignatures; + GeneratedTextColumn get validSignatures => + _validSignatures ??= _constructValidSignatures(); + GeneratedTextColumn _constructValidSignatures() { + return GeneratedTextColumn('valid_signatures', $tableName, true, + $customConstraints: ''); + } + + final VerificationMeta _verifiedMeta = const VerificationMeta('verified'); + GeneratedBoolColumn _verified; + GeneratedBoolColumn get verified => _verified ??= _constructVerified(); + GeneratedBoolColumn _constructVerified() { + return GeneratedBoolColumn('verified', $tableName, true, + $customConstraints: 'DEFAULT false', + defaultValue: const CustomExpression('false')); + } + + final VerificationMeta _blockedMeta = const VerificationMeta('blocked'); + GeneratedBoolColumn _blocked; + GeneratedBoolColumn get blocked => _blocked ??= _constructBlocked(); + GeneratedBoolColumn _constructBlocked() { + return GeneratedBoolColumn('blocked', $tableName, true, + $customConstraints: 'DEFAULT false', + defaultValue: const CustomExpression('false')); + } + + @override + List get $columns => [ + clientId, + userId, + publicKey, + content, + validSignatures, + verified, + blocked + ]; + @override + UserCrossSigningKeys get asDslTable => this; + @override + String get $tableName => _alias ?? 'user_cross_signing_keys'; + @override + final String actualTableName = 'user_cross_signing_keys'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('client_id')) { + context.handle(_clientIdMeta, + clientId.isAcceptableOrUnknown(data['client_id'], _clientIdMeta)); + } else if (isInserting) { + context.missing(_clientIdMeta); + } + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id'], _userIdMeta)); + } else if (isInserting) { + context.missing(_userIdMeta); + } + if (data.containsKey('public_key')) { + context.handle(_publicKeyMeta, + publicKey.isAcceptableOrUnknown(data['public_key'], _publicKeyMeta)); + } else if (isInserting) { + context.missing(_publicKeyMeta); + } + if (data.containsKey('content')) { + context.handle(_contentMeta, + content.isAcceptableOrUnknown(data['content'], _contentMeta)); + } else if (isInserting) { + context.missing(_contentMeta); + } + if (data.containsKey('valid_signatures')) { + context.handle( + _validSignaturesMeta, + validSignatures.isAcceptableOrUnknown( + data['valid_signatures'], _validSignaturesMeta)); + } + if (data.containsKey('verified')) { + context.handle(_verifiedMeta, + verified.isAcceptableOrUnknown(data['verified'], _verifiedMeta)); + } + if (data.containsKey('blocked')) { + context.handle(_blockedMeta, + blocked.isAcceptableOrUnknown(data['blocked'], _blockedMeta)); + } + return context; + } + + @override + Set get $primaryKey => {}; + @override + DbUserCrossSigningKey map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return DbUserCrossSigningKey.fromData(data, _db, prefix: effectivePrefix); + } + + @override + UserCrossSigningKeys createAlias(String alias) { + return UserCrossSigningKeys(_db, alias); + } + + @override + List get customConstraints => + const ['UNIQUE(client_id, user_id, public_key)']; + @override + bool get dontWriteConstraints => true; +} + class DbOlmSessions extends DataClass implements Insertable { final int clientId; final String identityKey; @@ -4680,6 +5116,13 @@ abstract class _$Database extends GeneratedDatabase { Index get userDeviceKeysKeyIndex => _userDeviceKeysKeyIndex ??= Index( 'user_device_keys_key_index', 'CREATE INDEX user_device_keys_key_index ON user_device_keys_key(client_id);'); + UserCrossSigningKeys _userCrossSigningKeys; + UserCrossSigningKeys get userCrossSigningKeys => + _userCrossSigningKeys ??= UserCrossSigningKeys(this); + Index _userCrossSigningKeysIndex; + Index get userCrossSigningKeysIndex => _userCrossSigningKeysIndex ??= Index( + 'user_cross_signing_keys_index', + 'CREATE INDEX user_cross_signing_keys_index ON user_cross_signing_keys(client_id);'); OlmSessions _olmSessions; OlmSessions get olmSessions => _olmSessions ??= OlmSessions(this); Index _olmSessionsIndex; @@ -4823,6 +5266,7 @@ abstract class _$Database extends GeneratedDatabase { userId: row.readString('user_id'), deviceId: row.readString('device_id'), content: row.readString('content'), + validSignatures: row.readString('valid_signatures'), verified: row.readBool('verified'), blocked: row.readBool('blocked'), ); @@ -4835,6 +5279,25 @@ abstract class _$Database extends GeneratedDatabase { readsFrom: {userDeviceKeysKey}).map(_rowToDbUserDeviceKeysKey); } + DbUserCrossSigningKey _rowToDbUserCrossSigningKey(QueryRow row) { + return DbUserCrossSigningKey( + clientId: row.readInt('client_id'), + userId: row.readString('user_id'), + publicKey: row.readString('public_key'), + content: row.readString('content'), + validSignatures: row.readString('valid_signatures'), + verified: row.readBool('verified'), + blocked: row.readBool('blocked'), + ); + } + + Selectable getAllUserCrossSigningKeys(int client_id) { + return customSelect( + 'SELECT * FROM user_cross_signing_keys WHERE client_id = :client_id', + variables: [Variable.withInt(client_id)], + readsFrom: {userCrossSigningKeys}).map(_rowToDbUserCrossSigningKey); + } + DbOlmSessions _rowToDbOlmSessions(QueryRow row) { return DbOlmSessions( clientId: row.readInt('client_id'), @@ -5036,15 +5499,22 @@ abstract class _$Database extends GeneratedDatabase { ); } - Future storeUserDeviceKey(int client_id, String user_id, - String device_id, String content, bool verified, bool blocked) { + Future storeUserDeviceKey( + int client_id, + String user_id, + String device_id, + String content, + String valid_signatures, + bool verified, + bool blocked) { return customInsert( - 'INSERT OR REPLACE INTO user_device_keys_key (client_id, user_id, device_id, content, verified, blocked) VALUES (:client_id, :user_id, :device_id, :content, :verified, :blocked)', + 'INSERT OR REPLACE INTO user_device_keys_key (client_id, user_id, device_id, content, valid_signatures, verified, blocked) VALUES (:client_id, :user_id, :device_id, :content, :valid_signatures, :verified, :blocked)', variables: [ Variable.withInt(client_id), Variable.withString(user_id), Variable.withString(device_id), Variable.withString(content), + Variable.withString(valid_signatures), Variable.withBool(verified), Variable.withBool(blocked) ], @@ -5066,6 +5536,73 @@ abstract class _$Database extends GeneratedDatabase { ); } + Future setVerifiedUserCrossSigningKey( + bool verified, int client_id, String user_id, String public_key) { + return customUpdate( + 'UPDATE user_cross_signing_keys SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key', + variables: [ + Variable.withBool(verified), + Variable.withInt(client_id), + Variable.withString(user_id), + Variable.withString(public_key) + ], + updates: {userCrossSigningKeys}, + updateKind: UpdateKind.update, + ); + } + + Future setBlockedUserCrossSigningKey( + bool blocked, int client_id, String user_id, String public_key) { + return customUpdate( + 'UPDATE user_cross_signing_keys SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key', + variables: [ + Variable.withBool(blocked), + Variable.withInt(client_id), + Variable.withString(user_id), + Variable.withString(public_key) + ], + updates: {userCrossSigningKeys}, + updateKind: UpdateKind.update, + ); + } + + Future storeUserCrossSigningKey( + int client_id, + String user_id, + String public_key, + String content, + String valid_signatures, + bool verified, + bool blocked) { + return customInsert( + 'INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, valid_signatures, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :valid_signatures, :verified, :blocked)', + variables: [ + Variable.withInt(client_id), + Variable.withString(user_id), + Variable.withString(public_key), + Variable.withString(content), + Variable.withString(valid_signatures), + Variable.withBool(verified), + Variable.withBool(blocked) + ], + updates: {userCrossSigningKeys}, + ); + } + + Future removeUserCrossSigningKey( + int client_id, String user_id, String public_key) { + return customUpdate( + 'DELETE FROM user_cross_signing_keys WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key', + variables: [ + Variable.withInt(client_id), + Variable.withString(user_id), + Variable.withString(public_key) + ], + updates: {userCrossSigningKeys}, + updateKind: UpdateKind.delete, + ); + } + Future insertClient( String name, String homeserver_url, @@ -5472,6 +6009,8 @@ abstract class _$Database extends GeneratedDatabase { userDeviceKeysIndex, userDeviceKeysKey, userDeviceKeysKeyIndex, + userCrossSigningKeys, + userCrossSigningKeysIndex, olmSessions, olmSessionsIndex, outboundGroupSessions, diff --git a/lib/src/database/database.moor b/lib/src/database/database.moor index ebe66ea..b26ea41 100644 --- a/lib/src/database/database.moor +++ b/lib/src/database/database.moor @@ -26,12 +26,25 @@ CREATE TABLE user_device_keys_key ( user_id TEXT NOT NULL, device_id TEXT NOT NULL, content TEXT NOT NULL, + valid_signatures TEXT, verified BOOLEAN DEFAULT false, blocked BOOLEAN DEFAULT false, UNIQUE(client_id, user_id, device_id) ) as DbUserDeviceKeysKey; CREATE INDEX user_device_keys_key_index ON user_device_keys_key(client_id); +CREATE TABLE user_cross_signing_keys ( + client_id INTEGER NOT NULL REFERENCES clients(client_id), + user_id TEXT NOT NULL, + public_key TEXT NOT NULL, + content TEXT NOT NULL, + valid_signatures TEXT, + verified BOOLEAN DEFAULT false, + blocked BOOLEAN DEFAULT false, + UNIQUE(client_id, user_id, public_key) +) as DbUserCrossSigningKey; +CREATE INDEX user_cross_signing_keys_index ON user_cross_signing_keys(client_id); + CREATE TABLE olm_sessions ( client_id INTEGER NOT NULL REFERENCES clients(client_id), identity_key TEXT NOT NULL, @@ -154,6 +167,7 @@ updateClientKeys: UPDATE clients SET olm_account = :olm_account WHERE client_id storePrevBatch: UPDATE clients SET prev_batch = :prev_batch WHERE client_id = :client_id; getAllUserDeviceKeys: SELECT * FROM user_device_keys WHERE client_id = :client_id; getAllUserDeviceKeysKeys: SELECT * FROM user_device_keys_key WHERE client_id = :client_id; +getAllUserCrossSigningKeys: SELECT * FROM user_cross_signing_keys WHERE client_id = :client_id; getAllOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id; storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle); getAllOutboundGroupSessions: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id; @@ -168,8 +182,12 @@ updateInboundGroupSessionIndexes: UPDATE inbound_group_sessions SET indexes = :i storeUserDeviceKeysInfo: INSERT OR REPLACE INTO user_device_keys (client_id, user_id, outdated) VALUES (:client_id, :user_id, :outdated); setVerifiedUserDeviceKey: UPDATE user_device_keys_key SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id; setBlockedUserDeviceKey: UPDATE user_device_keys_key SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id; -storeUserDeviceKey: INSERT OR REPLACE INTO user_device_keys_key (client_id, user_id, device_id, content, verified, blocked) VALUES (:client_id, :user_id, :device_id, :content, :verified, :blocked); +storeUserDeviceKey: INSERT OR REPLACE INTO user_device_keys_key (client_id, user_id, device_id, content, valid_signatures, verified, blocked) VALUES (:client_id, :user_id, :device_id, :content, :valid_signatures, :verified, :blocked); removeUserDeviceKey: DELETE FROM user_device_keys_key WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id; +setVerifiedUserCrossSigningKey: UPDATE user_cross_signing_keys SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; +setBlockedUserCrossSigningKey: UPDATE user_cross_signing_keys SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; +storeUserCrossSigningKey: INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, valid_signatures, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :valid_signatures, :verified, :blocked); +removeUserCrossSigningKey: DELETE FROM user_cross_signing_keys WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; insertClient: INSERT INTO clients (name, homeserver_url, token, user_id, device_id, device_name, prev_batch, olm_account) VALUES (:name, :homeserver_url, :token, :user_id, :device_id, :device_name, :prev_batch, :olm_account); ensureRoomExists: INSERT OR IGNORE INTO rooms (client_id, room_id, membership) VALUES (:client_id, :room_id, :membership); setRoomPrevBatch: UPDATE rooms SET prev_batch = :prev_batch WHERE client_id = :client_id AND room_id = :room_id; diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 8cacbdc..162eb59 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -1,36 +1,50 @@ import 'dart:convert'; +import 'package:canonical_json/canonical_json.dart'; +import 'package:olm/olm.dart' as olm; import '../client.dart'; -import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey; +import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey, DbUserCrossSigningKey; import '../event.dart'; import 'key_verification.dart'; class DeviceKeysList { + Client client; String userId; bool outdated = true; Map deviceKeys = {}; + Map crossSigningKeys = {}; - DeviceKeysList.fromDb(DbUserDeviceKey dbEntry, List childEntries) { + DeviceKeysList.fromDb(DbUserDeviceKey dbEntry, List childEntries, List crossSigningEntries, Client cl) { + client = cl; userId = dbEntry.userId; outdated = dbEntry.outdated; deviceKeys = {}; for (final childEntry in childEntries) { - final entry = DeviceKeys.fromDb(childEntry); + final entry = DeviceKeys.fromDb(childEntry, client); if (entry.isValid) { deviceKeys[childEntry.deviceId] = entry; } else { outdated = true; } } + for (final crossSigningEntry in crossSigningEntries) { + final entry = CrossSigningKey.fromDb(crossSigningEntry, client); + if (entry.isValid) { + crossSigningKeys[crossSigningEntry.publicKey] = entry; + } else { + outdated = true; + } + } } - DeviceKeysList.fromJson(Map json) { + DeviceKeysList.fromJson(Map json, Client cl) { + client = cl; userId = json['user_id']; outdated = json['outdated']; deviceKeys = {}; for (final rawDeviceKeyEntry in json['device_keys'].entries) { deviceKeys[rawDeviceKeyEntry.key] = - DeviceKeys.fromJson(rawDeviceKeyEntry.value); + DeviceKeys.fromJson(rawDeviceKeyEntry.value, client); } } @@ -54,27 +68,199 @@ class DeviceKeysList { DeviceKeysList(this.userId); } -class DeviceKeys { +abstract class _SignedKey { + Client client; String userId; - String deviceId; - List algorithms; + String identifier; + Map content; Map keys; Map signatures; - Map unsigned; + Map validSignatures; bool verified; bool blocked; + String get ed25519Key => keys['ed25519:$identifier']; + + bool get crossVerified { + try { + return hasValidSignatureChain(); + } catch (err, stacktrace) { + print('[Cross Signing] Error during trying to determine signature chain: ' + err.toString()); + print(stacktrace); + return false; + } + } + + String _getSigningContent() { + final data = Map.from(content); + data.remove('verified'); + data.remove('blocked'); + data.remove('unsigned'); + data.remove('signatures'); + return String.fromCharCodes(canonicalJson.encode(data)); + } + + bool _verifySignature(String pubKey, String signature) { + final olmutil = olm.Utility(); + var valid = false; + try { + olmutil.ed25519_verify(pubKey, _getSigningContent(), signature); + valid = true; + } finally { + olmutil.free(); + } + return valid; + } + + bool hasValidSignatureChain({Set visited}) { + if (visited == null) { + visited = Set(); + } + final setKey = '${userId};${identifier}'; + if (visited.contains(setKey)) { + return false; // prevent recursion + } + visited.add(setKey); + for (final signatureEntries in signatures.entries) { + final otherUserId = signatureEntries.key; + if (!(signatureEntries.value is Map) || !client.userDeviceKeys.containsKey(otherUserId)) { + continue; + } + for (final signatureEntry in signatureEntries.value.entries) { + final fullKeyId = signatureEntry.key; + final signature = signatureEntry.value; + if (!(fullKeyId is String) || !(signature is String)) { + continue; + } + final keyId = fullKeyId.substring('ed25519:'.length); + _SignedKey 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]; + } else { + continue; + } + if (key.blocked) { + continue; // we can't be bothered about this keys signatures + } + var haveValidSignature = false; + var gotSignatureFromCache = false; + if (validSignatures != null && validSignatures.containsKey(otherUserId) && validSignatures[otherUserId].containsKey(fullKeyId)) { + if (validSignatures[otherUserId][fullKeyId] == true) { + haveValidSignature = true; + gotSignatureFromCache = true; + } else if (validSignatures[otherUserId][fullKeyId] == false) { + gotSignatureFromCache = true; + } + } + if (!gotSignatureFromCache) { + // validate the signature manually + haveValidSignature = _verifySignature(key.ed25519Key, signature); + } + if (!haveValidSignature) { + // no valid signature, this key is useless + continue; + } + + if (key.verified) { + return true; // we verified this key and it is valid...all checks out! + } + // or else we just recurse into that key and chack if it works out + final haveChain = key.hasValidSignatureChain(visited: visited); + if (haveChain) { + return true; + } + } + } + return false; + } +} + +class CrossSigningKey extends _SignedKey { + String get publicKey => identifier; + List usage; + + bool get isValid => userId != null && publicKey != null && keys != null && ed25519Key != null; + + Future setVerified(bool newVerified) { + verified = newVerified; + return client.database?.setVerifiedUserCrossSigningKey(newVerified, client.id, userId, publicKey); + } + + Future setBlocked(bool newBlocked) { + blocked = newBlocked; + return client.database?.setBlockedUserCrossSigningKey(newBlocked, client.id, userId, publicKey); + } + + CrossSigningKey.fromDb(DbUserCrossSigningKey dbEntry, Client cl) { + client = cl; + final json = Event.getMapFromPayload(dbEntry.content); + content = Map.from(json); + userId = dbEntry.userId; + identifier = dbEntry.publicKey; + usage = json['usage'].cast(); + keys = json['keys'] != null ? Map.from(json['keys']) : null; + signatures = json['signatures'] != null ? Map.from(json['signatures']) : null; + validSignatures = null; + if (dbEntry.validSignatures != null) { + final validSignaturesContent = Event.getMapFromPayload(dbEntry.validSignatures); + if (validSignaturesContent is Map) { + validSignatures = validSignaturesContent.cast(); + } + } + verified = dbEntry.verified; + blocked = dbEntry.blocked; + } + + CrossSigningKey.fromJson(Map json, Client cl) { + client = cl; + content = Map.from(json); + userId = json['user_id']; + usage = json['usage'].cast(); + keys = json['keys'] != null ? Map.from(json['keys']) : null; + signatures = json['signatures'] != null + ? Map.from(json['signatures']) + : null; + validSignatures = null; + verified = json['verified'] ?? false; + blocked = json['blocked'] ?? false; + if (keys != null) { + identifier = keys.values.first; + } + } + + Map toJson() { + final data = Map.from(content); + data['user_id'] = userId; + data['usage'] = usage; + if (keys != null) { + data['keys'] = keys; + } + if (signatures != null) { + data['signatures'] = signatures; + } + data['verified'] = verified; + data['blocked'] = blocked; + return data; + } +} + +class DeviceKeys extends _SignedKey { + String get deviceId => identifier; + List algorithms; + Map unsigned; + String get curve25519Key => keys['curve25519:$deviceId']; - String get ed25519Key => keys['ed25519:$deviceId']; - bool get isValid => userId != null && deviceId != null && curve25519Key != null && ed25519Key != null; + bool get isValid => userId != null && deviceId != null && keys != null && curve25519Key != null && ed25519Key != null; - Future setVerified(bool newVerified, Client client) { + Future setVerified(bool newVerified) { verified = newVerified; return client.database?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId); } - Future setBlocked(bool newBlocked, Client client) { + Future setBlocked(bool newBlocked) { blocked = newBlocked; for (var room in client.rooms) { if (!room.encrypted) continue; @@ -85,36 +271,36 @@ class DeviceKeys { return client.database?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId); } - DeviceKeys({ - this.userId, - this.deviceId, - this.algorithms, - this.keys, - this.signatures, - this.unsigned, - this.verified, - this.blocked, - }); - - DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry) { - final content = Event.getMapFromPayload(dbEntry.content); + DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry, Client cl) { + client = cl; + final json = Event.getMapFromPayload(dbEntry.content); + content = Map.from(json); userId = dbEntry.userId; - deviceId = dbEntry.deviceId; - algorithms = content['algorithms'].cast(); - keys = content['keys'] != null ? Map.from(content['keys']) : null; - signatures = content['signatures'] != null - ? Map.from(content['signatures']) + identifier = dbEntry.deviceId; + algorithms = json['algorithms'].cast(); + keys = json['keys'] != null ? Map.from(json['keys']) : null; + signatures = json['signatures'] != null + ? Map.from(json['signatures']) : null; - unsigned = content['unsigned'] != null - ? Map.from(content['unsigned']) + unsigned = json['unsigned'] != null + ? Map.from(json['unsigned']) : null; + validSignatures = null; + if (dbEntry.validSignatures != null) { + final validSignaturesContent = Event.getMapFromPayload(dbEntry.validSignatures); + if (validSignaturesContent is Map) { + validSignatures = validSignaturesContent.cast(); + } + } verified = dbEntry.verified; blocked = dbEntry.blocked; } - DeviceKeys.fromJson(Map json) { + DeviceKeys.fromJson(Map json, Client cl) { + client = cl; + content = Map.from(json); userId = json['user_id']; - deviceId = json['device_id']; + identifier = json['device_id']; algorithms = json['algorithms'].cast(); keys = json['keys'] != null ? Map.from(json['keys']) : null; signatures = json['signatures'] != null @@ -128,7 +314,7 @@ class DeviceKeys { } Map toJson() { - final data = {}; + final data = Map.from(content); data['user_id'] = userId; data['device_id'] = deviceId; data['algorithms'] = algorithms; @@ -146,7 +332,7 @@ class DeviceKeys { return data; } - KeyVerification startVerification(Client client) { + KeyVerification startVerification() { final request = KeyVerification(client: client, userId: userId, deviceId: deviceId); request.start(); client.addKeyVerificationRequest(request); diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 5735d50..b58d575 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -271,7 +271,7 @@ class KeyVerification { return []; } - Future verifyKeys(Map keys, Future Function(String, DeviceKeys) verifier) async { + Future verifyKeys(Map keys, Future Function(String, dynamic) verifier) async { final verifiedDevices = []; if (!client.userDeviceKeys.containsKey(userId)) { @@ -288,14 +288,23 @@ class KeyVerification { return; } verifiedDevices.add(verifyDeviceId); - } else { - // TODO: we would check here if what we are verifying is actually a - // cross-signing key and not a "normal" device key + } else if (client.userDeviceKeys[userId].crossSigningKeys.containsKey(verifyDeviceId)) { + // this is a cross signing key! + if (!(await verifier(keyInfo, client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId]))) { + await cancel('m.key_mismatch'); + return; + } + verifiedDevices.add(verifyDeviceId); } } // okay, we reached this far, so all the devices are verified! for (final verifyDeviceId in verifiedDevices) { - await client.userDeviceKeys[userId].deviceKeys[verifyDeviceId].setVerified(true, client); + if (client.userDeviceKeys[userId].deviceKeys.containsKey(verifyDeviceId)) { + await client.userDeviceKeys[userId].deviceKeys[verifyDeviceId].setVerified(true); + } else if (client.userDeviceKeys[userId].crossSigningKeys.containsKey(verifyDeviceId)) { + await client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId].setVerified(true); + // TODO: sign the other persons master key + } } } @@ -645,8 +654,13 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { mac[entry.key] = entry.value; } } - await request.verifyKeys(mac, (String mac, DeviceKeys device) async { - return mac == _calculateMac(device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId); + await request.verifyKeys(mac, (String mac, dynamic device) async { + if (device is DeviceKeys) { + return mac == _calculateMac(device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId); + } else if (device is CrossSigningKey) { + return mac == _calculateMac(device.ed25519Key, baseInfo + 'ed25519:' + device.publicKey); + } + return false; }); await request.send('m.key.verification.done', {}); if (request.state != KeyVerificationState.error) { diff --git a/lib/src/utils/room_key_request.dart b/lib/src/utils/room_key_request.dart index d839daa..65dc521 100644 --- a/lib/src/utils/room_key_request.dart +++ b/lib/src/utils/room_key_request.dart @@ -22,7 +22,7 @@ class RoomKeyRequest extends ToDeviceEvent { for (final key in session.forwardingCurve25519KeyChain) { forwardedKeys.add(key); } - await requestingDevice?.setVerified(true, client); + await requestingDevice?.setVerified(true); var message = session.content; message['forwarding_curve25519_key_chain'] = forwardedKeys; From ead44e4014fa400961dc8fa7bb5a7503f8134b98 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 21 May 2020 15:32:06 +0200 Subject: [PATCH 02/64] fix tests and stuffs --- lib/src/utils/device_keys_list.dart | 4 +--- test/client_test.dart | 2 +- test/device_keys_list_test.dart | 8 ++++---- test_driver/famedlysdk_test.dart | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 162eb59..8dbe4f1 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -113,9 +113,7 @@ abstract class _SignedKey { } bool hasValidSignatureChain({Set visited}) { - if (visited == null) { - visited = Set(); - } + visited ??= {}; final setKey = '${userId};${identifier}'; if (visited.contains(setKey)) { return false; // prevent recursion diff --git a/test/client_test.dart b/test/client_test.dart index 2d5b66e..4e6f0a1 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -601,7 +601,7 @@ void main() { 'dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA' } } - }); + }, matrix); test('startOutgoingOlmSessions', () async { expect(matrix.olmSessions.length, 0); if (olmEnabled) { diff --git a/test/device_keys_list_test.dart b/test/device_keys_list_test.dart index 08b83e2..c9de76e 100644 --- a/test/device_keys_list_test.dart +++ b/test/device_keys_list_test.dart @@ -56,21 +56,21 @@ void main() { }; var userDeviceKeys = { - '@alice:example.com': DeviceKeysList.fromJson(rawListJson), + '@alice:example.com': DeviceKeysList.fromJson(rawListJson, null), }; var userDeviceKeyRaw = { '@alice:example.com': rawListJson, }; - expect(json.encode(DeviceKeys.fromJson(rawJson).toJson()), + expect(json.encode(DeviceKeys.fromJson(rawJson, null).toJson()), json.encode(rawJson)); - expect(json.encode(DeviceKeysList.fromJson(rawListJson).toJson()), + expect(json.encode(DeviceKeysList.fromJson(rawListJson, null).toJson()), json.encode(rawListJson)); var mapFromRaw = {}; for (final rawListEntry in userDeviceKeyRaw.entries) { mapFromRaw[rawListEntry.key] = - DeviceKeysList.fromJson(rawListEntry.value); + DeviceKeysList.fromJson(rawListEntry.value, null); } expect(mapFromRaw.toString(), userDeviceKeys.toString()); }); diff --git a/test_driver/famedlysdk_test.dart b/test_driver/famedlysdk_test.dart index fc145cd..8d3c96f 100644 --- a/test_driver/famedlysdk_test.dart +++ b/test_driver/famedlysdk_test.dart @@ -109,7 +109,7 @@ void test() async { assert(!testClientB .userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].blocked); await testClientA.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID] - .setVerified(true, testClientA); + .setVerified(true); print('++++ Check if own olm device is verified by default ++++'); assert(testClientA.userDeviceKeys.containsKey(testUserA)); From eaefdb64ca4f247a9fe0e04f3eb109731e7b1831 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 21 May 2020 16:51:15 +0200 Subject: [PATCH 03/64] make that cleints can only use "verified" --- lib/src/client.dart | 6 +++--- lib/src/utils/device_keys_list.dart | 26 +++++++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 3e817a7..4cfeb84 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1661,7 +1661,7 @@ class Client { if (!oldKeys.containsKey(deviceId) || oldKeys[deviceId].ed25519Key == entry.ed25519Key) { if (oldKeys.containsKey(deviceId)) { // be sure to save the verified status - entry.verified = oldKeys[deviceId].verified; + entry.setDirectVerified(oldKeys[deviceId].directVerified); entry.blocked = oldKeys[deviceId].blocked; entry.validSignatures = oldKeys[deviceId].validSignatures; } @@ -1670,7 +1670,7 @@ class Client { entry.ed25519Key == fingerprintKey) { // Always trust the own device - entry.verified = true; + entry.setDirectVerified(true); } } else { // This shouldn't ever happen. The same device ID has gotten @@ -1729,7 +1729,7 @@ class Client { if (!oldKeys.containsKey(publicKey) || oldKeys[publicKey].ed25519Key == entry.ed25519Key) { if (oldKeys.containsKey(publicKey)) { // be sure to save the verification status - entry.verified = oldKeys[publicKey].verified; + entry.setDirectVerified(oldKeys[publicKey].directVerified); entry.blocked = oldKeys[publicKey].blocked; entry.validSignatures = oldKeys[publicKey].validSignatures; } diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 8dbe4f1..baeb2f3 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -76,11 +76,19 @@ abstract class _SignedKey { Map keys; Map signatures; Map validSignatures; - bool verified; + bool _verified; bool blocked; String get ed25519Key => keys['ed25519:$identifier']; + bool get verified => directVerified || crossVerified; + + void setDirectVerified(bool v) { + _verified = v; + } + + bool get directVerified => _verified; + bool get crossVerified { try { return hasValidSignatureChain(); @@ -182,7 +190,7 @@ class CrossSigningKey extends _SignedKey { bool get isValid => userId != null && publicKey != null && keys != null && ed25519Key != null; Future setVerified(bool newVerified) { - verified = newVerified; + _verified = newVerified; return client.database?.setVerifiedUserCrossSigningKey(newVerified, client.id, userId, publicKey); } @@ -207,7 +215,7 @@ class CrossSigningKey extends _SignedKey { validSignatures = validSignaturesContent.cast(); } } - verified = dbEntry.verified; + _verified = dbEntry.verified; blocked = dbEntry.blocked; } @@ -221,7 +229,7 @@ class CrossSigningKey extends _SignedKey { ? Map.from(json['signatures']) : null; validSignatures = null; - verified = json['verified'] ?? false; + _verified = json['verified'] ?? false; blocked = json['blocked'] ?? false; if (keys != null) { identifier = keys.values.first; @@ -238,7 +246,7 @@ class CrossSigningKey extends _SignedKey { if (signatures != null) { data['signatures'] = signatures; } - data['verified'] = verified; + data['verified'] = _verified; data['blocked'] = blocked; return data; } @@ -254,7 +262,7 @@ class DeviceKeys extends _SignedKey { bool get isValid => userId != null && deviceId != null && keys != null && curve25519Key != null && ed25519Key != null; Future setVerified(bool newVerified) { - verified = newVerified; + _verified = newVerified; return client.database?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId); } @@ -290,7 +298,7 @@ class DeviceKeys extends _SignedKey { validSignatures = validSignaturesContent.cast(); } } - verified = dbEntry.verified; + _verified = dbEntry.verified; blocked = dbEntry.blocked; } @@ -307,7 +315,7 @@ class DeviceKeys extends _SignedKey { unsigned = json['unsigned'] != null ? Map.from(json['unsigned']) : null; - verified = json['verified'] ?? false; + _verified = json['verified'] ?? false; blocked = json['blocked'] ?? false; } @@ -325,7 +333,7 @@ class DeviceKeys extends _SignedKey { if (unsigned != null) { data['unsigned'] = unsigned; } - data['verified'] = verified; + data['verified'] = _verified; data['blocked'] = blocked; return data; } From 10372a9dbeec03dd7abc083eb5a6b96c671eb5ed Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 21 May 2020 16:52:25 +0200 Subject: [PATCH 04/64] verified devices are not blocked --- lib/src/utils/device_keys_list.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index baeb2f3..ca0d71d 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -81,7 +81,7 @@ abstract class _SignedKey { String get ed25519Key => keys['ed25519:$identifier']; - bool get verified => directVerified || crossVerified; + bool get verified => (directVerified || crossVerified) && !blocked; void setDirectVerified(bool v) { _verified = v; From 788353120bd3ac55c023070c6e452681afdbee04 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 21 May 2020 16:55:30 +0200 Subject: [PATCH 05/64] accidental inifnite recursion --- lib/src/utils/device_keys_list.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index ca0d71d..1dcd71d 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -169,7 +169,7 @@ abstract class _SignedKey { continue; } - if (key.verified) { + if (key.directVerified) { return true; // we verified this key and it is valid...all checks out! } // or else we just recurse into that key and chack if it works out From 74361fff0e5494aef054715c7fa55ebc2e457f37 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 22 May 2020 11:04:27 +0200 Subject: [PATCH 06/64] don't cache via db and properly handle invalid signatures --- lib/src/client.dart | 2 - lib/src/database/database.dart | 1 - lib/src/database/database.g.dart | 131 +++------------------------- lib/src/database/database.moor | 6 +- lib/src/utils/device_keys_list.dart | 71 +++++---------- 5 files changed, 37 insertions(+), 174 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 4cfeb84..0d5b1ad 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1684,7 +1684,6 @@ class Client { userId, deviceId, json.encode(entry.toJson()), - json.encode(entry.validSignatures), entry.verified, entry.blocked, )); @@ -1746,7 +1745,6 @@ class Client { userId, publicKey, json.encode(entry.toJson()), - json.encode(entry.validSignatures), entry.verified, entry.blocked, )); diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index f402068..9f6c264 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -46,7 +46,6 @@ class Database extends _$Database { if (from == 3) { await m.createTable(userCrossSigningKeys); await m.createIndex(userCrossSigningKeysIndex); - await m.addColumn(userDeviceKeysKey, userDeviceKeysKey.validSignatures); // 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++; diff --git a/lib/src/database/database.g.dart b/lib/src/database/database.g.dart index 0feeca1..daea006 100644 --- a/lib/src/database/database.g.dart +++ b/lib/src/database/database.g.dart @@ -699,7 +699,6 @@ class DbUserDeviceKeysKey extends DataClass final String userId; final String deviceId; final String content; - final String validSignatures; final bool verified; final bool blocked; DbUserDeviceKeysKey( @@ -707,7 +706,6 @@ class DbUserDeviceKeysKey extends DataClass @required this.userId, @required this.deviceId, @required this.content, - this.validSignatures, this.verified, this.blocked}); factory DbUserDeviceKeysKey.fromData( @@ -726,8 +724,6 @@ class DbUserDeviceKeysKey extends DataClass .mapFromDatabaseResponse(data['${effectivePrefix}device_id']), content: stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']), - validSignatures: stringType - .mapFromDatabaseResponse(data['${effectivePrefix}valid_signatures']), verified: boolType.mapFromDatabaseResponse(data['${effectivePrefix}verified']), blocked: @@ -749,9 +745,6 @@ class DbUserDeviceKeysKey extends DataClass if (!nullToAbsent || content != null) { map['content'] = Variable(content); } - if (!nullToAbsent || validSignatures != null) { - map['valid_signatures'] = Variable(validSignatures); - } if (!nullToAbsent || verified != null) { map['verified'] = Variable(verified); } @@ -769,7 +762,6 @@ class DbUserDeviceKeysKey extends DataClass userId: serializer.fromJson(json['user_id']), deviceId: serializer.fromJson(json['device_id']), content: serializer.fromJson(json['content']), - validSignatures: serializer.fromJson(json['valid_signatures']), verified: serializer.fromJson(json['verified']), blocked: serializer.fromJson(json['blocked']), ); @@ -782,7 +774,6 @@ class DbUserDeviceKeysKey extends DataClass 'user_id': serializer.toJson(userId), 'device_id': serializer.toJson(deviceId), 'content': serializer.toJson(content), - 'valid_signatures': serializer.toJson(validSignatures), 'verified': serializer.toJson(verified), 'blocked': serializer.toJson(blocked), }; @@ -793,7 +784,6 @@ class DbUserDeviceKeysKey extends DataClass String userId, String deviceId, String content, - String validSignatures, bool verified, bool blocked}) => DbUserDeviceKeysKey( @@ -801,7 +791,6 @@ class DbUserDeviceKeysKey extends DataClass userId: userId ?? this.userId, deviceId: deviceId ?? this.deviceId, content: content ?? this.content, - validSignatures: validSignatures ?? this.validSignatures, verified: verified ?? this.verified, blocked: blocked ?? this.blocked, ); @@ -812,7 +801,6 @@ class DbUserDeviceKeysKey extends DataClass ..write('userId: $userId, ') ..write('deviceId: $deviceId, ') ..write('content: $content, ') - ..write('validSignatures: $validSignatures, ') ..write('verified: $verified, ') ..write('blocked: $blocked') ..write(')')) @@ -826,10 +814,8 @@ class DbUserDeviceKeysKey extends DataClass userId.hashCode, $mrjc( deviceId.hashCode, - $mrjc( - content.hashCode, - $mrjc(validSignatures.hashCode, - $mrjc(verified.hashCode, blocked.hashCode))))))); + $mrjc(content.hashCode, + $mrjc(verified.hashCode, blocked.hashCode)))))); @override bool operator ==(dynamic other) => identical(this, other) || @@ -838,7 +824,6 @@ class DbUserDeviceKeysKey extends DataClass other.userId == this.userId && other.deviceId == this.deviceId && other.content == this.content && - other.validSignatures == this.validSignatures && other.verified == this.verified && other.blocked == this.blocked); } @@ -848,7 +833,6 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { final Value userId; final Value deviceId; final Value content; - final Value validSignatures; final Value verified; final Value blocked; const UserDeviceKeysKeyCompanion({ @@ -856,7 +840,6 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { this.userId = const Value.absent(), this.deviceId = const Value.absent(), this.content = const Value.absent(), - this.validSignatures = const Value.absent(), this.verified = const Value.absent(), this.blocked = const Value.absent(), }); @@ -865,7 +848,6 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { @required String userId, @required String deviceId, @required String content, - this.validSignatures = const Value.absent(), this.verified = const Value.absent(), this.blocked = const Value.absent(), }) : clientId = Value(clientId), @@ -877,7 +859,6 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { Expression userId, Expression deviceId, Expression content, - Expression validSignatures, Expression verified, Expression blocked, }) { @@ -886,7 +867,6 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { if (userId != null) 'user_id': userId, if (deviceId != null) 'device_id': deviceId, if (content != null) 'content': content, - if (validSignatures != null) 'valid_signatures': validSignatures, if (verified != null) 'verified': verified, if (blocked != null) 'blocked': blocked, }); @@ -897,7 +877,6 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { Value userId, Value deviceId, Value content, - Value validSignatures, Value verified, Value blocked}) { return UserDeviceKeysKeyCompanion( @@ -905,7 +884,6 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { userId: userId ?? this.userId, deviceId: deviceId ?? this.deviceId, content: content ?? this.content, - validSignatures: validSignatures ?? this.validSignatures, verified: verified ?? this.verified, blocked: blocked ?? this.blocked, ); @@ -926,9 +904,6 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion { if (content.present) { map['content'] = Variable(content.value); } - if (validSignatures.present) { - map['valid_signatures'] = Variable(validSignatures.value); - } if (verified.present) { map['verified'] = Variable(verified.value); } @@ -976,16 +951,6 @@ class UserDeviceKeysKey extends Table $customConstraints: 'NOT NULL'); } - final VerificationMeta _validSignaturesMeta = - const VerificationMeta('validSignatures'); - GeneratedTextColumn _validSignatures; - GeneratedTextColumn get validSignatures => - _validSignatures ??= _constructValidSignatures(); - GeneratedTextColumn _constructValidSignatures() { - return GeneratedTextColumn('valid_signatures', $tableName, true, - $customConstraints: ''); - } - final VerificationMeta _verifiedMeta = const VerificationMeta('verified'); GeneratedBoolColumn _verified; GeneratedBoolColumn get verified => _verified ??= _constructVerified(); @@ -1006,7 +971,7 @@ class UserDeviceKeysKey extends Table @override List get $columns => - [clientId, userId, deviceId, content, validSignatures, verified, blocked]; + [clientId, userId, deviceId, content, verified, blocked]; @override UserDeviceKeysKey get asDslTable => this; @override @@ -1043,12 +1008,6 @@ class UserDeviceKeysKey extends Table } else if (isInserting) { context.missing(_contentMeta); } - if (data.containsKey('valid_signatures')) { - context.handle( - _validSignaturesMeta, - validSignatures.isAcceptableOrUnknown( - data['valid_signatures'], _validSignaturesMeta)); - } if (data.containsKey('verified')) { context.handle(_verifiedMeta, verified.isAcceptableOrUnknown(data['verified'], _verifiedMeta)); @@ -1086,7 +1045,6 @@ class DbUserCrossSigningKey extends DataClass final String userId; final String publicKey; final String content; - final String validSignatures; final bool verified; final bool blocked; DbUserCrossSigningKey( @@ -1094,7 +1052,6 @@ class DbUserCrossSigningKey extends DataClass @required this.userId, @required this.publicKey, @required this.content, - this.validSignatures, this.verified, this.blocked}); factory DbUserCrossSigningKey.fromData( @@ -1113,8 +1070,6 @@ class DbUserCrossSigningKey extends DataClass .mapFromDatabaseResponse(data['${effectivePrefix}public_key']), content: stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']), - validSignatures: stringType - .mapFromDatabaseResponse(data['${effectivePrefix}valid_signatures']), verified: boolType.mapFromDatabaseResponse(data['${effectivePrefix}verified']), blocked: @@ -1136,9 +1091,6 @@ class DbUserCrossSigningKey extends DataClass if (!nullToAbsent || content != null) { map['content'] = Variable(content); } - if (!nullToAbsent || validSignatures != null) { - map['valid_signatures'] = Variable(validSignatures); - } if (!nullToAbsent || verified != null) { map['verified'] = Variable(verified); } @@ -1156,7 +1108,6 @@ class DbUserCrossSigningKey extends DataClass userId: serializer.fromJson(json['user_id']), publicKey: serializer.fromJson(json['public_key']), content: serializer.fromJson(json['content']), - validSignatures: serializer.fromJson(json['valid_signatures']), verified: serializer.fromJson(json['verified']), blocked: serializer.fromJson(json['blocked']), ); @@ -1169,7 +1120,6 @@ class DbUserCrossSigningKey extends DataClass 'user_id': serializer.toJson(userId), 'public_key': serializer.toJson(publicKey), 'content': serializer.toJson(content), - 'valid_signatures': serializer.toJson(validSignatures), 'verified': serializer.toJson(verified), 'blocked': serializer.toJson(blocked), }; @@ -1180,7 +1130,6 @@ class DbUserCrossSigningKey extends DataClass String userId, String publicKey, String content, - String validSignatures, bool verified, bool blocked}) => DbUserCrossSigningKey( @@ -1188,7 +1137,6 @@ class DbUserCrossSigningKey extends DataClass userId: userId ?? this.userId, publicKey: publicKey ?? this.publicKey, content: content ?? this.content, - validSignatures: validSignatures ?? this.validSignatures, verified: verified ?? this.verified, blocked: blocked ?? this.blocked, ); @@ -1199,7 +1147,6 @@ class DbUserCrossSigningKey extends DataClass ..write('userId: $userId, ') ..write('publicKey: $publicKey, ') ..write('content: $content, ') - ..write('validSignatures: $validSignatures, ') ..write('verified: $verified, ') ..write('blocked: $blocked') ..write(')')) @@ -1213,10 +1160,8 @@ class DbUserCrossSigningKey extends DataClass userId.hashCode, $mrjc( publicKey.hashCode, - $mrjc( - content.hashCode, - $mrjc(validSignatures.hashCode, - $mrjc(verified.hashCode, blocked.hashCode))))))); + $mrjc(content.hashCode, + $mrjc(verified.hashCode, blocked.hashCode)))))); @override bool operator ==(dynamic other) => identical(this, other) || @@ -1225,7 +1170,6 @@ class DbUserCrossSigningKey extends DataClass other.userId == this.userId && other.publicKey == this.publicKey && other.content == this.content && - other.validSignatures == this.validSignatures && other.verified == this.verified && other.blocked == this.blocked); } @@ -1236,7 +1180,6 @@ class UserCrossSigningKeysCompanion final Value userId; final Value publicKey; final Value content; - final Value validSignatures; final Value verified; final Value blocked; const UserCrossSigningKeysCompanion({ @@ -1244,7 +1187,6 @@ class UserCrossSigningKeysCompanion this.userId = const Value.absent(), this.publicKey = const Value.absent(), this.content = const Value.absent(), - this.validSignatures = const Value.absent(), this.verified = const Value.absent(), this.blocked = const Value.absent(), }); @@ -1253,7 +1195,6 @@ class UserCrossSigningKeysCompanion @required String userId, @required String publicKey, @required String content, - this.validSignatures = const Value.absent(), this.verified = const Value.absent(), this.blocked = const Value.absent(), }) : clientId = Value(clientId), @@ -1265,7 +1206,6 @@ class UserCrossSigningKeysCompanion Expression userId, Expression publicKey, Expression content, - Expression validSignatures, Expression verified, Expression blocked, }) { @@ -1274,7 +1214,6 @@ class UserCrossSigningKeysCompanion if (userId != null) 'user_id': userId, if (publicKey != null) 'public_key': publicKey, if (content != null) 'content': content, - if (validSignatures != null) 'valid_signatures': validSignatures, if (verified != null) 'verified': verified, if (blocked != null) 'blocked': blocked, }); @@ -1285,7 +1224,6 @@ class UserCrossSigningKeysCompanion Value userId, Value publicKey, Value content, - Value validSignatures, Value verified, Value blocked}) { return UserCrossSigningKeysCompanion( @@ -1293,7 +1231,6 @@ class UserCrossSigningKeysCompanion userId: userId ?? this.userId, publicKey: publicKey ?? this.publicKey, content: content ?? this.content, - validSignatures: validSignatures ?? this.validSignatures, verified: verified ?? this.verified, blocked: blocked ?? this.blocked, ); @@ -1314,9 +1251,6 @@ class UserCrossSigningKeysCompanion if (content.present) { map['content'] = Variable(content.value); } - if (validSignatures.present) { - map['valid_signatures'] = Variable(validSignatures.value); - } if (verified.present) { map['verified'] = Variable(verified.value); } @@ -1364,16 +1298,6 @@ class UserCrossSigningKeys extends Table $customConstraints: 'NOT NULL'); } - final VerificationMeta _validSignaturesMeta = - const VerificationMeta('validSignatures'); - GeneratedTextColumn _validSignatures; - GeneratedTextColumn get validSignatures => - _validSignatures ??= _constructValidSignatures(); - GeneratedTextColumn _constructValidSignatures() { - return GeneratedTextColumn('valid_signatures', $tableName, true, - $customConstraints: ''); - } - final VerificationMeta _verifiedMeta = const VerificationMeta('verified'); GeneratedBoolColumn _verified; GeneratedBoolColumn get verified => _verified ??= _constructVerified(); @@ -1393,15 +1317,8 @@ class UserCrossSigningKeys extends Table } @override - List get $columns => [ - clientId, - userId, - publicKey, - content, - validSignatures, - verified, - blocked - ]; + List get $columns => + [clientId, userId, publicKey, content, verified, blocked]; @override UserCrossSigningKeys get asDslTable => this; @override @@ -1438,12 +1355,6 @@ class UserCrossSigningKeys extends Table } else if (isInserting) { context.missing(_contentMeta); } - if (data.containsKey('valid_signatures')) { - context.handle( - _validSignaturesMeta, - validSignatures.isAcceptableOrUnknown( - data['valid_signatures'], _validSignaturesMeta)); - } if (data.containsKey('verified')) { context.handle(_verifiedMeta, verified.isAcceptableOrUnknown(data['verified'], _verifiedMeta)); @@ -5266,7 +5177,6 @@ abstract class _$Database extends GeneratedDatabase { userId: row.readString('user_id'), deviceId: row.readString('device_id'), content: row.readString('content'), - validSignatures: row.readString('valid_signatures'), verified: row.readBool('verified'), blocked: row.readBool('blocked'), ); @@ -5285,7 +5195,6 @@ abstract class _$Database extends GeneratedDatabase { userId: row.readString('user_id'), publicKey: row.readString('public_key'), content: row.readString('content'), - validSignatures: row.readString('valid_signatures'), verified: row.readBool('verified'), blocked: row.readBool('blocked'), ); @@ -5499,22 +5408,15 @@ abstract class _$Database extends GeneratedDatabase { ); } - Future storeUserDeviceKey( - int client_id, - String user_id, - String device_id, - String content, - String valid_signatures, - bool verified, - bool blocked) { + Future storeUserDeviceKey(int client_id, String user_id, + String device_id, String content, bool verified, bool blocked) { return customInsert( - 'INSERT OR REPLACE INTO user_device_keys_key (client_id, user_id, device_id, content, valid_signatures, verified, blocked) VALUES (:client_id, :user_id, :device_id, :content, :valid_signatures, :verified, :blocked)', + 'INSERT OR REPLACE INTO user_device_keys_key (client_id, user_id, device_id, content, verified, blocked) VALUES (:client_id, :user_id, :device_id, :content, :verified, :blocked)', variables: [ Variable.withInt(client_id), Variable.withString(user_id), Variable.withString(device_id), Variable.withString(content), - Variable.withString(valid_signatures), Variable.withBool(verified), Variable.withBool(blocked) ], @@ -5566,22 +5468,15 @@ abstract class _$Database extends GeneratedDatabase { ); } - Future storeUserCrossSigningKey( - int client_id, - String user_id, - String public_key, - String content, - String valid_signatures, - bool verified, - bool blocked) { + Future storeUserCrossSigningKey(int client_id, String user_id, + String public_key, String content, bool verified, bool blocked) { return customInsert( - 'INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, valid_signatures, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :valid_signatures, :verified, :blocked)', + 'INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :verified, :blocked)', variables: [ Variable.withInt(client_id), Variable.withString(user_id), Variable.withString(public_key), Variable.withString(content), - Variable.withString(valid_signatures), Variable.withBool(verified), Variable.withBool(blocked) ], diff --git a/lib/src/database/database.moor b/lib/src/database/database.moor index b26ea41..049c792 100644 --- a/lib/src/database/database.moor +++ b/lib/src/database/database.moor @@ -26,7 +26,6 @@ CREATE TABLE user_device_keys_key ( user_id TEXT NOT NULL, device_id TEXT NOT NULL, content TEXT NOT NULL, - valid_signatures TEXT, verified BOOLEAN DEFAULT false, blocked BOOLEAN DEFAULT false, UNIQUE(client_id, user_id, device_id) @@ -38,7 +37,6 @@ CREATE TABLE user_cross_signing_keys ( user_id TEXT NOT NULL, public_key TEXT NOT NULL, content TEXT NOT NULL, - valid_signatures TEXT, verified BOOLEAN DEFAULT false, blocked BOOLEAN DEFAULT false, UNIQUE(client_id, user_id, public_key) @@ -182,11 +180,11 @@ updateInboundGroupSessionIndexes: UPDATE inbound_group_sessions SET indexes = :i storeUserDeviceKeysInfo: INSERT OR REPLACE INTO user_device_keys (client_id, user_id, outdated) VALUES (:client_id, :user_id, :outdated); setVerifiedUserDeviceKey: UPDATE user_device_keys_key SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id; setBlockedUserDeviceKey: UPDATE user_device_keys_key SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id; -storeUserDeviceKey: INSERT OR REPLACE INTO user_device_keys_key (client_id, user_id, device_id, content, valid_signatures, verified, blocked) VALUES (:client_id, :user_id, :device_id, :content, :valid_signatures, :verified, :blocked); +storeUserDeviceKey: INSERT OR REPLACE INTO user_device_keys_key (client_id, user_id, device_id, content, verified, blocked) VALUES (:client_id, :user_id, :device_id, :content, :verified, :blocked); removeUserDeviceKey: DELETE FROM user_device_keys_key WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id; setVerifiedUserCrossSigningKey: UPDATE user_cross_signing_keys SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; setBlockedUserCrossSigningKey: UPDATE user_cross_signing_keys SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; -storeUserCrossSigningKey: INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, valid_signatures, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :valid_signatures, :verified, :blocked); +storeUserCrossSigningKey: INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :verified, :blocked); removeUserCrossSigningKey: DELETE FROM user_cross_signing_keys WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; insertClient: INSERT INTO clients (name, homeserver_url, token, user_id, device_id, device_name, prev_batch, olm_account) VALUES (:name, :homeserver_url, :token, :user_id, :device_id, :device_name, :prev_batch, :olm_account); ensureRoomExists: INSERT OR IGNORE INTO rooms (client_id, room_id, membership) VALUES (:client_id, :room_id, :membership); diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 1dcd71d..9d9bb5e 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -101,8 +101,10 @@ abstract class _SignedKey { String _getSigningContent() { final data = Map.from(content); + // some old data might have the custom verified and blocked keys data.remove('verified'); data.remove('blocked'); + // remove the keys not needed for signing data.remove('unsigned'); data.remove('signatures'); return String.fromCharCodes(canonicalJson.encode(data)); @@ -114,6 +116,9 @@ abstract class _SignedKey { try { olmutil.ed25519_verify(pubKey, _getSigningContent(), signature); valid = true; + } catch (_) { + // bad signature + valid = false; } finally { olmutil.free(); } @@ -157,12 +162,18 @@ abstract class _SignedKey { haveValidSignature = true; gotSignatureFromCache = true; } else if (validSignatures[otherUserId][fullKeyId] == false) { + haveValidSignature = false; gotSignatureFromCache = true; } } if (!gotSignatureFromCache) { // validate the signature manually haveValidSignature = _verifySignature(key.ed25519Key, signature); + validSignatures ??= {}; + if (!validSignatures.containsKey(otherUserId)) { + validSignatures[otherUserId] = {}; + } + validSignatures[otherUserId][fullKeyId] = haveValidSignature; } if (!haveValidSignature) { // no valid signature, this key is useless @@ -181,6 +192,17 @@ abstract class _SignedKey { } return false; } + + Map toJson() { + final data = Map.from(content); + // some old data may have the verified and blocked keys which are unneeded now + data.remove('verified'); + data.remove('blocked'); + return data; + } + + @override + String toString() => json.encode(toJson()); } class CrossSigningKey extends _SignedKey { @@ -208,13 +230,6 @@ class CrossSigningKey extends _SignedKey { usage = json['usage'].cast(); keys = json['keys'] != null ? Map.from(json['keys']) : null; signatures = json['signatures'] != null ? Map.from(json['signatures']) : null; - validSignatures = null; - if (dbEntry.validSignatures != null) { - final validSignaturesContent = Event.getMapFromPayload(dbEntry.validSignatures); - if (validSignaturesContent is Map) { - validSignatures = validSignaturesContent.cast(); - } - } _verified = dbEntry.verified; blocked = dbEntry.blocked; } @@ -228,28 +243,12 @@ class CrossSigningKey extends _SignedKey { signatures = json['signatures'] != null ? Map.from(json['signatures']) : null; - validSignatures = null; _verified = json['verified'] ?? false; blocked = json['blocked'] ?? false; if (keys != null) { identifier = keys.values.first; } } - - Map toJson() { - final data = Map.from(content); - data['user_id'] = userId; - data['usage'] = usage; - if (keys != null) { - data['keys'] = keys; - } - if (signatures != null) { - data['signatures'] = signatures; - } - data['verified'] = _verified; - data['blocked'] = blocked; - return data; - } } class DeviceKeys extends _SignedKey { @@ -291,13 +290,6 @@ class DeviceKeys extends _SignedKey { unsigned = json['unsigned'] != null ? Map.from(json['unsigned']) : null; - validSignatures = null; - if (dbEntry.validSignatures != null) { - final validSignaturesContent = Event.getMapFromPayload(dbEntry.validSignatures); - if (validSignaturesContent is Map) { - validSignatures = validSignaturesContent.cast(); - } - } _verified = dbEntry.verified; blocked = dbEntry.blocked; } @@ -319,25 +311,6 @@ class DeviceKeys extends _SignedKey { blocked = json['blocked'] ?? false; } - Map toJson() { - final data = Map.from(content); - data['user_id'] = userId; - data['device_id'] = deviceId; - data['algorithms'] = algorithms; - if (keys != null) { - data['keys'] = keys; - } - if (signatures != null) { - data['signatures'] = signatures; - } - if (unsigned != null) { - data['unsigned'] = unsigned; - } - data['verified'] = _verified; - data['blocked'] = blocked; - return data; - } - KeyVerification startVerification() { final request = KeyVerification(client: client, userId: userId, deviceId: deviceId); request.start(); From ee9090b7a729d82677436e823c9d10b441e95693 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 22 May 2020 11:13:58 +0200 Subject: [PATCH 07/64] fix tests --- test/device_keys_list_test.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/device_keys_list_test.dart b/test/device_keys_list_test.dart index c9de76e..71afb36 100644 --- a/test/device_keys_list_test.dart +++ b/test/device_keys_list_test.dart @@ -62,8 +62,13 @@ void main() { '@alice:example.com': rawListJson, }; - expect(json.encode(DeviceKeys.fromJson(rawJson, null).toJson()), + final key = DeviceKeys.fromJson(rawJson, null); + rawJson.remove('verified'); + rawJson.remove('blocked'); + expect(json.encode(key.toJson()), json.encode(rawJson)); + expect(key.verified, false); + expect(key.blocked, true); expect(json.encode(DeviceKeysList.fromJson(rawListJson, null).toJson()), json.encode(rawListJson)); From c9a0c5302afabcf9174572a3daed96d98ee34813 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 22 May 2020 13:18:45 +0200 Subject: [PATCH 08/64] format --- lib/src/client.dart | 56 ++++++++++++++++++----------- lib/src/database/database.dart | 15 ++++---- lib/src/utils/device_keys_list.dart | 36 +++++++++++++------ lib/src/utils/key_verification.dart | 29 ++++++++++----- test/device_keys_list_test.dart | 3 +- 5 files changed, 91 insertions(+), 48 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 8e68ee7..8f39895 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1664,7 +1664,8 @@ class Client { if (entry.isValid) { // is this a new key or the same one as an old one? // better store an update - the signatures might have changed! - if (!oldKeys.containsKey(deviceId) || oldKeys[deviceId].ed25519Key == entry.ed25519Key) { + if (!oldKeys.containsKey(deviceId) || + oldKeys[deviceId].ed25519Key == entry.ed25519Key) { if (oldKeys.containsKey(deviceId)) { // be sure to save the verified status entry.setDirectVerified(oldKeys[deviceId].directVerified); @@ -1681,7 +1682,8 @@ class Client { // This shouldn't ever happen. The same device ID has gotten // a new public key. So we ignore the update. TODO: ask krille // if we should instead use the new key with unknown verified / blocked status - _userDeviceKeys[userId].deviceKeys[deviceId] = oldKeys[deviceId]; + _userDeviceKeys[userId].deviceKeys[deviceId] = + oldKeys[deviceId]; } if (database != null) { dbActions.add(() => database.storeUserDeviceKey( @@ -1713,24 +1715,33 @@ class Client { } } // next we parse and persist the cross signing keys - for (final keyType in ['master_keys', 'self_signing_keys', 'user_signing_keys']) { + for (final keyType in [ + 'master_keys', + 'self_signing_keys', + 'user_signing_keys' + ]) { if (!(response[keyType] is Map)) { continue; } for (final rawDeviceKeyListEntry in response[keyType].entries) { final String userId = rawDeviceKeyListEntry.key; - final oldKeys = Map.from(_userDeviceKeys[userId].crossSigningKeys); + final oldKeys = Map.from( + _userDeviceKeys[userId].crossSigningKeys); _userDeviceKeys[userId].crossSigningKeys = {}; // add the types we arne't handling atm back for (final oldEntry in oldKeys.entries) { - if (!oldEntry.value.usage.contains(keyType.substring(0, keyType.length - '_keys'.length))) { - _userDeviceKeys[userId].crossSigningKeys[oldEntry.key] = oldEntry.value; + if (!oldEntry.value.usage.contains( + keyType.substring(0, keyType.length - '_keys'.length))) { + _userDeviceKeys[userId].crossSigningKeys[oldEntry.key] = + oldEntry.value; } } - final entry = CrossSigningKey.fromJson(rawDeviceKeyListEntry.value, this); + final entry = + CrossSigningKey.fromJson(rawDeviceKeyListEntry.value, this); if (entry.isValid) { final publicKey = entry.publicKey; - if (!oldKeys.containsKey(publicKey) || oldKeys[publicKey].ed25519Key == entry.ed25519Key) { + if (!oldKeys.containsKey(publicKey) || + oldKeys[publicKey].ed25519Key == entry.ed25519Key) { if (oldKeys.containsKey(publicKey)) { // be sure to save the verification status entry.setDirectVerified(oldKeys[publicKey].directVerified); @@ -1742,34 +1753,37 @@ class Client { // This shouldn't ever happen. The same device ID has gotten // a new public key. So we ignore the update. TODO: ask krille // if we should instead use the new key with unknown verified / blocked status - _userDeviceKeys[userId].crossSigningKeys[publicKey] = oldKeys[publicKey]; + _userDeviceKeys[userId].crossSigningKeys[publicKey] = + oldKeys[publicKey]; } if (database != null) { dbActions.add(() => database.storeUserCrossSigningKey( - id, - userId, - publicKey, - json.encode(entry.toJson()), - entry.directVerified, - entry.blocked, - )); + id, + userId, + publicKey, + json.encode(entry.toJson()), + entry.directVerified, + entry.blocked, + )); } } // delete old/unused entries if (database != null) { for (final oldCrossSigningKeyEntry in oldKeys.entries) { final publicKey = oldCrossSigningKeyEntry.key; - if (!_userDeviceKeys[userId].crossSigningKeys.containsKey(publicKey)) { + if (!_userDeviceKeys[userId] + .crossSigningKeys + .containsKey(publicKey)) { // we need to remove an old key - dbActions.add( - () => database.removeUserCrossSigningKey(id, userId, publicKey)); + dbActions.add(() => database.removeUserCrossSigningKey( + id, userId, publicKey)); } } } _userDeviceKeys[userId].outdated = false; if (database != null) { - dbActions - .add(() => database.storeUserDeviceKeysInfo(id, userId, false)); + dbActions.add( + () => database.storeUserDeviceKeysInfo(id, userId, false)); } } } diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index f5b26bc..f83b15f 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -47,7 +47,8 @@ class Database extends _$Database { 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'); + await m.issueCustomQuery( + 'UPDATE user_device_keys SET outdated = true'); from++; } }, @@ -59,7 +60,8 @@ class Database extends _$Database { return res.first; } - Future> getUserDeviceKeys(sdk.Client client) async { + Future> getUserDeviceKeys( + sdk.Client client) async { final deviceKeys = await getAllUserDeviceKeys(client.id).get(); if (deviceKeys.isEmpty) { return {}; @@ -68,10 +70,11 @@ class Database extends _$Database { final crossSigningKeys = await getAllUserCrossSigningKeys(client.id).get(); final res = {}; for (final entry in deviceKeys) { - res[entry.userId] = sdk.DeviceKeysList.fromDb(entry, - deviceKeysKeys.where((k) => k.userId == entry.userId).toList(), - crossSigningKeys.where((k) => k.userId == entry.userId).toList(), - client); + res[entry.userId] = sdk.DeviceKeysList.fromDb( + entry, + deviceKeysKeys.where((k) => k.userId == entry.userId).toList(), + crossSigningKeys.where((k) => k.userId == entry.userId).toList(), + client); } return res; } diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 8235736..c43b6da 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -3,7 +3,8 @@ import 'package:canonical_json/canonical_json.dart'; import 'package:olm/olm.dart' as olm; import '../client.dart'; -import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey, DbUserCrossSigningKey; +import '../database/database.dart' + show DbUserDeviceKey, DbUserDeviceKeysKey, DbUserCrossSigningKey; import '../event.dart'; import 'key_verification.dart'; @@ -14,7 +15,11 @@ class DeviceKeysList { Map deviceKeys = {}; Map crossSigningKeys = {}; - DeviceKeysList.fromDb(DbUserDeviceKey dbEntry, List childEntries, List crossSigningEntries, Client cl) { + DeviceKeysList.fromDb( + DbUserDeviceKey dbEntry, + List childEntries, + List crossSigningEntries, + Client cl) { client = cl; userId = dbEntry.userId; outdated = dbEntry.outdated; @@ -93,7 +98,9 @@ abstract class _SignedKey { try { return hasValidSignatureChain(); } catch (err, stacktrace) { - print('[Cross Signing] Error during trying to determine signature chain: ' + err.toString()); + print( + '[Cross Signing] Error during trying to determine signature chain: ' + + err.toString()); print(stacktrace); return false; } @@ -134,7 +141,8 @@ abstract class _SignedKey { visited.add(setKey); for (final signatureEntries in signatures.entries) { final otherUserId = signatureEntries.key; - if (!(signatureEntries.value is Map) || !client.userDeviceKeys.containsKey(otherUserId)) { + if (!(signatureEntries.value is Map) || + !client.userDeviceKeys.containsKey(otherUserId)) { continue; } for (final signatureEntry in signatureEntries.value.entries) { @@ -147,7 +155,8 @@ abstract class _SignedKey { _SignedKey key; if (client.userDeviceKeys[otherUserId].deviceKeys.containsKey(keyId)) { key = client.userDeviceKeys[otherUserId].deviceKeys[keyId]; - } else if (client.userDeviceKeys[otherUserId].crossSigningKeys.containsKey(keyId)) { + } else if (client.userDeviceKeys[otherUserId].crossSigningKeys + .containsKey(keyId)) { key = client.userDeviceKeys[otherUserId].crossSigningKeys[keyId]; } else { continue; @@ -157,7 +166,9 @@ abstract class _SignedKey { } var haveValidSignature = false; var gotSignatureFromCache = false; - if (validSignatures != null && validSignatures.containsKey(otherUserId) && validSignatures[otherUserId].containsKey(fullKeyId)) { + if (validSignatures != null && + validSignatures.containsKey(otherUserId) && + validSignatures[otherUserId].containsKey(fullKeyId)) { if (validSignatures[otherUserId][fullKeyId] == true) { haveValidSignature = true; gotSignatureFromCache = true; @@ -209,16 +220,19 @@ class CrossSigningKey extends _SignedKey { String get publicKey => identifier; List usage; - bool get isValid => userId != null && publicKey != null && keys != null && ed25519Key != null; + bool get isValid => + userId != null && publicKey != null && keys != null && ed25519Key != null; Future setVerified(bool newVerified) { _verified = newVerified; - return client.database?.setVerifiedUserCrossSigningKey(newVerified, client.id, userId, publicKey); + return client.database?.setVerifiedUserCrossSigningKey( + newVerified, client.id, userId, publicKey); } Future setBlocked(bool newBlocked) { blocked = newBlocked; - return client.database?.setBlockedUserCrossSigningKey(newBlocked, client.id, userId, publicKey); + return client.database?.setBlockedUserCrossSigningKey( + newBlocked, client.id, userId, publicKey); } CrossSigningKey.fromDb(DbUserCrossSigningKey dbEntry, Client cl) { @@ -229,7 +243,9 @@ class CrossSigningKey extends _SignedKey { identifier = dbEntry.publicKey; usage = json['usage'].cast(); keys = json['keys'] != null ? Map.from(json['keys']) : null; - signatures = json['signatures'] != null ? Map.from(json['signatures']) : null; + signatures = json['signatures'] != null + ? Map.from(json['signatures']) + : null; _verified = dbEntry.verified; blocked = dbEntry.blocked; } diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index f06d620..614c970 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -291,7 +291,8 @@ class KeyVerification { return []; } - Future verifyKeys(Map keys, Future Function(String, dynamic) verifier) async { + Future verifyKeys(Map keys, + Future Function(String, dynamic) verifier) async { final verifiedDevices = []; if (!client.userDeviceKeys.containsKey(userId)) { @@ -310,9 +311,11 @@ class KeyVerification { return; } verifiedDevices.add(verifyDeviceId); - } else if (client.userDeviceKeys[userId].crossSigningKeys.containsKey(verifyDeviceId)) { + } else if (client.userDeviceKeys[userId].crossSigningKeys + .containsKey(verifyDeviceId)) { // this is a cross signing key! - if (!(await verifier(keyInfo, client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId]))) { + if (!(await verifier(keyInfo, + client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId]))) { await cancel('m.key_mismatch'); return; } @@ -321,10 +324,14 @@ class KeyVerification { } // okay, we reached this far, so all the devices are verified! for (final verifyDeviceId in verifiedDevices) { - if (client.userDeviceKeys[userId].deviceKeys.containsKey(verifyDeviceId)) { - await client.userDeviceKeys[userId].deviceKeys[verifyDeviceId].setVerified(true); - } else if (client.userDeviceKeys[userId].crossSigningKeys.containsKey(verifyDeviceId)) { - await client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId].setVerified(true); + if (client.userDeviceKeys[userId].deviceKeys + .containsKey(verifyDeviceId)) { + await client.userDeviceKeys[userId].deviceKeys[verifyDeviceId] + .setVerified(true); + } else if (client.userDeviceKeys[userId].crossSigningKeys + .containsKey(verifyDeviceId)) { + await client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId] + .setVerified(true); // TODO: sign the other persons master key } } @@ -708,9 +715,13 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { } await request.verifyKeys(mac, (String mac, dynamic device) async { if (device is DeviceKeys) { - return mac == _calculateMac(device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId); + return mac == + _calculateMac( + device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId); } else if (device is CrossSigningKey) { - return mac == _calculateMac(device.ed25519Key, baseInfo + 'ed25519:' + device.publicKey); + return mac == + _calculateMac( + device.ed25519Key, baseInfo + 'ed25519:' + device.publicKey); } return false; }); diff --git a/test/device_keys_list_test.dart b/test/device_keys_list_test.dart index 71afb36..e8efe29 100644 --- a/test/device_keys_list_test.dart +++ b/test/device_keys_list_test.dart @@ -65,8 +65,7 @@ void main() { final key = DeviceKeys.fromJson(rawJson, null); rawJson.remove('verified'); rawJson.remove('blocked'); - expect(json.encode(key.toJson()), - json.encode(rawJson)); + expect(json.encode(key.toJson()), json.encode(rawJson)); expect(key.verified, false); expect(key.blocked, true); expect(json.encode(DeviceKeysList.fromJson(rawListJson, null).toJson()), From 1a8ddb2750dad77140fb701e6aaeb463df4f93c1 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 22 May 2020 13:22:28 +0200 Subject: [PATCH 09/64] fixes --- lib/src/utils/device_keys_list.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index c43b6da..928d1c2 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -281,7 +281,7 @@ class DeviceKeys extends _SignedKey { curve25519Key != null && ed25519Key != null; - Future setVerified(bool newVerified, Client client) { + Future setVerified(bool newVerified) { _verified = newVerified; return client.database ?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId); @@ -336,7 +336,7 @@ class DeviceKeys extends _SignedKey { blocked = json['blocked'] ?? false; } - KeyVerification startVerification(Client client) { + KeyVerification startVerification() { final request = KeyVerification(client: client, userId: userId, deviceId: deviceId); request.start(); From 280cd4fc162a1d115917b2d861901397291ee88a Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 23 May 2020 17:04:27 +0200 Subject: [PATCH 10/64] first SSSS stuff --- lib/src/client.dart | 15 +- lib/src/database/database.dart | 13 + lib/src/database/database.g.dart | 290 ++++++++++++++++++ lib/src/database/database.moor | 10 + lib/src/ssss.dart | 456 ++++++++++++++++++++++++++++ lib/src/utils/key_verification.dart | 28 +- pubspec.lock | 37 ++- pubspec.yaml | 6 +- 8 files changed, 845 insertions(+), 10 deletions(-) create mode 100644 lib/src/ssss.dart diff --git a/lib/src/client.dart b/lib/src/client.dart index 8f39895..5c75423 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -47,6 +47,7 @@ import 'package:pedantic/pedantic.dart'; import 'event.dart'; import 'room.dart'; +import 'ssss.dart'; import 'sync/event_update.dart'; import 'sync/room_update.dart'; import 'sync/user_update.dart'; @@ -79,6 +80,8 @@ class Client { bool enableE2eeRecovery; + SSSS ssss; + /// Create a client /// clientName = unique identifier of this client /// debug: Print debug output? @@ -86,6 +89,7 @@ class Client { /// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions Client(this.clientName, {this.debug = false, this.database, this.enableE2eeRecovery = false}) { + ssss = SSSS(this); onLoginStateChanged.stream.listen((loginState) { print('LoginState: ${loginState.toString()}'); }); @@ -1146,6 +1150,9 @@ class Client { if (toDeviceEvent.type.startsWith('m.key.verification.')) { _handleToDeviceKeyVerificationRequest(toDeviceEvent); } + if (toDeviceEvent.type.startsWith('m.secret.')) { + ssss.handleToDeviceEvent(toDeviceEvent); + } onToDeviceEvent.add(toDeviceEvent); } } @@ -2013,12 +2020,16 @@ class Client { Map message, { bool encrypted = true, List toUsers, + bool onlyVerified = false, }) async { if (encrypted && !encryptionEnabled) return; - // Don't send this message to blocked devices. + // Don't send this message to blocked devices, and if specified onlyVerified + // then only send it to verified devices if (deviceKeys.isNotEmpty) { deviceKeys.removeWhere((DeviceKeys deviceKeys) => - deviceKeys.blocked || deviceKeys.deviceId == deviceID); + deviceKeys.blocked || + deviceKeys.deviceId == deviceID || + (onlyVerified && !deviceKeys.verified)); if (deviceKeys.isEmpty) return; } diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index f83b15f..7a673d1 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -46,6 +46,7 @@ class Database extends _$Database { if (from == 3) { await m.createTable(userCrossSigningKeys); await m.createIndex(userCrossSigningKeysIndex); + await m.createTable(ssssCache); // mark all keys as outdated so that the cross signing keys will be fetched await m.issueCustomQuery( 'UPDATE user_device_keys SET outdated = true'); @@ -125,6 +126,14 @@ class Database extends _$Database { return res.first; } + Future getSSSSCache(int clientId, String type) async { + final res = await dbGetSSSSCache(clientId, type).get(); + if (res.isEmpty) { + return null; + } + return res.first; + } + Future> getRoomList(sdk.Client client, {bool onlyLeft = false}) async { final res = await (select(rooms) @@ -416,10 +425,14 @@ class Database extends _$Database { ..where((r) => r.clientId.equals(clientId))) .go(); await (delete(olmSessions)..where((r) => r.clientId.equals(clientId))).go(); + await (delete(userCrossSigningKeys) + ..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(ssssCache)..where((r) => r.clientId.equals(clientId))).go(); await (delete(clients)..where((r) => r.clientId.equals(clientId))).go(); } diff --git a/lib/src/database/database.g.dart b/lib/src/database/database.g.dart index daea006..437265c 100644 --- a/lib/src/database/database.g.dart +++ b/lib/src/database/database.g.dart @@ -4801,6 +4801,263 @@ class Presences extends Table with TableInfo { bool get dontWriteConstraints => true; } +class DbSSSSCache extends DataClass implements Insertable { + final int clientId; + final String type; + final String keyId; + final String content; + DbSSSSCache( + {@required this.clientId, + @required this.type, + @required this.keyId, + @required this.content}); + factory DbSSSSCache.fromData(Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final intType = db.typeSystem.forDartType(); + final stringType = db.typeSystem.forDartType(); + return DbSSSSCache( + clientId: + intType.mapFromDatabaseResponse(data['${effectivePrefix}client_id']), + type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type']), + keyId: + stringType.mapFromDatabaseResponse(data['${effectivePrefix}key_id']), + content: + stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']), + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || clientId != null) { + map['client_id'] = Variable(clientId); + } + if (!nullToAbsent || type != null) { + map['type'] = Variable(type); + } + if (!nullToAbsent || keyId != null) { + map['key_id'] = Variable(keyId); + } + if (!nullToAbsent || content != null) { + map['content'] = Variable(content); + } + return map; + } + + factory DbSSSSCache.fromJson(Map json, + {ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return DbSSSSCache( + clientId: serializer.fromJson(json['client_id']), + type: serializer.fromJson(json['type']), + keyId: serializer.fromJson(json['key_id']), + content: serializer.fromJson(json['content']), + ); + } + @override + Map toJson({ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'client_id': serializer.toJson(clientId), + 'type': serializer.toJson(type), + 'key_id': serializer.toJson(keyId), + 'content': serializer.toJson(content), + }; + } + + DbSSSSCache copyWith( + {int clientId, String type, String keyId, String content}) => + DbSSSSCache( + clientId: clientId ?? this.clientId, + type: type ?? this.type, + keyId: keyId ?? this.keyId, + content: content ?? this.content, + ); + @override + String toString() { + return (StringBuffer('DbSSSSCache(') + ..write('clientId: $clientId, ') + ..write('type: $type, ') + ..write('keyId: $keyId, ') + ..write('content: $content') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc(clientId.hashCode, + $mrjc(type.hashCode, $mrjc(keyId.hashCode, content.hashCode)))); + @override + bool operator ==(dynamic other) => + identical(this, other) || + (other is DbSSSSCache && + other.clientId == this.clientId && + other.type == this.type && + other.keyId == this.keyId && + other.content == this.content); +} + +class SsssCacheCompanion extends UpdateCompanion { + final Value clientId; + final Value type; + final Value keyId; + final Value content; + const SsssCacheCompanion({ + this.clientId = const Value.absent(), + this.type = const Value.absent(), + this.keyId = const Value.absent(), + this.content = const Value.absent(), + }); + SsssCacheCompanion.insert({ + @required int clientId, + @required String type, + @required String keyId, + @required String content, + }) : clientId = Value(clientId), + type = Value(type), + keyId = Value(keyId), + content = Value(content); + static Insertable custom({ + Expression clientId, + Expression type, + Expression keyId, + Expression content, + }) { + return RawValuesInsertable({ + if (clientId != null) 'client_id': clientId, + if (type != null) 'type': type, + if (keyId != null) 'key_id': keyId, + if (content != null) 'content': content, + }); + } + + SsssCacheCompanion copyWith( + {Value clientId, + Value type, + Value keyId, + Value content}) { + return SsssCacheCompanion( + clientId: clientId ?? this.clientId, + type: type ?? this.type, + keyId: keyId ?? this.keyId, + content: content ?? this.content, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (keyId.present) { + map['key_id'] = Variable(keyId.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + return map; + } +} + +class SsssCache extends Table with TableInfo { + final GeneratedDatabase _db; + final String _alias; + SsssCache(this._db, [this._alias]); + final VerificationMeta _clientIdMeta = const VerificationMeta('clientId'); + GeneratedIntColumn _clientId; + GeneratedIntColumn get clientId => _clientId ??= _constructClientId(); + GeneratedIntColumn _constructClientId() { + return GeneratedIntColumn('client_id', $tableName, false, + $customConstraints: 'NOT NULL REFERENCES clients(client_id)'); + } + + final VerificationMeta _typeMeta = const VerificationMeta('type'); + GeneratedTextColumn _type; + GeneratedTextColumn get type => _type ??= _constructType(); + GeneratedTextColumn _constructType() { + return GeneratedTextColumn('type', $tableName, false, + $customConstraints: 'NOT NULL'); + } + + final VerificationMeta _keyIdMeta = const VerificationMeta('keyId'); + GeneratedTextColumn _keyId; + GeneratedTextColumn get keyId => _keyId ??= _constructKeyId(); + GeneratedTextColumn _constructKeyId() { + return GeneratedTextColumn('key_id', $tableName, false, + $customConstraints: 'NOT NULL'); + } + + final VerificationMeta _contentMeta = const VerificationMeta('content'); + GeneratedTextColumn _content; + GeneratedTextColumn get content => _content ??= _constructContent(); + GeneratedTextColumn _constructContent() { + return GeneratedTextColumn('content', $tableName, false, + $customConstraints: 'NOT NULL'); + } + + @override + List get $columns => [clientId, type, keyId, content]; + @override + SsssCache get asDslTable => this; + @override + String get $tableName => _alias ?? 'ssss_cache'; + @override + final String actualTableName = 'ssss_cache'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('client_id')) { + context.handle(_clientIdMeta, + clientId.isAcceptableOrUnknown(data['client_id'], _clientIdMeta)); + } else if (isInserting) { + context.missing(_clientIdMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, type.isAcceptableOrUnknown(data['type'], _typeMeta)); + } else if (isInserting) { + context.missing(_typeMeta); + } + if (data.containsKey('key_id')) { + context.handle( + _keyIdMeta, keyId.isAcceptableOrUnknown(data['key_id'], _keyIdMeta)); + } else if (isInserting) { + context.missing(_keyIdMeta); + } + if (data.containsKey('content')) { + context.handle(_contentMeta, + content.isAcceptableOrUnknown(data['content'], _contentMeta)); + } else if (isInserting) { + context.missing(_contentMeta); + } + return context; + } + + @override + Set get $primaryKey => {}; + @override + DbSSSSCache map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return DbSSSSCache.fromData(data, _db, prefix: effectivePrefix); + } + + @override + SsssCache createAlias(String alias) { + return SsssCache(_db, alias); + } + + @override + List get customConstraints => const ['UNIQUE(client_id, type)']; + @override + bool get dontWriteConstraints => true; +} + class DbFile extends DataClass implements Insertable { final String mxcUri; final Uint8List bytes; @@ -5087,6 +5344,8 @@ abstract class _$Database extends GeneratedDatabase { Index _presencesIndex; Index get presencesIndex => _presencesIndex ??= Index('presences_index', 'CREATE INDEX presences_index ON presences(client_id);'); + SsssCache _ssssCache; + SsssCache get ssssCache => _ssssCache ??= SsssCache(this); Files _files; Files get files => _files ??= Files(this); DbClient _rowToDbClient(QueryRow row) { @@ -5498,6 +5757,36 @@ abstract class _$Database extends GeneratedDatabase { ); } + Future storeSSSSCache( + int client_id, String type, String key_id, String content) { + return customInsert( + 'INSERT OR REPLACE INTO ssss_cache (client_id, type, key_id, content) VALUES (:client_id, :type, :key_id, :content)', + variables: [ + Variable.withInt(client_id), + Variable.withString(type), + Variable.withString(key_id), + Variable.withString(content) + ], + updates: {ssssCache}, + ); + } + + DbSSSSCache _rowToDbSSSSCache(QueryRow row) { + return DbSSSSCache( + clientId: row.readInt('client_id'), + type: row.readString('type'), + keyId: row.readString('key_id'), + content: row.readString('content'), + ); + } + + Selectable dbGetSSSSCache(int client_id, String type) { + return customSelect( + 'SELECT * FROM ssss_cache WHERE client_id = :client_id AND type = :type', + variables: [Variable.withInt(client_id), Variable.withString(type)], + readsFrom: {ssssCache}).map(_rowToDbSSSSCache); + } + Future insertClient( String name, String homeserver_url, @@ -5924,6 +6213,7 @@ abstract class _$Database extends GeneratedDatabase { roomAccountDataIndex, presences, presencesIndex, + ssssCache, files ]; } diff --git a/lib/src/database/database.moor b/lib/src/database/database.moor index 049c792..5c84841 100644 --- a/lib/src/database/database.moor +++ b/lib/src/database/database.moor @@ -74,6 +74,14 @@ CREATE TABLE inbound_group_sessions ( ) AS DbInboundGroupSession; CREATE INDEX inbound_group_sessions_index ON inbound_group_sessions(client_id); +CREATE TABLE ssss_cache ( + client_id INTEGER NOT NULL REFERENCES clients(client_id), + type TEXT NOT NULL, + key_id TEXT NOT NULL, + content TEXT NOT NULL, + UNIQUE(client_id, type) +) AS DbSSSSCache; + CREATE TABLE rooms ( client_id INTEGER NOT NULL REFERENCES clients(client_id), room_id TEXT NOT NULL, @@ -186,6 +194,8 @@ setVerifiedUserCrossSigningKey: UPDATE user_cross_signing_keys SET verified = :v setBlockedUserCrossSigningKey: UPDATE user_cross_signing_keys SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; storeUserCrossSigningKey: INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :verified, :blocked); removeUserCrossSigningKey: DELETE FROM user_cross_signing_keys WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; +storeSSSSCache: INSERT OR REPLACE INTO ssss_cache (client_id, type, key_id, content) VALUES (:client_id, :type, :key_id, :content); +dbGetSSSSCache: SELECT * FROM ssss_cache WHERE client_id = :client_id AND type = :type; insertClient: INSERT INTO clients (name, homeserver_url, token, user_id, device_id, device_name, prev_batch, olm_account) VALUES (:name, :homeserver_url, :token, :user_id, :device_id, :device_name, :prev_batch, :olm_account); ensureRoomExists: INSERT OR IGNORE INTO rooms (client_id, room_id, membership) VALUES (:client_id, :room_id, :membership); setRoomPrevBatch: UPDATE rooms SET prev_batch = :prev_batch WHERE client_id = :client_id AND room_id = :room_id; diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart new file mode 100644 index 0000000..62f2348 --- /dev/null +++ b/lib/src/ssss.dart @@ -0,0 +1,456 @@ +import 'dart:typed_data'; +import 'dart:convert'; + +import 'package:encrypt/encrypt.dart'; +import 'package:crypto/crypto.dart'; +import "package:base58check/base58.dart"; +import 'package:password_hash/password_hash.dart'; +import 'package:random_string/random_string.dart'; + +import 'client.dart'; +import 'account_data.dart'; +import 'utils/device_keys_list.dart'; +import 'utils/to_device_event.dart'; + +const CACHE_TYPES = [ + 'm.cross_signing.self_signing', + 'm.cross_signing.user_signing', + 'm.megolm_backup.v1' +]; +const ZERO_STR = + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'; +const BASE58_ALPHABET = + '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; +const base58 = const Base58Codec(BASE58_ALPHABET); +const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; +const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm +const AES_BLOCKSIZE = 16; + +class SSSS { + final Client client; + final pendingShareRequests = {}; + SSSS(this.client); + + static _DerivedKeys deriveKeys(Uint8List key, String name) { + final zerosalt = Uint8List(8); + final prk = Hmac(sha256, zerosalt).convert(key); + final b = Uint8List(1); + b[0] = 1; + final aesKey = Hmac(sha256, prk.bytes).convert(utf8.encode(name) + b); + b[0] = 2; + final a = aesKey.bytes + utf8.encode(name) + b; + final hmacKey = + Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b); + return _DerivedKeys(aesKey: aesKey.bytes, hmacKey: hmacKey.bytes); + } + + static _Encrypted encryptAes(String data, Uint8List key, String name, + [String ivStr]) { + Uint8List iv; + if (ivStr != null) { + iv = base64.decode(ivStr); + } else { + iv = Uint8List.fromList(SecureRandom(16).bytes); + } + // we need to clear bit 63 of the IV + iv[8] &= 0x7f; + + final keys = deriveKeys(key, name); + + // workaround for https://github.com/leocavalcante/encrypt/issues/136 + var plain = Uint8List.fromList(utf8.encode(data)); + final bytesMissing = AES_BLOCKSIZE - (plain.lengthInBytes % AES_BLOCKSIZE); + if (bytesMissing != AES_BLOCKSIZE) { + // we want to be able to modify it + final oldPlain = plain; + plain = Uint8List(plain.lengthInBytes + bytesMissing); + for (var i = 0; i < oldPlain.lengthInBytes; i++) { + plain[i] = oldPlain[i]; + } + } + var ciphertext = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null) + .encrypt(plain, iv: IV(iv)).bytes; + if (bytesMissing != AES_BLOCKSIZE) { + // chop off those extra bytes again + ciphertext = ciphertext.sublist(0, plain.length - bytesMissing); + } + + final hmac = Hmac(sha256, keys.hmacKey).convert(ciphertext); + + return _Encrypted( + iv: base64.encode(iv), + ciphertext: base64.encode(ciphertext), + mac: base64.encode(hmac.bytes)); + } + + static String decryptAes(_Encrypted data, Uint8List key, String name) { + final keys = deriveKeys(key, name); + final hmac = base64 + .encode(Hmac(sha256, keys.hmacKey) + .convert(base64.decode(data.ciphertext)) + .bytes) + .replaceAll(RegExp(r'=+$'), ''); + if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) { + throw 'Bad MAC'; + } + // workaround for https://github.com/leocavalcante/encrypt/issues/136 + var cipher = base64.decode(data.ciphertext); + final bytesMissing = AES_BLOCKSIZE - (cipher.lengthInBytes % AES_BLOCKSIZE); + if (bytesMissing != AES_BLOCKSIZE) { + // we want to be able to modify it + final oldCipher = cipher; + cipher = Uint8List(cipher.lengthInBytes + bytesMissing); + for (var i = 0; i < oldCipher.lengthInBytes; i++) { + cipher[i] = oldCipher[i]; + } + } + final decipher = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null).decrypt( + Encrypted(cipher), + iv: IV(base64.decode(data.iv))); + if (bytesMissing != AES_BLOCKSIZE) { + // chop off those extra bytes again + return String.fromCharCodes(decipher.sublist(0, decipher.length - bytesMissing)); + } + return String.fromCharCodes(decipher); + } + + static Uint8List decodeRecoveryKey(String recoveryKey) { + final result = base58.decode(recoveryKey.replaceAll(' ', '')); + + var parity = 0; + for (final b in result) { + parity ^= b; + } + if (parity != 0) { + throw 'Incorrect parity'; + } + + for (var i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; i++) { + if (result[i] != OLM_RECOVERY_KEY_PREFIX[i]) { + throw 'Incorrect prefix'; + } + } + + if (result.length != + OLM_RECOVERY_KEY_PREFIX.length + OLM_PRIVATE_KEY_LENGTH + 1) { + throw 'Incorrect length'; + } + + return Uint8List.fromList(result.sublist(OLM_RECOVERY_KEY_PREFIX.length, + OLM_RECOVERY_KEY_PREFIX.length + OLM_PRIVATE_KEY_LENGTH)); + } + + static Uint8List keyFromPassword(String password, _PasswordInfo info) { + if (info.algorithm != 'm.pbkdf2') { + throw 'Unknown algorithm'; + } + final generator = PBKDF2(hashAlgorithm: sha512); + return Uint8List.fromList(generator.generateKey( + password, info.salt, info.iterations, info.bits != null ? info.bits / 8 : 32)); + } + + String get defaultKeyId { + final keyData = client.accountData['m.secret_storage.default_key']; + if (keyData == null || !(keyData.content['key'] is String)) { + return null; + } + return keyData.content['key']; + } + + AccountData getKey(String keyId) { + return client.accountData['m.secret_storage.key.${keyId}']; + } + + bool checkKey(Uint8List key, AccountData keyData) { + final info = keyData.content; + if (info['algorithm'] == 'm.secret_storage.v1.aes-hmac-sha2') { + if ((info['mac'] is String) && (info['iv'] is String)) { + final encrypted = encryptAes(ZERO_STR, key, '', info['iv']); + return info['mac'].replaceAll(RegExp(r'=+$'), '') == + encrypted.mac.replaceAll(RegExp(r'=+$'), ''); + } else { + // no real information about the key, assume it is valid + return true; + } + } else { + throw 'Unknown Algorithm'; + } + } + + Future getCached(String type) async { + if (client.database == null) { + return null; + } + final ret = await client.database.getSSSSCache(client.id, type); + if (ret == null) { + return null; + } + // check if it is still valid + final keys = keyIdsFromType(type); + if (keys.contains(ret.keyId)) { + return ret.content; + } + return null; + } + + Future getStored(String type, String keyId, Uint8List key) async { + final secretInfo = client.accountData[type]; + if (secretInfo == null) { + throw 'Not found'; + } + if (!(secretInfo.content['encrypted'] is Map)) { + throw 'Content is not encrypted'; + } + if (!(secretInfo.content['encrypted'][keyId] is Map)) { + throw 'Wrong / unknown key'; + } + final enc = secretInfo.content['encrypted'][keyId]; + final encryptInfo = _Encrypted( + iv: enc['iv'], ciphertext: enc['ciphertext'], mac: enc['mac']); + final decrypted = decryptAes(encryptInfo, key, type); + if (CACHE_TYPES.contains(type) && client.database != null) { + // cache the thing + await client.database.storeSSSSCache(client.id, type, keyId, decrypted); + } + return decrypted; + } + + Future store( + String type, String secret, String keyId, Uint8List key) async { + final encrypted = encryptAes(secret, key, type); + final content = { + 'encrypted': {}, + }; + content['encrypted'][keyId] = { + 'iv': encrypted.iv, + 'ciphertext': encrypted.ciphertext, + 'mac': encrypted.mac, + }; + // store the thing in your account data + await client.jsonRequest( + type: HTTPType.PUT, + action: '/client/r0/user/${client.userID}/account_data/${type}', + data: content, + ); + if (CACHE_TYPES.contains(type) && client.database != null) { + // cache the thing + await client.database.storeSSSSCache(client.id, type, keyId, secret); + } + } + + Future maybeRequestAll(List devices) async { + for (final type in CACHE_TYPES) { + final secret = await getCached(type); + if (secret == null) { + await request(type, devices); + } + } + } + + Future request(String type, List devices) async { + // only send to own, verified devices + print('[SSSS] Requesting type ${type}...'); + devices.removeWhere((DeviceKeys d) => + d.userId != client.userID || + !d.verified || + d.blocked || + d.deviceId == client.deviceID); + if (devices.isEmpty) { + print('[SSSS] Warn: No devices'); + return; + } + final requestId = + randomString(512) + DateTime.now().millisecondsSinceEpoch.toString(); + final request = _ShareRequest( + requestId: requestId, + type: type, + devices: devices, + ); + pendingShareRequests[requestId] = request; + await client.sendToDevice(devices, 'm.secret.request', { + 'action': 'request', + 'requesting_device_id': client.deviceID, + 'request_id': requestId, + 'name': type, + }); + } + + Future handleToDeviceEvent(ToDeviceEvent event) async { + if (event.type == 'm.secret.request') { + // got a request to share a secret + print('[SSSS] Received sharing request...'); + if (event.sender != client.userID || + !client.userDeviceKeys.containsKey(client.userID)) { + print('[SSSS] Not sent by us'); + return; // we aren't asking for it ourselves, so ignore + } + if (event.content['action'] != 'request') { + print('[SSSS] it is actually a cancelation'); + return; // not actually requesting, so ignore + } + final device = client.userDeviceKeys[client.userID] + .deviceKeys[event.content['requesting_device_id']]; + if (device == null || !device.verified || device.blocked) { + print('[SSSS] Unknown / unverified devices, ignoring'); + return; // nope....unknown or untrusted device + } + // alright, all seems fine...let's check if we actually have the secret they are asking for + final type = event.content['name']; + final secret = await getCached(type); + if (secret == null) { + print('[SSSS] We don\'t have the secret for ${type} ourself, ignoring'); + return; // seems like we don't have this, either + } + // okay, all checks out...time to share this secret! + print('[SSSS] Replying with secret for ${type}'); + await client.sendToDevice( + [device], + 'm.secret.send', + { + 'request_id': event.content['request_id'], + 'secret': secret, + }); + } else if (event.type == 'm.secret.send') { + // receiving a secret we asked for + print('[SSSS] Received shared secret...'); + if (event.sender != client.userID || + !pendingShareRequests.containsKey(event.content['request_id'])) { + print('[SSSS] Not by us or unknown request'); + return; // we have no idea what we just received + } + final request = pendingShareRequests[event.content['request_id']]; + // alright, as we received a known request id we know that it must have originated from a trusted source + pendingShareRequests.remove(request.requestId); + if (!(event.content['secret'] is String)) { + print('[SSSS] Secret wasn\'t a string'); + return; // the secret wasn't a string....wut? + } + if (request.start.add(Duration(minutes: 15)).isBefore(DateTime.now())) { + print('[SSSS] Request is too far in the past'); + return; // our request is more than 15min in the past...better not trust it anymore + } + print('[SSSS] Secret for type ${request.type} is ok, storing it'); + if (client.database != null) { + final keyId = keyIdFromType(request.type); + if (keyId != null) { + await client.database.storeSSSSCache( + client.id, request.type, keyId, event.content['secret']); + } + } + } + } + + Set keyIdsFromType(String type) { + final data = client.accountData[type]; + if (data == null) { + return null; + } + if (data.content['encrypted'] is Map) { + final keys = Set(); + String maybeKey; + for (final key in data.content['encrypted'].keys) { + keys.add(key); + } + return keys; + } + return null; + } + + String keyIdFromType(String type) { + final keys = keyIdsFromType(type); + if (keys == null || keys.isEmpty) { + return null; + } + if (keys.contains(defaultKeyId)) { + return defaultKeyId; + } + return keys.first; + } + + OpenSSSS open([String identifier]) { + if (identifier == null) { + identifier = defaultKeyId; + } + if (identifier == null) { + throw 'Dont know what to open'; + } + final keyToOpen = keyIdFromType(identifier) ?? identifier; + if (keyToOpen == null) { + throw 'No key found to open'; + } + final key = getKey(keyToOpen); + if (key == null) { + throw 'Unknown key to open'; + } + return OpenSSSS(ssss: this, keyId: keyToOpen, keyData: key); + } +} + +class _ShareRequest { + final String requestId; + final String type; + final List devices; + final DateTime start; + + _ShareRequest({this.requestId, this.type, this.devices}) + : start = DateTime.now(); +} + +class _Encrypted { + final String iv; + final String ciphertext; + final String mac; + + _Encrypted({this.iv, this.ciphertext, this.mac}); +} + +class _DerivedKeys { + final Uint8List aesKey; + final Uint8List hmacKey; + + _DerivedKeys({this.aesKey, this.hmacKey}); +} + +class _PasswordInfo { + final String algorithm; + final String salt; + final int iterations; + final int bits; + + _PasswordInfo({this.algorithm, this.salt, this.iterations, this.bits}); +} + +class OpenSSSS { + final SSSS ssss; + final String keyId; + final AccountData keyData; + OpenSSSS({this.ssss, this.keyId, this.keyData}); + Uint8List privateKey; + + bool get isUnlocked => privateKey != null; + + void unlock({String password, String recoveryKey}) { + if (password != null) { + privateKey = SSSS.keyFromPassword( + password, + _PasswordInfo( + algorithm: keyData.content['passphrase']['algorithm'], + salt: keyData.content['passphrase']['salt'], + iterations: keyData.content['passphrase']['iterations'], + bits: keyData.content['passphrase']['bits'])); + } else if (recoveryKey != null) { + privateKey = SSSS.decodeRecoveryKey(recoveryKey); + } else { + throw 'Nothing specified'; + } + // verify the validity of the key + if (!ssss.checkKey(privateKey, keyData)) { + privateKey = null; + throw 'Inalid key'; + } + } + + Future getStored(String type) async { + return await ssss.getStored(type, keyId, privateKey); + } +} diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 614c970..9f30d82 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -132,7 +132,8 @@ class KeyVerification { Future start() async { if (room == null) { - transactionId = randomString(512); + transactionId = + randomString(512) + DateTime.now().millisecondsSinceEpoch.toString(); } await send('m.key.verification.request', { 'methods': VERIFICATION_METHODS, @@ -323,16 +324,31 @@ class KeyVerification { } } // okay, we reached this far, so all the devices are verified! + var verifiedMasterKey = false; + final verifiedUserDevices = []; for (final verifyDeviceId in verifiedDevices) { if (client.userDeviceKeys[userId].deviceKeys .containsKey(verifyDeviceId)) { - await client.userDeviceKeys[userId].deviceKeys[verifyDeviceId] - .setVerified(true); + final key = client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]; + await key.setVerified(true); + verifiedUserDevices.add(key); } else if (client.userDeviceKeys[userId].crossSigningKeys .containsKey(verifyDeviceId)) { - await client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId] - .setVerified(true); - // TODO: sign the other persons master key + final key = + client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId]; + await key.setVerified(true); + if (key.usage.contains('master')) { + verifiedMasterKey = true; + } + } + } + if (verifiedMasterKey) { + if (userId == client.userID) { + // it was our own master key, let's request the cross signing keys + // we do it in the background, thus no await needed here + client.ssss.maybeRequestAll(verifiedUserDevices); + } else { + // it was someone elses master key, let's sign it } } } diff --git a/pubspec.lock b/pubspec.lock index 7f54f84..509c244 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -36,6 +36,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" async: dependency: transitive description: @@ -43,6 +50,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.4.1" + base58check: + dependency: "direct main" + description: + name: base58check + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" boolean_selector: dependency: transitive description: @@ -134,6 +148,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.4" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" code_builder: dependency: transitive description: @@ -163,7 +184,7 @@ packages: source: hosted version: "0.13.9" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto url: "https://pub.dartlang.org" @@ -183,6 +204,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.6" + encrypt: + dependency: "direct main" + description: + name: encrypt + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" ffi: dependency: transitive description: @@ -397,6 +425,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.9.3" + password_hash: + dependency: "direct main" + description: + name: password_hash + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" path: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1fb7673..d5a29ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,10 @@ dependencies: html_unescape: ^1.0.1+3 moor: ^3.0.2 random_string: ^2.0.1 + encrypt: ^4.0.1 + crypto: ^2.1.4 + base58check: ^1.0.1 + password_hash: ^2.0.0 olm: git: @@ -33,4 +37,4 @@ dev_dependencies: moor_generator: ^3.0.0 build_runner: ^1.5.2 pedantic: ^1.9.0 - moor_ffi: ^0.5.0 \ No newline at end of file + moor_ffi: ^0.5.0 From 5fde85cbfa807b4af914caadf71a79177ad81b1e Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 23 May 2020 17:05:55 +0200 Subject: [PATCH 11/64] format --- lib/src/ssss.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart index 62f2348..856229b 100644 --- a/lib/src/ssss.dart +++ b/lib/src/ssss.dart @@ -69,7 +69,8 @@ class SSSS { } } var ciphertext = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null) - .encrypt(plain, iv: IV(iv)).bytes; + .encrypt(plain, iv: IV(iv)) + .bytes; if (bytesMissing != AES_BLOCKSIZE) { // chop off those extra bytes again ciphertext = ciphertext.sublist(0, plain.length - bytesMissing); @@ -104,12 +105,12 @@ class SSSS { cipher[i] = oldCipher[i]; } } - final decipher = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null).decrypt( - Encrypted(cipher), - iv: IV(base64.decode(data.iv))); + final decipher = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null) + .decrypt(Encrypted(cipher), iv: IV(base64.decode(data.iv))); if (bytesMissing != AES_BLOCKSIZE) { // chop off those extra bytes again - return String.fromCharCodes(decipher.sublist(0, decipher.length - bytesMissing)); + return String.fromCharCodes( + decipher.sublist(0, decipher.length - bytesMissing)); } return String.fromCharCodes(decipher); } @@ -145,8 +146,8 @@ class SSSS { throw 'Unknown algorithm'; } final generator = PBKDF2(hashAlgorithm: sha512); - return Uint8List.fromList(generator.generateKey( - password, info.salt, info.iterations, info.bits != null ? info.bits / 8 : 32)); + return Uint8List.fromList(generator.generateKey(password, info.salt, + info.iterations, info.bits != null ? info.bits / 8 : 32)); } String get defaultKeyId { From c13f66c85fa42c9b03d5fb15cbb875f28fdf5850 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 25 May 2020 15:30:53 +0200 Subject: [PATCH 12/64] in theory sign others keys --- lib/src/client.dart | 8 ++ lib/src/cross_signing.dart | 126 +++++++++++++++++++++++++++ lib/src/ssss.dart | 25 ++++-- lib/src/utils/device_keys_list.dart | 42 +++++++-- lib/src/utils/key_verification.dart | 127 ++++++++++++++++------------ 5 files changed, 261 insertions(+), 67 deletions(-) create mode 100644 lib/src/cross_signing.dart diff --git a/lib/src/client.dart b/lib/src/client.dart index c5e8068..42f348d 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -48,6 +48,7 @@ import 'package:pedantic/pedantic.dart'; import 'event.dart'; import 'room.dart'; import 'ssss.dart'; +import 'cross_signing.dart'; import 'sync/event_update.dart'; import 'sync/room_update.dart'; import 'sync/user_update.dart'; @@ -81,6 +82,7 @@ class Client { bool enableE2eeRecovery; SSSS ssss; + CrossSigning crossSigning; /// Create a client /// clientName = unique identifier of this client @@ -90,6 +92,7 @@ class Client { Client(this.clientName, {this.debug = false, this.database, this.enableE2eeRecovery = false}) { ssss = SSSS(this); + crossSigning = CrossSigning(this); onLoginStateChanged.stream.listen((loginState) { print('LoginState: ${loginState.toString()}'); }); @@ -1845,6 +1848,11 @@ class Client { return payload; } + /// Just gets the signature of a string + String signString(String s) { + return _olmAccount.sign(s); + } + /// Checks the signature of a signed json object. bool checkJsonSignature(String key, Map signedJson, String userId, String deviceId) { diff --git a/lib/src/cross_signing.dart b/lib/src/cross_signing.dart new file mode 100644 index 0000000..af1c3fb --- /dev/null +++ b/lib/src/cross_signing.dart @@ -0,0 +1,126 @@ +import 'dart:typed_data'; +import 'dart:convert'; + +import 'package:olm/olm.dart' as olm; + +import 'client.dart'; +import 'utils/device_keys_list.dart'; + +const SELF_SIGNING_KEY = 'm.cross_signing.self_signing'; +const USER_SIGNING_KEY = 'm.cross_signing.user_signing'; +const MASTER_KEY = 'm.cross_signing.master'; + +class CrossSigning { + final Client client; + CrossSigning(this.client); + + bool get enabled => + client.accountData[SELF_SIGNING_KEY] != null && + client.accountData[USER_SIGNING_KEY] != null && + client.accountData[MASTER_KEY] != null; + + Future isCached() async { + if (!enabled) { + return false; + } + return (await client.ssss.getCached(SELF_SIGNING_KEY)) != null && + (await client.ssss.getCached(USER_SIGNING_KEY)) != null; + } + + bool signable(List keys) { + for (final key in keys) { + if (key is CrossSigningKey && key.usage.contains('master')) { + return true; + } + if (key.userId == client.userID && + (key is DeviceKeys) && + key.identifier != client.deviceID) { + return true; + } + } + return false; + } + + Future sign(List keys) async { + Uint8List selfSigningKey; + Uint8List userSigningKey; + final signatures = {}; + var signedKey = false; + final addSignature = + (SignedKey key, SignedKey signedWith, String signature) { + if (key == null || signedWith == null || signature == null) { + return; + } + if (!signatures.containsKey(key.userId)) { + signatures[key.userId] = {}; + } + if (!signatures[key.userId].containsKey(key.identifier)) { + signatures[key.userId][key.identifier] = key.toJson(); + } + if (!signatures[key.userId][key.identifier].containsKey('signatures')) { + signatures[key.userId][key.identifier] + ['signatures'] = {}; + } + if (!signatures[key.userId][key.identifier]['signatures'] + .containsKey(signedWith.userId)) { + signatures[key.userId][key.identifier]['signatures'] + [signedWith.userId] = {}; + } + signatures[key.userId][key.identifier]['signatures'][signedWith.userId] + [signedWith.identifier] = signature; + signedKey = true; + }; + for (final key in keys) { + if (key.userId == client.userID) { + // we are singing a key of ourself + if (key is CrossSigningKey) { + if (key.usage.contains('master')) { + // okay, we'll sign our own master key + final signature = client.signString(key.signingContent); + addSignature( + key, + client.userDeviceKeys[client.userID].deviceKeys[client.deviceID], + signature); + } + // we don't care about signing other cross-signing keys + } else if (key.identifier != client.deviceID) { + // okay, we'll sign a device key with our self signing key + selfSigningKey ??= + base64.decode(await client.ssss.getCached(SELF_SIGNING_KEY) ?? ''); + if (selfSigningKey != null) { + final signature = _sign(key.signingContent, selfSigningKey); + addSignature(key, client.userDeviceKeys[client.userID].selfSigningKey, + signature); + } + } + } else if (key is CrossSigningKey && key.usage.contains('master')) { + // we are signing someone elses master key + userSigningKey ??= + base64.decode(await client.ssss.getCached(USER_SIGNING_KEY) ?? ''); + if (userSigningKey != null) { + final signature = _sign(key.signingContent, userSigningKey); + addSignature( + key, client.userDeviceKeys[client.userID].userSigningKey, signature); + } + } + } + if (signedKey) { + // post our new keys! + await client.jsonRequest( + type: HTTPType.POST, + action: '/client/r0/keys/signatures/upload', + data: signatures, + ); + } + } + + String _sign(String canonicalJson, Uint8List key) { + final keyObj = olm.PkSigning(); + keyObj.init_with_seed(key); + try { + return keyObj.sign(canonicalJson); + } finally { + keyObj.free(); + } + } +} diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart index 856229b..9657d41 100644 --- a/lib/src/ssss.dart +++ b/lib/src/ssss.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:encrypt/encrypt.dart'; import 'package:crypto/crypto.dart'; -import "package:base58check/base58.dart"; +import 'package:base58check/base58.dart'; import 'package:password_hash/password_hash.dart'; import 'package:random_string/random_string.dart'; @@ -21,7 +21,7 @@ const ZERO_STR = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'; const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; -const base58 = const Base58Codec(BASE58_ALPHABET); +const base58 = Base58Codec(BASE58_ALPHABET); const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm const AES_BLOCKSIZE = 16; @@ -38,7 +38,6 @@ class SSSS { b[0] = 1; final aesKey = Hmac(sha256, prk.bytes).convert(utf8.encode(name) + b); b[0] = 2; - final a = aesKey.bytes + utf8.encode(name) + b; final hmacKey = Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b); return _DerivedKeys(aesKey: aesKey.bytes, hmacKey: hmacKey.bytes); @@ -239,6 +238,15 @@ class SSSS { } } + Future maybeCacheAll(String keyId, Uint8List key) async { + for (final type in CACHE_TYPES) { + final secret = await getCached(type); + if (secret == null) { + await getStored(type, keyId, key); + } + } + } + Future maybeRequestAll(List devices) async { for (final type in CACHE_TYPES) { final secret = await getCached(type); @@ -347,8 +355,7 @@ class SSSS { return null; } if (data.content['encrypted'] is Map) { - final keys = Set(); - String maybeKey; + final Set keys = {}; for (final key in data.content['encrypted'].keys) { keys.add(key); } @@ -369,9 +376,7 @@ class SSSS { } OpenSSSS open([String identifier]) { - if (identifier == null) { - identifier = defaultKeyId; - } + identifier ??= defaultKeyId; if (identifier == null) { throw 'Dont know what to open'; } @@ -454,4 +459,8 @@ class OpenSSSS { Future getStored(String type) async { return await ssss.getStored(type, keyId, privateKey); } + + Future maybeCacheAll() async { + await ssss.maybeCacheAll(keyId, privateKey); + } } diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 928d1c2..75ec0c2 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -15,6 +15,28 @@ class DeviceKeysList { Map deviceKeys = {}; Map crossSigningKeys = {}; + SignedKey getKey(String id) { + if (deviceKeys.containsKey(id)) { + return deviceKeys[id]; + } + if (crossSigningKeys.containsKey(id)) { + return crossSigningKeys[id]; + } + return null; + } + + CrossSigningKey getCrossSigningKey(String type) { + final keys = crossSigningKeys.values.where((k) => k.usage.contains(type)); + if (keys.isEmpty) { + return null; + } + return keys.first; + } + + CrossSigningKey get masterKey => getCrossSigningKey('master'); + CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing'); + CrossSigningKey get userSigningKey => getCrossSigningKey('user_signing'); + DeviceKeysList.fromDb( DbUserDeviceKey dbEntry, List childEntries, @@ -73,7 +95,7 @@ class DeviceKeysList { DeviceKeysList(this.userId); } -abstract class _SignedKey { +abstract class SignedKey { Client client; String userId; String identifier; @@ -106,7 +128,7 @@ abstract class _SignedKey { } } - String _getSigningContent() { + String get signingContent { final data = Map.from(content); // some old data might have the custom verified and blocked keys data.remove('verified'); @@ -121,7 +143,7 @@ abstract class _SignedKey { final olmutil = olm.Utility(); var valid = false; try { - olmutil.ed25519_verify(pubKey, _getSigningContent(), signature); + olmutil.ed25519_verify(pubKey, signingContent, signature); valid = true; } catch (_) { // bad signature @@ -152,7 +174,7 @@ abstract class _SignedKey { continue; } final keyId = fullKeyId.substring('ed25519:'.length); - _SignedKey key; + SignedKey key; if (client.userDeviceKeys[otherUserId].deviceKeys.containsKey(keyId)) { key = client.userDeviceKeys[otherUserId].deviceKeys[keyId]; } else if (client.userDeviceKeys[otherUserId].crossSigningKeys @@ -204,6 +226,10 @@ abstract class _SignedKey { return false; } + Future setVerified(bool newVerified); + + Future setBlocked(bool newBlocked); + Map toJson() { final data = Map.from(content); // some old data may have the verified and blocked keys which are unneeded now @@ -216,19 +242,21 @@ abstract class _SignedKey { String toString() => json.encode(toJson()); } -class CrossSigningKey extends _SignedKey { +class CrossSigningKey extends SignedKey { String get publicKey => identifier; List usage; bool get isValid => userId != null && publicKey != null && keys != null && ed25519Key != null; + @override Future setVerified(bool newVerified) { _verified = newVerified; return client.database?.setVerifiedUserCrossSigningKey( newVerified, client.id, userId, publicKey); } + @override Future setBlocked(bool newBlocked) { blocked = newBlocked; return client.database?.setBlockedUserCrossSigningKey( @@ -267,7 +295,7 @@ class CrossSigningKey extends _SignedKey { } } -class DeviceKeys extends _SignedKey { +class DeviceKeys extends SignedKey { String get deviceId => identifier; List algorithms; Map unsigned; @@ -281,12 +309,14 @@ class DeviceKeys extends _SignedKey { curve25519Key != null && ed25519Key != null; + @override Future setVerified(bool newVerified) { _verified = newVerified; return client.database ?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId); } + @override Future setBlocked(bool newBlocked) { blocked = newBlocked; for (var room in client.rooms) { diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 9f30d82..d0d9d6c 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:random_string/random_string.dart'; import 'package:canonical_json/canonical_json.dart'; +import 'package:pedantic/pedantic.dart'; import 'package:olm/olm.dart' as olm; import 'device_keys_list.dart'; import '../client.dart'; @@ -44,6 +45,7 @@ import '../room.dart'; enum KeyVerificationState { askAccept, + askSSSS, waitingAccept, askSas, waitingSas, @@ -103,6 +105,8 @@ class KeyVerification { _KeyVerificationMethod method; List possibleMethods; Map startPaylaod; + String _nextAction; + List _verifiedDevices; DateTime lastActivity; String lastStep; @@ -130,7 +134,7 @@ class KeyVerification { : null); } - Future start() async { + Future sendStart() async { if (room == null) { transactionId = randomString(512) + DateTime.now().millisecondsSinceEpoch.toString(); @@ -141,6 +145,17 @@ class KeyVerification { }); startedVerification = true; setState(KeyVerificationState.waitingAccept); + lastActivity = DateTime.now(); + } + + Future start() async { + if (client.crossSigning.enabled && + !(await client.crossSigning.isCached())) { + setState(KeyVerificationState.askSSSS); + _nextAction = 'request'; + } else { + await sendStart(); + } } Future handlePayload(String type, Map payload, @@ -228,6 +243,29 @@ class KeyVerification { } } + Future openSSSS( + {String password, String recoveryKey, bool skip = false}) async { + final next = () { + if (_nextAction == 'request') { + sendStart(); + } else if (_nextAction == 'done') { + if (_verifiedDevices != null) { + // and now let's sign them all in the background + client.crossSigning.sign(_verifiedDevices); + } + setState(KeyVerificationState.done); + } + }; + if (skip) { + next(); + return; + } + final handle = client.ssss.open('m.cross_signing.user_signing'); + await handle.unlock(password: password, recoveryKey: recoveryKey); + await handle.maybeCacheAll(); + next(); + } + /// called when the user accepts an incoming verification Future acceptVerification() async { if (!(await verifyLastStep( @@ -293,8 +331,8 @@ class KeyVerification { } Future verifyKeys(Map keys, - Future Function(String, dynamic) verifier) async { - final verifiedDevices = []; + Future Function(String, SignedKey) verifier) async { + _verifiedDevices = []; if (!client.userDeviceKeys.containsKey(userId)) { await cancel('m.key_mismatch'); @@ -304,53 +342,48 @@ 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]))) { + final key = client.userDeviceKeys[userId].getKey(verifyDeviceId); + if (key != null) { + if (!(await verifier(keyInfo, key))) { await cancel('m.key_mismatch'); return; } - verifiedDevices.add(verifyDeviceId); - } else if (client.userDeviceKeys[userId].crossSigningKeys - .containsKey(verifyDeviceId)) { - // this is a cross signing key! - if (!(await verifier(keyInfo, - client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId]))) { - await cancel('m.key_mismatch'); - return; - } - verifiedDevices.add(verifyDeviceId); + _verifiedDevices.add(key); } } // okay, we reached this far, so all the devices are verified! var verifiedMasterKey = false; - final verifiedUserDevices = []; - for (final verifyDeviceId in verifiedDevices) { - if (client.userDeviceKeys[userId].deviceKeys - .containsKey(verifyDeviceId)) { - final key = client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]; - await key.setVerified(true); - verifiedUserDevices.add(key); - } else if (client.userDeviceKeys[userId].crossSigningKeys - .containsKey(verifyDeviceId)) { - final key = - client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId]; - await key.setVerified(true); - if (key.usage.contains('master')) { - verifiedMasterKey = true; - } + for (final key in _verifiedDevices) { + await key.setVerified(true); + if (key is CrossSigningKey && key.usage.contains('master')) { + verifiedMasterKey = true; } } - if (verifiedMasterKey) { - if (userId == client.userID) { - // it was our own master key, let's request the cross signing keys - // we do it in the background, thus no await needed here - client.ssss.maybeRequestAll(verifiedUserDevices); + if (verifiedMasterKey && userId == client.userID) { + // it was our own master key, let's request the cross signing keys + // we do it in the background, thus no await needed here + unawaited(client.ssss.maybeRequestAll( + _verifiedDevices.whereType().toList())); + } + await send('m.key.verification.done', {}); + + var askingSSSS = false; + if (client.crossSigning.enabled && + client.crossSigning.signable(_verifiedDevices)) { + // these keys can be signed! Let's do so + if (await client.crossSigning.isCached()) { + // and now let's sign them all in the background + unawaited(client.crossSigning.sign(_verifiedDevices)); } else { - // it was someone elses master key, let's sign it + askingSSSS = true; } } + if (askingSSSS) { + setState(KeyVerificationState.askSSSS); + _nextAction = 'done'; + } else { + setState(KeyVerificationState.done); + } } Future verifyActivity() async { @@ -729,22 +762,10 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { mac[entry.key] = entry.value; } } - await request.verifyKeys(mac, (String mac, dynamic device) async { - if (device is DeviceKeys) { - return mac == - _calculateMac( - device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId); - } else if (device is CrossSigningKey) { - return mac == - _calculateMac( - device.ed25519Key, baseInfo + 'ed25519:' + device.publicKey); - } - return false; + await request.verifyKeys(mac, (String mac, SignedKey key) async { + return mac == + _calculateMac(key.ed25519Key, baseInfo + 'ed25519:' + key.identifier); }); - await request.send('m.key.verification.done', {}); - if (request.state != KeyVerificationState.error) { - request.setState(KeyVerificationState.done); - } } String _makeCommitment(String pubKey, String canonicalJson) { From adc5591e54e532f2b0c9798987d25dc35cc89170 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 25 May 2020 15:58:37 +0200 Subject: [PATCH 13/64] add ed25519: --- lib/src/cross_signing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/cross_signing.dart b/lib/src/cross_signing.dart index af1c3fb..cb247cf 100644 --- a/lib/src/cross_signing.dart +++ b/lib/src/cross_signing.dart @@ -67,7 +67,7 @@ class CrossSigning { [signedWith.userId] = {}; } signatures[key.userId][key.identifier]['signatures'][signedWith.userId] - [signedWith.identifier] = signature; + ['ed25519:${signedWith.identifier}'] = signature; signedKey = true; }; for (final key in keys) { From 80c7125d1dbd82b7a1bf3c5306c23d617e58933f Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 25 May 2020 17:55:49 +0200 Subject: [PATCH 14/64] remove silly workaround for aes-ctr encrypt/decrypt --- lib/src/ssss.dart | 35 +++-------------------------------- pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart index 9657d41..1e6e311 100644 --- a/lib/src/ssss.dart +++ b/lib/src/ssss.dart @@ -24,7 +24,6 @@ const BASE58_ALPHABET = const base58 = Base58Codec(BASE58_ALPHABET); const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm -const AES_BLOCKSIZE = 16; class SSSS { final Client client; @@ -56,24 +55,10 @@ class SSSS { final keys = deriveKeys(key, name); - // workaround for https://github.com/leocavalcante/encrypt/issues/136 - var plain = Uint8List.fromList(utf8.encode(data)); - final bytesMissing = AES_BLOCKSIZE - (plain.lengthInBytes % AES_BLOCKSIZE); - if (bytesMissing != AES_BLOCKSIZE) { - // we want to be able to modify it - final oldPlain = plain; - plain = Uint8List(plain.lengthInBytes + bytesMissing); - for (var i = 0; i < oldPlain.lengthInBytes; i++) { - plain[i] = oldPlain[i]; - } - } - var ciphertext = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null) + final plain = Uint8List.fromList(utf8.encode(data)); + final ciphertext = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null) .encrypt(plain, iv: IV(iv)) .bytes; - if (bytesMissing != AES_BLOCKSIZE) { - // chop off those extra bytes again - ciphertext = ciphertext.sublist(0, plain.length - bytesMissing); - } final hmac = Hmac(sha256, keys.hmacKey).convert(ciphertext); @@ -94,23 +79,9 @@ class SSSS { throw 'Bad MAC'; } // workaround for https://github.com/leocavalcante/encrypt/issues/136 - var cipher = base64.decode(data.ciphertext); - final bytesMissing = AES_BLOCKSIZE - (cipher.lengthInBytes % AES_BLOCKSIZE); - if (bytesMissing != AES_BLOCKSIZE) { - // we want to be able to modify it - final oldCipher = cipher; - cipher = Uint8List(cipher.lengthInBytes + bytesMissing); - for (var i = 0; i < oldCipher.lengthInBytes; i++) { - cipher[i] = oldCipher[i]; - } - } + final cipher = base64.decode(data.ciphertext); final decipher = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null) .decrypt(Encrypted(cipher), iv: IV(base64.decode(data.iv))); - if (bytesMissing != AES_BLOCKSIZE) { - // chop off those extra bytes again - return String.fromCharCodes( - decipher.sublist(0, decipher.length - bytesMissing)); - } return String.fromCharCodes(decipher); } diff --git a/pubspec.lock b/pubspec.lock index 509c244..9c51531 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -210,7 +210,7 @@ packages: name: encrypt url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.0.2" ffi: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d5a29ba..324dcd4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: html_unescape: ^1.0.1+3 moor: ^3.0.2 random_string: ^2.0.1 - encrypt: ^4.0.1 + encrypt: ^4.0.2 crypto: ^2.1.4 base58check: ^1.0.1 password_hash: ^2.0.0 From 2ecf4151b958877866c45246c5e54ebc55fa7cae Mon Sep 17 00:00:00 2001 From: Sorunome Date: Tue, 26 May 2020 09:54:46 +0200 Subject: [PATCH 15/64] also send master key on verification and stuffs --- lib/src/cross_signing.dart | 19 +++++++++++-------- lib/src/utils/key_verification.dart | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/src/cross_signing.dart b/lib/src/cross_signing.dart index cb247cf..ab9f018 100644 --- a/lib/src/cross_signing.dart +++ b/lib/src/cross_signing.dart @@ -55,7 +55,9 @@ class CrossSigning { signatures[key.userId] = {}; } if (!signatures[key.userId].containsKey(key.identifier)) { - signatures[key.userId][key.identifier] = key.toJson(); + signatures[key.userId][key.identifier] = + Map.from(key.toJson()); + signatures[key.userId][key.identifier].remove('signatures'); } if (!signatures[key.userId][key.identifier].containsKey('signatures')) { signatures[key.userId][key.identifier] @@ -79,18 +81,19 @@ class CrossSigning { final signature = client.signString(key.signingContent); addSignature( key, - client.userDeviceKeys[client.userID].deviceKeys[client.deviceID], + client + .userDeviceKeys[client.userID].deviceKeys[client.deviceID], signature); } // we don't care about signing other cross-signing keys } else if (key.identifier != client.deviceID) { // okay, we'll sign a device key with our self signing key - selfSigningKey ??= - base64.decode(await client.ssss.getCached(SELF_SIGNING_KEY) ?? ''); + selfSigningKey ??= base64 + .decode(await client.ssss.getCached(SELF_SIGNING_KEY) ?? ''); if (selfSigningKey != null) { final signature = _sign(key.signingContent, selfSigningKey); - addSignature(key, client.userDeviceKeys[client.userID].selfSigningKey, - signature); + addSignature(key, + client.userDeviceKeys[client.userID].selfSigningKey, signature); } } } else if (key is CrossSigningKey && key.usage.contains('master')) { @@ -99,8 +102,8 @@ class CrossSigning { base64.decode(await client.ssss.getCached(USER_SIGNING_KEY) ?? ''); if (userSigningKey != null) { final signature = _sign(key.signingContent, userSigningKey); - addSignature( - key, client.userDeviceKeys[client.userID].userSigningKey, signature); + addSignature(key, client.userDeviceKeys[client.userID].userSigningKey, + signature); } } } diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index d0d9d6c..de501b4 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -362,8 +362,8 @@ class KeyVerification { if (verifiedMasterKey && userId == client.userID) { // it was our own master key, let's request the cross signing keys // we do it in the background, thus no await needed here - unawaited(client.ssss.maybeRequestAll( - _verifiedDevices.whereType().toList())); + unawaited(client.ssss + .maybeRequestAll(_verifiedDevices.whereType().toList())); } await send('m.key.verification.done', {}); @@ -727,6 +727,17 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { _calculateMac(client.fingerprintKey, baseInfo + deviceKeyId); keyList.add(deviceKeyId); + final masterKey = client.userDeviceKeys.containsKey(client.userID) + ? client.userDeviceKeys[client.userID].masterKey + : null; + if (masterKey != null && masterKey.verified) { + // we have our own master key verified, let's send it! + final masterKeyId = 'ed25519:${masterKey.publicKey}'; + mac[masterKeyId] = + _calculateMac(masterKey.publicKey, baseInfo + masterKeyId); + keyList.add(masterKeyId); + } + keyList.sort(); final keys = _calculateMac(keyList.join(','), baseInfo + 'KEY_IDS'); await request.send('m.key.verification.mac', { From 1c9da050c00a875434de43168d314820f347a303 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Tue, 26 May 2020 15:58:14 +0200 Subject: [PATCH 16/64] smoothen out verification and signature uploading --- lib/src/client.dart | 34 +++++++++++++++++++++++++++-- lib/src/ssss.dart | 4 ++++ lib/src/utils/key_verification.dart | 16 +++++++++----- pubspec.yaml | 9 ++++---- 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 42f348d..48952d6 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -148,6 +148,27 @@ class Client { /// Whether this client is able to encrypt and decrypt files. bool get fileEncryptionEnabled => true; + /// Wheather this session is unknown to others + bool get isUnknownSession { + if (!userDeviceKeys.containsKey(userID)) { + return true; + } + final masterKey = userDeviceKeys[userID].masterKey; + if (masterKey == null) { + return true; + } + if (!masterKey.directVerified) { + return true; + } + if (!userDeviceKeys[userID].deviceKeys.containsKey(deviceID)) { + return true; + } + if (!userDeviceKeys[userID].deviceKeys[deviceID].crossVerified) { + return true; + } + return false; + } + /// Warning! This endpoint is for testing only! set rooms(List newList) { print('Warning! This endpoint is for testing only!'); @@ -1028,8 +1049,9 @@ class Client { } on MatrixException catch (exception) { onError.add(exception); await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync); - } catch (exception) { + } catch (exception, stack) { print('Error during processing events: ' + exception.toString()); + print(stack); await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync); } } @@ -1108,8 +1130,9 @@ class Client { } void _cleanupKeyVerificationRequests() { + final actions = Function()>[]; for (final entry in _keyVerificationRequests.entries) { - (() async { + actions.add(() async { var dispose = entry.value.canceled || entry.value.state == KeyVerificationState.done || entry.value.state == KeyVerificationState.error; @@ -1120,6 +1143,13 @@ class Client { entry.value.dispose(); _keyVerificationRequests.remove(entry.key); } + }); + } + if (actions.isNotEmpty) { + (() async { + for (final a in actions) { + await a(); + } })(); } } diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart index 1e6e311..b350da4 100644 --- a/lib/src/ssss.dart +++ b/lib/src/ssss.dart @@ -431,6 +431,10 @@ class OpenSSSS { return await ssss.getStored(type, keyId, privateKey); } + Future store(String type, String secret) async { + await ssss.store(type, secret, keyId, privateKey); + } + Future maybeCacheAll() async { await ssss.maybeCacheAll(keyId, privateKey); } diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index de501b4..7508c30 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -120,6 +120,7 @@ class KeyVerification { {this.client, this.room, this.userId, String deviceId, this.onUpdate}) { lastActivity = DateTime.now(); _deviceId ??= deviceId; + print('Setting device id constructor: ' + _deviceId.toString()); } void dispose() { @@ -135,10 +136,6 @@ class KeyVerification { } Future sendStart() async { - if (room == null) { - transactionId = - randomString(512) + DateTime.now().millisecondsSinceEpoch.toString(); - } await send('m.key.verification.request', { 'methods': VERIFICATION_METHODS, 'timestamp': DateTime.now().millisecondsSinceEpoch, @@ -149,8 +146,12 @@ class KeyVerification { } Future start() async { + if (room == null) { + transactionId = + randomString(512) + DateTime.now().millisecondsSinceEpoch.toString(); + } if (client.crossSigning.enabled && - !(await client.crossSigning.isCached())) { + !(await client.crossSigning.isCached()) && !client.isUnknownSession) { setState(KeyVerificationState.askSSSS); _nextAction = 'request'; } else { @@ -165,6 +166,7 @@ class KeyVerification { switch (type) { case 'm.key.verification.request': _deviceId ??= payload['from_device']; + print('Setting device id request: ' + _deviceId.toString()); transactionId ??= eventId ?? payload['transaction_id']; // verify the timestamp final now = DateTime.now(); @@ -200,6 +202,7 @@ class KeyVerification { break; case 'm.key.verification.start': _deviceId ??= payload['from_device']; + print('Setting device id start: ' + _deviceId.toString()); transactionId ??= eventId ?? payload['transaction_id']; if (!(await verifyLastStep(['m.key.verification.request', null]))) { return; // abort @@ -353,6 +356,7 @@ class KeyVerification { } // okay, we reached this far, so all the devices are verified! var verifiedMasterKey = false; + final wasUnknownSession = client.isUnknownSession; for (final key in _verifiedDevices) { await key.setVerified(true); if (key is CrossSigningKey && key.usage.contains('master')) { @@ -374,7 +378,7 @@ class KeyVerification { if (await client.crossSigning.isCached()) { // and now let's sign them all in the background unawaited(client.crossSigning.sign(_verifiedDevices)); - } else { + } else if (!wasUnknownSession) { askingSSSS = true; } } diff --git a/pubspec.yaml b/pubspec.yaml index 324dcd4..51d3e62 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,12 +24,13 @@ dependencies: olm: git: url: https://gitlab.com/famedly/libraries/dart-olm.git - ref: 1.x.y + ref: 8749474d611f02a89893e067b6e479ebfd40c51d matrix_file_e2ee: - git: - url: https://gitlab.com/famedly/libraries/matrix_file_e2ee.git - ref: 1.x.y + path: /home/sorunome/repos/famedly/matrix_file_e2ee +# git: +# url: https://gitlab.com/famedly/libraries/matrix_file_e2ee.git +# ref: 1.x.y dev_dependencies: test: ^1.0.0 From fabffea1cd0747aa8ebe26b81b27d16a96265206 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 27 May 2020 10:13:22 +0200 Subject: [PATCH 17/64] verification status by user, sign manually verified keys --- lib/src/utils/device_keys_list.dart | 41 ++++++++++++++++++++--------- lib/src/utils/key_verification.dart | 2 +- lib/src/utils/room_key_request.dart | 1 - 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 75ec0c2..ce62d4b 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -8,6 +8,8 @@ import '../database/database.dart' import '../event.dart'; import 'key_verification.dart'; +enum UserVerifiedStatus { verified, unknown, unknownDevice } + class DeviceKeysList { Client client; String userId; @@ -25,18 +27,27 @@ class DeviceKeysList { return null; } - CrossSigningKey getCrossSigningKey(String type) { - final keys = crossSigningKeys.values.where((k) => k.usage.contains(type)); - if (keys.isEmpty) { - return null; - } - return keys.first; - } + CrossSigningKey getCrossSigningKey(String type) => crossSigningKeys.values.firstWhere((k) => k.usage.contains(type)); CrossSigningKey get masterKey => getCrossSigningKey('master'); CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing'); CrossSigningKey get userSigningKey => getCrossSigningKey('user_signing'); + UserVerifiedStatus get verified { + if (masterKey == null) { + return UserVerifiedStatus.unknown; + } + if (masterKey.verified) { + for (final key in deviceKeys.values) { + if (!key.verified) { + return UserVerifiedStatus.unknownDevice; + } + } + return UserVerifiedStatus.verified; + } + return UserVerifiedStatus.unknown; + } + DeviceKeysList.fromDb( DbUserDeviceKey dbEntry, List childEntries, @@ -226,7 +237,13 @@ abstract class SignedKey { return false; } - Future setVerified(bool newVerified); + Future setVerified(bool newVerified, [bool sign = true]) { + _verified = newVerified; + if (sign && client.crossSigning.signable([this])) { + // sign the key! + client.crossSigning.sign([this]); + } + } Future setBlocked(bool newBlocked); @@ -250,8 +267,8 @@ class CrossSigningKey extends SignedKey { userId != null && publicKey != null && keys != null && ed25519Key != null; @override - Future setVerified(bool newVerified) { - _verified = newVerified; + Future setVerified(bool newVerified, [bool sign = true]) { + super.setVerified(newVerified, sign); return client.database?.setVerifiedUserCrossSigningKey( newVerified, client.id, userId, publicKey); } @@ -310,8 +327,8 @@ class DeviceKeys extends SignedKey { ed25519Key != null; @override - Future setVerified(bool newVerified) { - _verified = newVerified; + Future setVerified(bool newVerified, [bool sign = true]) { + super.setVerified(newVerified, sign); return client.database ?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId); } diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 7508c30..fed1a63 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -358,7 +358,7 @@ class KeyVerification { var verifiedMasterKey = false; final wasUnknownSession = client.isUnknownSession; for (final key in _verifiedDevices) { - await key.setVerified(true); + await key.setVerified(true, false); // we don't want to sign the keys juuuust yet if (key is CrossSigningKey && key.usage.contains('master')) { verifiedMasterKey = true; } diff --git a/lib/src/utils/room_key_request.dart b/lib/src/utils/room_key_request.dart index ae97601..838c43f 100644 --- a/lib/src/utils/room_key_request.dart +++ b/lib/src/utils/room_key_request.dart @@ -22,7 +22,6 @@ class RoomKeyRequest extends ToDeviceEvent { for (final key in session.forwardingCurve25519KeyChain) { forwardedKeys.add(key); } - await requestingDevice.setVerified(true); var message = session.content; message['forwarding_curve25519_key_chain'] = forwardedKeys; From f78657957a757c07b5a255ea0af4f2e733690fda Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 27 May 2020 10:33:22 +0200 Subject: [PATCH 18/64] fix emotes if canonical alias is null --- lib/src/room.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/room.dart b/lib/src/room.dart index 4a4612e..044208e 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -592,7 +592,7 @@ class Room { final event = room.getState('im.ponies.room_emotes', stateKey); if (event != null && stateKeyEntry.value is Map) { addEmotePack( - room.canonicalAlias.isEmpty ? room.id : canonicalAlias, + (room.canonicalAlias?.isEmpty ?? true) ? room.id : canonicalAlias, event.content, stateKeyEntry.value['name']); } From 8d75c2a0afed1758015bf39c67614c7729b81b35 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 27 May 2020 10:33:42 +0200 Subject: [PATCH 19/64] format --- lib/src/room.dart | 4 +++- lib/src/utils/device_keys_list.dart | 3 ++- lib/src/utils/key_verification.dart | 6 ++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/src/room.dart b/lib/src/room.dart index 044208e..6975fff 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -592,7 +592,9 @@ class Room { final event = room.getState('im.ponies.room_emotes', stateKey); if (event != null && stateKeyEntry.value is Map) { addEmotePack( - (room.canonicalAlias?.isEmpty ?? true) ? room.id : canonicalAlias, + (room.canonicalAlias?.isEmpty ?? true) + ? room.id + : canonicalAlias, event.content, stateKeyEntry.value['name']); } diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index ce62d4b..f140298 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -27,7 +27,8 @@ class DeviceKeysList { return null; } - CrossSigningKey getCrossSigningKey(String type) => crossSigningKeys.values.firstWhere((k) => k.usage.contains(type)); + CrossSigningKey getCrossSigningKey(String type) => + crossSigningKeys.values.firstWhere((k) => k.usage.contains(type)); CrossSigningKey get masterKey => getCrossSigningKey('master'); CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing'); diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index fed1a63..96b56cc 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -151,7 +151,8 @@ class KeyVerification { randomString(512) + DateTime.now().millisecondsSinceEpoch.toString(); } if (client.crossSigning.enabled && - !(await client.crossSigning.isCached()) && !client.isUnknownSession) { + !(await client.crossSigning.isCached()) && + !client.isUnknownSession) { setState(KeyVerificationState.askSSSS); _nextAction = 'request'; } else { @@ -358,7 +359,8 @@ class KeyVerification { var verifiedMasterKey = false; final wasUnknownSession = client.isUnknownSession; for (final key in _verifiedDevices) { - await key.setVerified(true, false); // we don't want to sign the keys juuuust yet + await key.setVerified( + true, false); // we don't want to sign the keys juuuust yet if (key is CrossSigningKey && key.usage.contains('master')) { verifiedMasterKey = true; } From e4e43861783679e32d7b9e0724a3b7898ac45b95 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 27 May 2020 17:37:14 +0200 Subject: [PATCH 20/64] signed vs verified logic --- lib/src/client.dart | 23 ++++------------------- lib/src/utils/device_keys_list.dart | 13 ++++++++++--- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 48952d6..bbd7775 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -149,25 +149,10 @@ class Client { bool get fileEncryptionEnabled => true; /// Wheather this session is unknown to others - bool get isUnknownSession { - if (!userDeviceKeys.containsKey(userID)) { - return true; - } - final masterKey = userDeviceKeys[userID].masterKey; - if (masterKey == null) { - return true; - } - if (!masterKey.directVerified) { - return true; - } - if (!userDeviceKeys[userID].deviceKeys.containsKey(deviceID)) { - return true; - } - if (!userDeviceKeys[userID].deviceKeys[deviceID].crossVerified) { - return true; - } - return false; - } + bool get isUnknownSession => + !userDeviceKeys.containsKey(userID) || + !userDeviceKeys[userID].deviceKeys.containsKey(deviceID) || + !userDeviceKeys[userID].deviceKeys[deviceID].signed; /// Warning! This endpoint is for testing only! set rooms(List newList) { diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index f140298..d96c2db 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -140,6 +140,8 @@ abstract class SignedKey { } } + bool get signed => hasValidSignatureChain(verifiedOnly: false); + String get signingContent { final data = Map.from(content); // some old data might have the custom verified and blocked keys @@ -166,7 +168,7 @@ abstract class SignedKey { return valid; } - bool hasValidSignatureChain({Set visited}) { + bool hasValidSignatureChain({bool verfiedOnly = true, Set visited}) { visited ??= {}; final setKey = '${userId};${identifier}'; if (visited.contains(setKey)) { @@ -225,11 +227,16 @@ abstract class SignedKey { continue; } - if (key.directVerified) { + if ((verifiedOnly && key.directVerified) || + (key is SignedKey && + key.usage.includes('master') && + key.directVerified && + key.userId == client.userID)) { return true; // we verified this key and it is valid...all checks out! } // or else we just recurse into that key and chack if it works out - final haveChain = key.hasValidSignatureChain(visited: visited); + final haveChain = key.hasValidSignatureChain( + verfiedOnly: verfiedOnly, visited: visited); if (haveChain) { return true; } From aefe029c0a216c34bd2d12bc33822c090f420048 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 27 May 2020 18:50:09 +0200 Subject: [PATCH 21/64] add ability to sign yourself based on ssss --- lib/src/cross_signing.dart | 29 +++++++++++++++++++++++++++-- lib/src/ssss.dart | 6 +++++- lib/src/utils/device_keys_list.dart | 8 ++++---- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/src/cross_signing.dart b/lib/src/cross_signing.dart index ab9f018..67803c3 100644 --- a/lib/src/cross_signing.dart +++ b/lib/src/cross_signing.dart @@ -27,6 +27,31 @@ class CrossSigning { (await client.ssss.getCached(USER_SIGNING_KEY)) != null; } + Future selfSign({String password, String recoveryKey}) async { + final handle = client.ssss.open(MASTER_KEY); + await handle.unlock(password: password, recoveryKey: recoveryKey); + await handle.maybeCacheAll(); + final masterPrivateKey = base64.decode(await handle.getStored(MASTER_KEY)); + final keyObj = olm.PkSigning(); + String masterPubkey; + try { + masterPubkey = keyObj.init_with_seed(masterPrivateKey); + } finally { + keyObj.free(); + } + if (masterPubkey == null || !client.userDeviceKeys.containsKey(client.userID) || !client.userDeviceKeys[client.userID].deviceKeys.containsKey(client.deviceID)) { + throw 'Master or user keys not found'; + } + final masterKey = client.userDeviceKeys[client.userID].masterKey; + if (masterKey == null || masterKey.ed25519Key != masterPubkey) { + throw 'Master pubkey key doesn\'t match'; + } + // master key is valid, set it to verified + masterKey.setVerified(true, false); + // and now sign bout our own key and our master key + await sign([masterKey, client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]]); + } + bool signable(List keys) { for (final key in keys) { if (key is CrossSigningKey && key.usage.contains('master')) { @@ -86,7 +111,7 @@ class CrossSigning { signature); } // we don't care about signing other cross-signing keys - } else if (key.identifier != client.deviceID) { + } else { // okay, we'll sign a device key with our self signing key selfSigningKey ??= base64 .decode(await client.ssss.getCached(SELF_SIGNING_KEY) ?? ''); @@ -119,8 +144,8 @@ class CrossSigning { String _sign(String canonicalJson, Uint8List key) { final keyObj = olm.PkSigning(); - keyObj.init_with_seed(key); try { + keyObj.init_with_seed(key); return keyObj.sign(canonicalJson); } finally { keyObj.free(); diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart index b350da4..70cf540 100644 --- a/lib/src/ssss.dart +++ b/lib/src/ssss.dart @@ -213,7 +213,11 @@ class SSSS { for (final type in CACHE_TYPES) { final secret = await getCached(type); if (secret == null) { - await getStored(type, keyId, key); + try { + await getStored(type, keyId, key); + } catch (_) { + // the entry wasn't stored, just ignore it + } } } } diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index d96c2db..9559218 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -168,7 +168,7 @@ abstract class SignedKey { return valid; } - bool hasValidSignatureChain({bool verfiedOnly = true, Set visited}) { + bool hasValidSignatureChain({bool verifiedOnly = true, Set visited}) { visited ??= {}; final setKey = '${userId};${identifier}'; if (visited.contains(setKey)) { @@ -228,15 +228,15 @@ abstract class SignedKey { } if ((verifiedOnly && key.directVerified) || - (key is SignedKey && - key.usage.includes('master') && + (key is CrossSigningKey && + key.usage.contains('master') && key.directVerified && key.userId == client.userID)) { return true; // we verified this key and it is valid...all checks out! } // or else we just recurse into that key and chack if it works out final haveChain = key.hasValidSignatureChain( - verfiedOnly: verfiedOnly, visited: visited); + verifiedOnly: verifiedOnly, visited: visited); if (haveChain) { return true; } From dda0b177241b1c7c448bdef881d6a1f8b9cc1e61 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 27 May 2020 21:35:00 +0200 Subject: [PATCH 22/64] in-room verification to verify users instead of devices --- lib/src/client.dart | 53 +++++++++++++++++++++++++++++ lib/src/cross_signing.dart | 11 ++++-- lib/src/utils/device_keys_list.dart | 16 +++++++++ lib/src/utils/key_verification.dart | 47 ++++++++++++++++++++++--- pubspec.yaml | 2 +- 5 files changed, 122 insertions(+), 7 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index bbd7775..b6facac 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1216,6 +1216,54 @@ class Client { } } + void _handleRoomKeyVerificationRequest(EventUpdate update) { + final event = update.content; + final type = event['type'].startsWith('m.key.verification.') + ? event['type'] + : event['content']['msgtype']; + if (!type.startsWith('m.key.verification.')) { + return; + } + if (type == 'm.key.verification.request') { + event['content']['timestamp'] = event['origin_server_ts']; + } + final transactionId = + KeyVerification.getTransactionId(event['content']) ?? event['event_id']; + if (_keyVerificationRequests.containsKey(transactionId)) { + final req = _keyVerificationRequests[transactionId]; + if (event['sender'] != userID) { + req.handlePayload(type, event['content'], event['event_id']); + } else if (req.userId == userID && req.deviceId == null) { + req + .handlePayload(type, event['content'], event['event_id']) + .then((ret) { + if (req.deviceId != deviceID) { + req.otherDeviceAccepted(); + req.dispose(); + _keyVerificationRequests.remove(transactionId); + } + }); + } + } else if (event['sender'] != userID) { + final room = + getRoomById(update.roomID) ?? Room(id: update.roomID, client: this); + final newKeyRequest = + KeyVerification(client: this, userId: event['sender'], room: room); + newKeyRequest + .handlePayload(type, event['content'], event['event_id']) + .then((res) { + if (newKeyRequest.state != KeyVerificationState.askAccept) { + // something went wrong, let's just dispose the request + newKeyRequest.dispose(); + } else { + // new request! Let's notify it and stuff + _keyVerificationRequests[transactionId] = newKeyRequest; + onKeyVerificationRequest.add(newKeyRequest); + } + }); + } + } + Future _handleRooms( Map rooms, Membership membership) async { for (final entry in rooms.entries) { @@ -1416,6 +1464,11 @@ class Client { await database.storeEventUpdate(id, update); } _updateRoomsByEventUpdate(update); + if (event['type'].startsWith('m.key.verification.') || + (event['type'] == 'm.room.message' && + event['content']['msgtype'].startsWith('m.key.verification.'))) { + _handleRoomKeyVerificationRequest(update); + } onEvent.add(update); if (event['type'] == 'm.call.invite') { diff --git a/lib/src/cross_signing.dart b/lib/src/cross_signing.dart index 67803c3..efd0f76 100644 --- a/lib/src/cross_signing.dart +++ b/lib/src/cross_signing.dart @@ -39,7 +39,10 @@ class CrossSigning { } finally { keyObj.free(); } - if (masterPubkey == null || !client.userDeviceKeys.containsKey(client.userID) || !client.userDeviceKeys[client.userID].deviceKeys.containsKey(client.deviceID)) { + if (masterPubkey == null || + !client.userDeviceKeys.containsKey(client.userID) || + !client.userDeviceKeys[client.userID].deviceKeys + .containsKey(client.deviceID)) { throw 'Master or user keys not found'; } final masterKey = client.userDeviceKeys[client.userID].masterKey; @@ -49,7 +52,10 @@ class CrossSigning { // master key is valid, set it to verified masterKey.setVerified(true, false); // and now sign bout our own key and our master key - await sign([masterKey, client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]]); + await sign([ + masterKey, + client.userDeviceKeys[client.userID].deviceKeys[client.deviceID] + ]); } bool signable(List keys) { @@ -82,6 +88,7 @@ class CrossSigning { if (!signatures[key.userId].containsKey(key.identifier)) { signatures[key.userId][key.identifier] = Map.from(key.toJson()); + // we don't need to send all old signatures, so let's just remove them signatures[key.userId][key.identifier].remove('signatures'); } if (!signatures[key.userId][key.identifier].containsKey('signatures')) { diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 9559218..4a89f2b 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -3,6 +3,8 @@ import 'package:canonical_json/canonical_json.dart'; import 'package:olm/olm.dart' as olm; import '../client.dart'; +import '../user.dart'; +import '../room.dart'; import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey, DbUserCrossSigningKey; import '../event.dart'; @@ -49,6 +51,20 @@ class DeviceKeysList { return UserVerifiedStatus.unknown; } + Future startVerification() async { + final roomId = + await User(userId, room: Room(client: client)).startDirectChat(); + if (roomId == null) { + throw 'Unable to start new room'; + } + final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client); + final request = KeyVerification(client: client, room: room, userId: userId); + await request.start(); + // no need to add to the request client object. As we are doing a room + // verification request that'll happen automatically once we know the transaction id + return request; + } + DeviceKeysList.fromDb( DbUserDeviceKey dbEntry, List childEntries, diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 96b56cc..1858a8d 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -83,11 +83,11 @@ List _bytesToInt(Uint8List bytes, int totalBits) { return ret; } -final VERIFICATION_METHODS = [_KeyVerificationMethodSas.type]; +final VERIFICATION_METHODS = ['m.sas.v1']; _KeyVerificationMethod _makeVerificationMethod( String type, KeyVerification request) { - if (type == _KeyVerificationMethodSas.type) { + if (type == 'm.sas.v1') { return _KeyVerificationMethodSas(request: request); } throw 'Unkown method type'; @@ -138,7 +138,7 @@ class KeyVerification { Future sendStart() async { await send('m.key.verification.request', { 'methods': VERIFICATION_METHODS, - 'timestamp': DateTime.now().millisecondsSinceEpoch, + if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch, }); startedVerification = true; setState(KeyVerificationState.waitingAccept); @@ -189,6 +189,7 @@ class KeyVerification { setState(KeyVerificationState.askAccept); break; case 'm.key.verification.ready': + _deviceId ??= payload['from_device']; possibleMethods = _intersect(VERIFICATION_METHODS, payload['methods']); if (possibleMethods.isEmpty) { @@ -205,6 +206,32 @@ class KeyVerification { _deviceId ??= payload['from_device']; print('Setting device id start: ' + _deviceId.toString()); transactionId ??= eventId ?? payload['transaction_id']; + if (method != null) { + print('DUPLICATE START'); + // the other side sent us a start, even though we already sent one + if (payload['method'] == method.type) { + // same method. Determine priority + final ourEntry = '${client.userID}|${client.deviceID}'; + final entries = [ourEntry, '${userId}|${deviceId}']; + entries.sort(); + if (entries.first == ourEntry) { + // our start won, nothing to do + print('we won, nothing to do'); + return; + } else { + print('They won, handing off'); + // the other start won, let's hand off + startedVerification = false; // it is now as if they started + lastStep = + 'm.key.verification.request'; // we fake the last step + method.dispose(); // in case anything got created already + } + } else { + // methods don't match up, let's cancel this + await cancel('m.unexpected_message'); + return; + } + } if (!(await verifyLastStep(['m.key.verification.request', null]))) { return; // abort } @@ -221,6 +248,7 @@ class KeyVerification { startPaylaod = payload; setState(KeyVerificationState.askAccept); } else { + print('handling start in method.....'); await method.handlePayload(type, payload); } break; @@ -247,6 +275,13 @@ class KeyVerification { } } + void otherDeviceAccepted() { + canceled = true; + canceledCode = 'm.accepted'; + canceledReason = 'm.accepted'; + setState(KeyVerificationState.error); + } + Future openSSSS( {String password, String recoveryKey, bool skip = false}) async { final next = () { @@ -482,6 +517,9 @@ abstract class _KeyVerificationMethod { return false; } + String _type; + String get type => _type; + Future sendStart(); void dispose() {} } @@ -495,7 +533,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { _KeyVerificationMethodSas({KeyVerification request}) : super(request: request); - static String type = 'm.sas.v1'; + @override + String _type = 'm.sas.v1'; String keyAgreementProtocol; String hash; diff --git a/pubspec.yaml b/pubspec.yaml index 51d3e62..8fd70fd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: olm: git: url: https://gitlab.com/famedly/libraries/dart-olm.git - ref: 8749474d611f02a89893e067b6e479ebfd40c51d + ref: a6cde466e707b54e39df8d035a0a79714621bc0b matrix_file_e2ee: path: /home/sorunome/repos/famedly/matrix_file_e2ee From c779d398180e2322e134f8772c90be0148cee921 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 27 May 2020 21:40:58 +0200 Subject: [PATCH 23/64] fix small stuffs --- lib/src/cross_signing.dart | 2 +- lib/src/ssss.dart | 2 +- lib/src/utils/device_keys_list.dart | 20 ++++++++++---------- lib/src/utils/key_verification.dart | 2 +- pubspec.lock | 12 +++++------- pubspec.yaml | 2 +- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/lib/src/cross_signing.dart b/lib/src/cross_signing.dart index efd0f76..8307a96 100644 --- a/lib/src/cross_signing.dart +++ b/lib/src/cross_signing.dart @@ -50,7 +50,7 @@ class CrossSigning { throw 'Master pubkey key doesn\'t match'; } // master key is valid, set it to verified - masterKey.setVerified(true, false); + await masterKey.setVerified(true, false); // and now sign bout our own key and our master key await sign([ masterKey, diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart index 70cf540..219e525 100644 --- a/lib/src/ssss.dart +++ b/lib/src/ssss.dart @@ -435,7 +435,7 @@ class OpenSSSS { return await ssss.getStored(type, keyId, privateKey); } - Future store(String type, String secret) async { + Future store(String type, String secret) async { await ssss.store(type, secret, keyId, privateKey); } diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 4a89f2b..c20cac6 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -261,7 +261,7 @@ abstract class SignedKey { return false; } - Future setVerified(bool newVerified, [bool sign = true]) { + void setVerified(bool newVerified, [bool sign = true]) { _verified = newVerified; if (sign && client.crossSigning.signable([this])) { // sign the key! @@ -269,7 +269,7 @@ abstract class SignedKey { } } - Future setBlocked(bool newBlocked); + void setBlocked(bool newBlocked); Map toJson() { final data = Map.from(content); @@ -291,16 +291,16 @@ class CrossSigningKey extends SignedKey { userId != null && publicKey != null && keys != null && ed25519Key != null; @override - Future setVerified(bool newVerified, [bool sign = true]) { + void setVerified(bool newVerified, [bool sign = true]) { super.setVerified(newVerified, sign); - return client.database?.setVerifiedUserCrossSigningKey( + client.database?.setVerifiedUserCrossSigningKey( newVerified, client.id, userId, publicKey); } @override - Future setBlocked(bool newBlocked) { + void setBlocked(bool newBlocked) { blocked = newBlocked; - return client.database?.setBlockedUserCrossSigningKey( + client.database?.setBlockedUserCrossSigningKey( newBlocked, client.id, userId, publicKey); } @@ -351,14 +351,14 @@ class DeviceKeys extends SignedKey { ed25519Key != null; @override - Future setVerified(bool newVerified, [bool sign = true]) { + void setVerified(bool newVerified, [bool sign = true]) { super.setVerified(newVerified, sign); - return client.database + client.database ?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId); } @override - Future setBlocked(bool newBlocked) { + void setBlocked(bool newBlocked) { blocked = newBlocked; for (var room in client.rooms) { if (!room.encrypted) continue; @@ -366,7 +366,7 @@ class DeviceKeys extends SignedKey { room.clearOutboundGroupSession(); } } - return client.database + client.database ?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId); } diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 1858a8d..4f171f8 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -534,7 +534,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { : super(request: request); @override - String _type = 'm.sas.v1'; + final _type = 'm.sas.v1'; String keyAgreementProtocol; String hash; diff --git a/pubspec.lock b/pubspec.lock index 9c51531..1d8863b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -333,11 +333,9 @@ packages: matrix_file_e2ee: dependency: "direct main" description: - path: "." - ref: "1.x.y" - resolved-ref: "32edeff765369a7a77a0822f4b19302ca24a017b" - url: "https://gitlab.com/famedly/libraries/matrix_file_e2ee.git" - source: git + path: "/home/sorunome/repos/famedly/matrix_file_e2ee" + relative: false + source: path version: "1.0.3" meta: dependency: transitive @@ -413,8 +411,8 @@ packages: dependency: "direct main" description: path: "." - ref: "1.x.y" - resolved-ref: f66975bd1b5cb1865eba5efe6e3a392aa5e396a5 + ref: "0c612a525511652a7760126b058de8c924fe8900" + resolved-ref: "0c612a525511652a7760126b058de8c924fe8900" url: "https://gitlab.com/famedly/libraries/dart-olm.git" source: git version: "1.1.1" diff --git a/pubspec.yaml b/pubspec.yaml index 8fd70fd..c1a1765 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: olm: git: url: https://gitlab.com/famedly/libraries/dart-olm.git - ref: a6cde466e707b54e39df8d035a0a79714621bc0b + ref: 0c612a525511652a7760126b058de8c924fe8900 matrix_file_e2ee: path: /home/sorunome/repos/famedly/matrix_file_e2ee From 6e21cff0a7db61d7672a6a51e95aad6d2745102c Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 27 May 2020 21:49:49 +0200 Subject: [PATCH 24/64] fix error thingy --- lib/src/client.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/client.dart b/lib/src/client.dart index b6facac..e0d40fc 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1466,6 +1466,7 @@ class Client { _updateRoomsByEventUpdate(update); if (event['type'].startsWith('m.key.verification.') || (event['type'] == 'm.room.message' && + (event['content']['msgtype'] is String) && event['content']['msgtype'].startsWith('m.key.verification.'))) { _handleRoomKeyVerificationRequest(update); } From 72fdb68e82fb93e1a3bf0d5e3b376c77013393a6 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 29 May 2020 08:49:52 +0200 Subject: [PATCH 25/64] some fixes --- lib/src/client.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index e0d40fc..7035a4b 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -66,6 +66,11 @@ enum HTTPType { GET, POST, PUT, DELETE } enum LoginState { logged, loggedOut } +class GenericException implements Exception { + final dynamic content; + GenericException(this.content); +} + /// Represents a Matrix client to communicate with a /// [Matrix](https://matrix.org) homeserver and is the entry point for this /// SDK. @@ -1167,7 +1172,7 @@ class Client { print(s); onOlmError.add( ToDeviceEventDecryptionError( - exception: e, + exception: (e is Exception) ? e : GenericException(e), stackTrace: s, toDeviceEvent: toDeviceEvent, ), From 15be6c52446e5acb371eb941182e2b67aa6f64ec Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 29 May 2020 09:06:36 +0200 Subject: [PATCH 26/64] stuff and things --- lib/src/ssss.dart | 18 +++++++++++++----- lib/src/utils/device_keys_list.dart | 21 +++++++++++---------- lib/src/utils/key_verification.dart | 4 +--- test/room_key_request_test.dart | 2 +- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart index 219e525..cb2b3ff 100644 --- a/lib/src/ssss.dart +++ b/lib/src/ssss.dart @@ -5,7 +5,6 @@ import 'package:encrypt/encrypt.dart'; import 'package:crypto/crypto.dart'; import 'package:base58check/base58.dart'; import 'package:password_hash/password_hash.dart'; -import 'package:random_string/random_string.dart'; import 'client.dart'; import 'account_data.dart'; @@ -243,8 +242,7 @@ class SSSS { print('[SSSS] Warn: No devices'); return; } - final requestId = - randomString(512) + DateTime.now().millisecondsSinceEpoch.toString(); + final requestId = client.generateUniqueTransactionId(); final request = _ShareRequest( requestId: requestId, type: type, @@ -298,12 +296,22 @@ class SSSS { // receiving a secret we asked for print('[SSSS] Received shared secret...'); if (event.sender != client.userID || - !pendingShareRequests.containsKey(event.content['request_id'])) { + !pendingShareRequests.containsKey(event.content['request_id']) || + event.encryptedContent == null) { print('[SSSS] Not by us or unknown request'); return; // we have no idea what we just received } final request = pendingShareRequests[event.content['request_id']]; - // alright, as we received a known request id we know that it must have originated from a trusted source + // alright, as we received a known request id, let's check if the sender is valid + final device = request.devices.firstWhere( + (d) => + d.userId == event.sender && + d.curve25519Key == event.encryptedContent['sender_key'], + orElse: () => null); + if (device == null) { + print('[SSSS] Someone else replied?'); + return; // someone replied whom we didn't send the share request to + } pendingShareRequests.remove(request.requestId); if (!(event.content['secret'] is String)) { print('[SSSS] Secret wasn\'t a string'); diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index c20cac6..211e2a3 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -261,15 +261,16 @@ abstract class SignedKey { return false; } - void setVerified(bool newVerified, [bool sign = true]) { + Future setVerified(bool newVerified, [bool sign = true]) { _verified = newVerified; if (sign && client.crossSigning.signable([this])) { // sign the key! client.crossSigning.sign([this]); } + return Future.value(); } - void setBlocked(bool newBlocked); + Future setBlocked(bool newBlocked); Map toJson() { final data = Map.from(content); @@ -291,16 +292,16 @@ class CrossSigningKey extends SignedKey { userId != null && publicKey != null && keys != null && ed25519Key != null; @override - void setVerified(bool newVerified, [bool sign = true]) { + Future setVerified(bool newVerified, [bool sign = true]) { super.setVerified(newVerified, sign); - client.database?.setVerifiedUserCrossSigningKey( + return client.database?.setVerifiedUserCrossSigningKey( newVerified, client.id, userId, publicKey); } @override - void setBlocked(bool newBlocked) { + Future setBlocked(bool newBlocked) { blocked = newBlocked; - client.database?.setBlockedUserCrossSigningKey( + return client.database?.setBlockedUserCrossSigningKey( newBlocked, client.id, userId, publicKey); } @@ -351,14 +352,14 @@ class DeviceKeys extends SignedKey { ed25519Key != null; @override - void setVerified(bool newVerified, [bool sign = true]) { + Future setVerified(bool newVerified, [bool sign = true]) { super.setVerified(newVerified, sign); - client.database + return client.database ?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId); } @override - void setBlocked(bool newBlocked) { + Future setBlocked(bool newBlocked) { blocked = newBlocked; for (var room in client.rooms) { if (!room.encrypted) continue; @@ -366,7 +367,7 @@ class DeviceKeys extends SignedKey { room.clearOutboundGroupSession(); } } - client.database + return client.database ?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId); } diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 4f171f8..1088ff4 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -1,5 +1,4 @@ import 'dart:typed_data'; -import 'package:random_string/random_string.dart'; import 'package:canonical_json/canonical_json.dart'; import 'package:pedantic/pedantic.dart'; import 'package:olm/olm.dart' as olm; @@ -147,8 +146,7 @@ class KeyVerification { Future start() async { if (room == null) { - transactionId = - randomString(512) + DateTime.now().millisecondsSinceEpoch.toString(); + transactionId = client.generateUniqueTransactionId(); } if (client.crossSigning.enabled && !(await client.crossSigning.isCached()) && diff --git a/test/room_key_request_test.dart b/test/room_key_request_test.dart index 5ff83c3..fa6229f 100644 --- a/test/room_key_request_test.dart +++ b/test/room_key_request_test.dart @@ -136,7 +136,7 @@ void main() { matrix.setUserId('@alice:example.com'); // we need to pretend to be alice FakeMatrixApi.calledEndpoints.clear(); await matrix.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE'] - .setVerified(true, matrix); + .setVerified(true); // test a successful share var event = ToDeviceEvent( sender: '@alice:example.com', From a7bb8375dc6c2880f2b7ce5c6c085cf6cb0abd18 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 29 May 2020 10:21:36 +0200 Subject: [PATCH 27/64] fetch from online key backup --- lib/src/key_manager.dart | 108 +++++++++++++++++++++++++++++++++++++++ lib/src/room.dart | 1 + pubspec.yaml | 7 +-- 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/lib/src/key_manager.dart b/lib/src/key_manager.dart index 32c6554..abe5b37 100644 --- a/lib/src/key_manager.dart +++ b/lib/src/key_manager.dart @@ -1,8 +1,15 @@ +import 'dart:core'; +import 'dart:convert'; + +import 'package:olm/olm.dart' as olm; + import 'client.dart'; import 'room.dart'; import 'utils/to_device_event.dart'; import 'utils/device_keys_list.dart'; +const MEGOLM_KEY = 'm.megolm_backup.v1'; + class KeyManager { final Client client; final outgoingShareRequests = {}; @@ -10,8 +17,109 @@ class KeyManager { KeyManager(this.client); + bool get enabled => client.accountData[MEGOLM_KEY] != null; + + Future> getRoomKeysInfo() async { + return await client.jsonRequest( + type: HTTPType.GET, + action: '/client/r0/room_keys/version', + ); + } + + Future isCached() async { + if (!enabled) { + return false; + } + return (await client.ssss.getCached(MEGOLM_KEY)) != null; + } + + Future loadFromResponse(Map payload) async { + if (!(await isCached())) { + return; + } + if (!(payload['rooms'] is Map)) { + return; + } + final privateKey = base64.decode(await client.ssss.getCached(MEGOLM_KEY)); + final decryption = olm.PkDecryption(); + String backupPubKey; + try { + backupPubKey = decryption.init_with_private_key(privateKey); + } catch (_) { + decryption.free(); + rethrow; + } + if (backupPubKey == null) { + decryption.free(); + return; + } + // TODO: check if pubkey is valid + for (final roomEntries in payload['rooms'].entries) { + final roomId = roomEntries.key; + if (!(roomEntries.value is Map) || !(roomEntries.value['sessions'] is Map)) { + continue; + } + for (final sessionEntries in roomEntries.value['sessions'].entries) { + final sessionId = sessionEntries.key; + final rawEncryptedSession = sessionEntries.value; + if (!(rawEncryptedSession is Map)) { + continue; + } + final firstMessageIndex = rawEncryptedSession['first_message_index'] is int ? rawEncryptedSession['first_message_index'] : null; + final forwardedCount = rawEncryptedSession['forwarded_count'] is int ? rawEncryptedSession['forwarded_count'] : null; + final isVerified = rawEncryptedSession['is_verified'] is bool ? rawEncryptedSession['is_verified'] : null; + final sessionData = rawEncryptedSession['session_data']; + if (firstMessageIndex == null || forwardedCount == null || isVerified == null || !(sessionData is Map)) { + continue; + } + final senderKey = sessionData['sender_key']; + Map decrypted; + try { + decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], sessionData['mac'], sessionData['ciphertext'])); + } catch (err) { + print('[LibOlm] Error decrypting room key: ' + err.toString()); + } + if (decrypted != null) { + decrypted['session_id'] = sessionId; + decrypted['room_id'] = roomId; + final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client); + room.setInboundGroupSession(sessionId, decrypted, forwarded: true); + } + } + } + } + + Future loadSingleKey(String roomId, String sessionId) async { + final info = await getRoomKeysInfo(); + final ret = await client.jsonRequest( + type: HTTPType.GET, + action: '/client/r0/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${info['version']}', + ); + await loadFromResponse({ + 'rooms': { + roomId: { + 'sessions': { + sessionId: ret, + }, + }, + }, + }); + } + /// Request a certain key from another device Future request(Room room, String sessionId, String senderKey) async { + // let's first check our online key backup store thingy... + var hadPreviously = room.inboundGroupSessions.containsKey(sessionId); + try { + await loadSingleKey(room.id, sessionId); + } catch (err, stacktrace) { + print('++++++++++++++++++'); + print(err.toString()); + print(stacktrace); + } + if (!hadPreviously && room.inboundGroupSessions.containsKey(sessionId)) { + return; // we managed to load the session from online backup, no need to care about it now + } // while we just send the to-device event to '*', we still need to save the // devices themself to know where to send the cancel to after receiving a reply final devices = await room.getUserDeviceKeys(); diff --git a/lib/src/room.dart b/lib/src/room.dart index a17b5b3..4c7048a 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -251,6 +251,7 @@ class Room { inboundGroupSession = null; print('[LibOlm] Could not create new InboundGroupSession: ' + e.toString()); + return; } } _inboundGroupSessions[sessionId] = SessionKey( diff --git a/pubspec.yaml b/pubspec.yaml index c1a1765..c37e405 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,9 +22,10 @@ dependencies: password_hash: ^2.0.0 olm: - git: - url: https://gitlab.com/famedly/libraries/dart-olm.git - ref: 0c612a525511652a7760126b058de8c924fe8900 + path: /home/sorunome/repos/famedly/dart-olm +# git: +# url: https://gitlab.com/famedly/libraries/dart-olm.git +# ref: 0c612a525511652a7760126b058de8c924fe8900 matrix_file_e2ee: path: /home/sorunome/repos/famedly/matrix_file_e2ee From 41a08d4c287751da78626a2b22f0ccb1aad4be53 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 30 May 2020 13:13:42 +0200 Subject: [PATCH 28/64] additional validation of received secrets --- lib/src/client.dart | 2 +- lib/src/cross_signing.dart | 23 +++++++++- lib/src/key_manager.dart | 87 ++++++++++++++++++++++---------------- lib/src/ssss.dart | 15 ++++++- 4 files changed, 86 insertions(+), 41 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 4aa54fa..d464121 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -97,8 +97,8 @@ class Client { /// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions Client(this.clientName, {this.debug = false, this.database, this.enableE2eeRecovery = false}) { - keyManager = KeyManager(this); ssss = SSSS(this); + keyManager = KeyManager(this); crossSigning = CrossSigning(this); onLoginStateChanged.stream.listen((loginState) { print('LoginState: ${loginState.toString()}'); diff --git a/lib/src/cross_signing.dart b/lib/src/cross_signing.dart index 8307a96..a884a37 100644 --- a/lib/src/cross_signing.dart +++ b/lib/src/cross_signing.dart @@ -12,7 +12,28 @@ const MASTER_KEY = 'm.cross_signing.master'; class CrossSigning { final Client client; - CrossSigning(this.client); + CrossSigning(this.client) { + client.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async { + final keyObj = olm.PkSigning(); + try { + return keyObj.init_with_seed(base64.decode(secret)) == client.userDeviceKeys[client.userID].selfSigningKey.ed25519Key; + } catch (_) { + return false; + } finally { + keyObj.free(); + } + }); + client.ssss.setValidator(USER_SIGNING_KEY, (String secret) async { + final keyObj = olm.PkSigning(); + try { + return keyObj.init_with_seed(base64.decode(secret)) == client.userDeviceKeys[client.userID].userSigningKey.ed25519Key; + } catch (_) { + return false; + } finally { + keyObj.free(); + } + }); + } bool get enabled => client.accountData[SELF_SIGNING_KEY] != null && diff --git a/lib/src/key_manager.dart b/lib/src/key_manager.dart index abe5b37..6e9d710 100644 --- a/lib/src/key_manager.dart +++ b/lib/src/key_manager.dart @@ -15,7 +15,19 @@ class KeyManager { final outgoingShareRequests = {}; final incomingShareRequests = {}; - KeyManager(this.client); + KeyManager(this.client) { + client.ssss.setValidator(MEGOLM_KEY, (String secret) async { + final keyObj = olm.PkDecryption(); + try { + final info = await getRoomKeysInfo(); + return keyObj.init_with_private_key(base64.decode(secret)) == info['auth_data']['public_key']; + } catch (_) { + return false; + } finally { + keyObj.free(); + } + }); + } bool get enabled => client.accountData[MEGOLM_KEY] != null; @@ -42,50 +54,51 @@ class KeyManager { } final privateKey = base64.decode(await client.ssss.getCached(MEGOLM_KEY)); final decryption = olm.PkDecryption(); + final info = await getRoomKeysInfo(); String backupPubKey; try { backupPubKey = decryption.init_with_private_key(privateKey); - } catch (_) { - decryption.free(); - rethrow; - } - if (backupPubKey == null) { - decryption.free(); - return; - } - // TODO: check if pubkey is valid - for (final roomEntries in payload['rooms'].entries) { - final roomId = roomEntries.key; - if (!(roomEntries.value is Map) || !(roomEntries.value['sessions'] is Map)) { - continue; + + if (backupPubKey == null || !info.containsKey('auth_data') || !(info['auth_data'] is Map) || info['auth_data']['public_key'] != backupPubKey) { + + return; } - for (final sessionEntries in roomEntries.value['sessions'].entries) { - final sessionId = sessionEntries.key; - final rawEncryptedSession = sessionEntries.value; - if (!(rawEncryptedSession is Map)) { + // TODO: check if pubkey is valid + for (final roomEntries in payload['rooms'].entries) { + final roomId = roomEntries.key; + if (!(roomEntries.value is Map) || !(roomEntries.value['sessions'] is Map)) { continue; } - final firstMessageIndex = rawEncryptedSession['first_message_index'] is int ? rawEncryptedSession['first_message_index'] : null; - final forwardedCount = rawEncryptedSession['forwarded_count'] is int ? rawEncryptedSession['forwarded_count'] : null; - final isVerified = rawEncryptedSession['is_verified'] is bool ? rawEncryptedSession['is_verified'] : null; - final sessionData = rawEncryptedSession['session_data']; - if (firstMessageIndex == null || forwardedCount == null || isVerified == null || !(sessionData is Map)) { - continue; - } - final senderKey = sessionData['sender_key']; - Map decrypted; - try { - decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], sessionData['mac'], sessionData['ciphertext'])); - } catch (err) { - print('[LibOlm] Error decrypting room key: ' + err.toString()); - } - if (decrypted != null) { - decrypted['session_id'] = sessionId; - decrypted['room_id'] = roomId; - final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client); - room.setInboundGroupSession(sessionId, decrypted, forwarded: true); + for (final sessionEntries in roomEntries.value['sessions'].entries) { + final sessionId = sessionEntries.key; + final rawEncryptedSession = sessionEntries.value; + if (!(rawEncryptedSession is Map)) { + continue; + } + final firstMessageIndex = rawEncryptedSession['first_message_index'] is int ? rawEncryptedSession['first_message_index'] : null; + final forwardedCount = rawEncryptedSession['forwarded_count'] is int ? rawEncryptedSession['forwarded_count'] : null; + final isVerified = rawEncryptedSession['is_verified'] is bool ? rawEncryptedSession['is_verified'] : null; + final sessionData = rawEncryptedSession['session_data']; + if (firstMessageIndex == null || forwardedCount == null || isVerified == null || !(sessionData is Map)) { + continue; + } + final senderKey = sessionData['sender_key']; + Map decrypted; + try { + decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], sessionData['mac'], sessionData['ciphertext'])); + } catch (err) { + print('[LibOlm] Error decrypting room key: ' + err.toString()); + } + if (decrypted != null) { + decrypted['session_id'] = sessionId; + decrypted['room_id'] = roomId; + final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client); + room.setInboundGroupSession(sessionId, decrypted, forwarded: true); + } } } + } finally { + decryption.free(); } } diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart index cb2b3ff..b639af9 100644 --- a/lib/src/ssss.dart +++ b/lib/src/ssss.dart @@ -27,6 +27,7 @@ const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm class SSSS { final Client client; final pendingShareRequests = {}; + final _validators = Function(String)>{}; SSSS(this.client); static _DerivedKeys deriveKeys(Uint8List key, String name) { @@ -119,6 +120,10 @@ class SSSS { info.iterations, info.bits != null ? info.bits / 8 : 32)); } + void setValidator(String type, Future Function(String) validator) { + _validators[type] = validator; + } + String get defaultKeyId { final keyData = client.accountData['m.secret_storage.default_key']; if (keyData == null || !(keyData.content['key'] is String)) { @@ -312,11 +317,17 @@ class SSSS { print('[SSSS] Someone else replied?'); return; // someone replied whom we didn't send the share request to } - pendingShareRequests.remove(request.requestId); + final secret = event.content['secret']; if (!(event.content['secret'] is String)) { print('[SSSS] Secret wasn\'t a string'); return; // the secret wasn't a string....wut? } + // let's validate if the secret is, well, valid + if (_validators.containsKey(request.type) && !(await _validators[request.type](secret))) { + print('[SSSS] The received secret was invalid'); + return; // didn't pass the validator + } + pendingShareRequests.remove(request.requestId); if (request.start.add(Duration(minutes: 15)).isBefore(DateTime.now())) { print('[SSSS] Request is too far in the past'); return; // our request is more than 15min in the past...better not trust it anymore @@ -326,7 +337,7 @@ class SSSS { final keyId = keyIdFromType(request.type); if (keyId != null) { await client.database.storeSSSSCache( - client.id, request.type, keyId, event.content['secret']); + client.id, request.type, keyId, secret); } } } From 44e4b0799985d3799c207992782bd559c981fbaa Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 30 May 2020 13:19:39 +0200 Subject: [PATCH 29/64] remove obsolete comments --- lib/src/key_manager.dart | 1 - lib/src/ssss.dart | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/src/key_manager.dart b/lib/src/key_manager.dart index 6e9d710..5ac075d 100644 --- a/lib/src/key_manager.dart +++ b/lib/src/key_manager.dart @@ -63,7 +63,6 @@ class KeyManager { return; } - // TODO: check if pubkey is valid for (final roomEntries in payload['rooms'].entries) { final roomId = roomEntries.key; if (!(roomEntries.value is Map) || !(roomEntries.value['sessions'] is Map)) { diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart index b639af9..5eca4a4 100644 --- a/lib/src/ssss.dart +++ b/lib/src/ssss.dart @@ -70,16 +70,15 @@ class SSSS { static String decryptAes(_Encrypted data, Uint8List key, String name) { final keys = deriveKeys(key, name); + final cipher = base64.decode(data.ciphertext); final hmac = base64 .encode(Hmac(sha256, keys.hmacKey) - .convert(base64.decode(data.ciphertext)) + .convert(cipher) .bytes) .replaceAll(RegExp(r'=+$'), ''); if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) { throw 'Bad MAC'; } - // workaround for https://github.com/leocavalcante/encrypt/issues/136 - final cipher = base64.decode(data.ciphertext); final decipher = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null) .decrypt(Encrypted(cipher), iv: IV(base64.decode(data.iv))); return String.fromCharCodes(decipher); From d7f2bbe2f94b3cbe2e2f1036fa17477e464d1d80 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 30 May 2020 13:22:34 +0200 Subject: [PATCH 30/64] smoothen out some stuffs --- lib/src/utils/device_keys_list.dart | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 211e2a3..79e4c63 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -30,7 +30,7 @@ class DeviceKeysList { } CrossSigningKey getCrossSigningKey(String type) => - crossSigningKeys.values.firstWhere((k) => k.usage.contains(type)); + crossSigningKeys.values.firstWhere((k) => k.usage.contains(type), orElse: () => null); CrossSigningKey get masterKey => getCrossSigningKey('master'); CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing'); @@ -144,17 +144,7 @@ abstract class SignedKey { bool get directVerified => _verified; - bool get crossVerified { - try { - return hasValidSignatureChain(); - } catch (err, stacktrace) { - print( - '[Cross Signing] Error during trying to determine signature chain: ' + - err.toString()); - print(stacktrace); - return false; - } - } + bool get crossVerified => hasValidSignatureChain(); bool get signed => hasValidSignatureChain(verifiedOnly: false); From 9971e7377e38dbdff466783ffa9824d8922e70e7 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 30 May 2020 13:55:09 +0200 Subject: [PATCH 31/64] configurable verification methods --- lib/src/client.dart | 17 +++++++++--- lib/src/cross_signing.dart | 6 +++-- lib/src/key_manager.dart | 40 +++++++++++++++++++--------- lib/src/ssss.dart | 11 ++++---- lib/src/utils/device_keys_list.dart | 4 +-- lib/src/utils/key_verification.dart | 41 ++++++++++++++++++++++------- pubspec.lock | 8 +++--- 7 files changed, 87 insertions(+), 40 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index d464121..5222f60 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -90,16 +90,25 @@ class Client { SSSS ssss; CrossSigning crossSigning; + Set verificationMethods; + /// Create a client /// clientName = unique identifier of this client /// debug: Print debug output? /// database: The database instance to use /// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions + /// verificationMethods: A set of all the verification methods this client can handle. Includes: + /// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported + /// KeyVerificationMethod.emoji: Compare emojis Client(this.clientName, - {this.debug = false, this.database, this.enableE2eeRecovery = false}) { + {this.debug = false, + this.database, + this.enableE2eeRecovery = false, + this.verificationMethods}) { ssss = SSSS(this); keyManager = KeyManager(this); crossSigning = CrossSigning(this); + verificationMethods ??= {}; onLoginStateChanged.stream.listen((loginState) { print('LoginState: ${loginState.toString()}'); }); @@ -1209,7 +1218,8 @@ class Client { } void _handleToDeviceKeyVerificationRequest(ToDeviceEvent toDeviceEvent) { - if (!toDeviceEvent.type.startsWith('m.key.verification.')) { + if (!toDeviceEvent.type.startsWith('m.key.verification.') || + verificationMethods.isEmpty) { return; } // we have key verification going on! @@ -1243,7 +1253,8 @@ class Client { final type = event['type'].startsWith('m.key.verification.') ? event['type'] : event['content']['msgtype']; - if (!type.startsWith('m.key.verification.')) { + if (!type.startsWith('m.key.verification.') || + verificationMethods.isEmpty) { return; } if (type == 'm.key.verification.request') { diff --git a/lib/src/cross_signing.dart b/lib/src/cross_signing.dart index a884a37..779e03e 100644 --- a/lib/src/cross_signing.dart +++ b/lib/src/cross_signing.dart @@ -16,7 +16,8 @@ class CrossSigning { client.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async { final keyObj = olm.PkSigning(); try { - return keyObj.init_with_seed(base64.decode(secret)) == client.userDeviceKeys[client.userID].selfSigningKey.ed25519Key; + return keyObj.init_with_seed(base64.decode(secret)) == + client.userDeviceKeys[client.userID].selfSigningKey.ed25519Key; } catch (_) { return false; } finally { @@ -26,7 +27,8 @@ class CrossSigning { client.ssss.setValidator(USER_SIGNING_KEY, (String secret) async { final keyObj = olm.PkSigning(); try { - return keyObj.init_with_seed(base64.decode(secret)) == client.userDeviceKeys[client.userID].userSigningKey.ed25519Key; + return keyObj.init_with_seed(base64.decode(secret)) == + client.userDeviceKeys[client.userID].userSigningKey.ed25519Key; } catch (_) { return false; } finally { diff --git a/lib/src/key_manager.dart b/lib/src/key_manager.dart index 5ac075d..ef24233 100644 --- a/lib/src/key_manager.dart +++ b/lib/src/key_manager.dart @@ -20,7 +20,8 @@ class KeyManager { final keyObj = olm.PkDecryption(); try { final info = await getRoomKeysInfo(); - return keyObj.init_with_private_key(base64.decode(secret)) == info['auth_data']['public_key']; + return keyObj.init_with_private_key(base64.decode(secret)) == + info['auth_data']['public_key']; } catch (_) { return false; } finally { @@ -59,13 +60,16 @@ class KeyManager { try { backupPubKey = decryption.init_with_private_key(privateKey); - if (backupPubKey == null || !info.containsKey('auth_data') || !(info['auth_data'] is Map) || info['auth_data']['public_key'] != backupPubKey) { - + if (backupPubKey == null || + !info.containsKey('auth_data') || + !(info['auth_data'] is Map) || + info['auth_data']['public_key'] != backupPubKey) { return; } for (final roomEntries in payload['rooms'].entries) { final roomId = roomEntries.key; - if (!(roomEntries.value is Map) || !(roomEntries.value['sessions'] is Map)) { + if (!(roomEntries.value is Map) || + !(roomEntries.value['sessions'] is Map)) { continue; } for (final sessionEntries in roomEntries.value['sessions'].entries) { @@ -74,24 +78,35 @@ class KeyManager { if (!(rawEncryptedSession is Map)) { continue; } - final firstMessageIndex = rawEncryptedSession['first_message_index'] is int ? rawEncryptedSession['first_message_index'] : null; - final forwardedCount = rawEncryptedSession['forwarded_count'] is int ? rawEncryptedSession['forwarded_count'] : null; - final isVerified = rawEncryptedSession['is_verified'] is bool ? rawEncryptedSession['is_verified'] : null; + final firstMessageIndex = + rawEncryptedSession['first_message_index'] is int + ? rawEncryptedSession['first_message_index'] + : null; + final forwardedCount = rawEncryptedSession['forwarded_count'] is int + ? rawEncryptedSession['forwarded_count'] + : null; + final isVerified = rawEncryptedSession['is_verified'] is bool + ? rawEncryptedSession['is_verified'] + : null; final sessionData = rawEncryptedSession['session_data']; - if (firstMessageIndex == null || forwardedCount == null || isVerified == null || !(sessionData is Map)) { + if (firstMessageIndex == null || + forwardedCount == null || + isVerified == null || + !(sessionData is Map)) { continue; } - final senderKey = sessionData['sender_key']; Map decrypted; try { - decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], sessionData['mac'], sessionData['ciphertext'])); + decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], + sessionData['mac'], sessionData['ciphertext'])); } catch (err) { print('[LibOlm] Error decrypting room key: ' + err.toString()); } if (decrypted != null) { decrypted['session_id'] = sessionId; decrypted['room_id'] = roomId; - final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client); + final room = + client.getRoomById(roomId) ?? Room(id: roomId, client: client); room.setInboundGroupSession(sessionId, decrypted, forwarded: true); } } @@ -105,7 +120,8 @@ class KeyManager { final info = await getRoomKeysInfo(); final ret = await client.jsonRequest( type: HTTPType.GET, - action: '/client/r0/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${info['version']}', + action: + '/client/r0/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${info['version']}', ); await loadFromResponse({ 'rooms': { diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart index 5eca4a4..a151ab4 100644 --- a/lib/src/ssss.dart +++ b/lib/src/ssss.dart @@ -72,9 +72,7 @@ class SSSS { final keys = deriveKeys(key, name); final cipher = base64.decode(data.ciphertext); final hmac = base64 - .encode(Hmac(sha256, keys.hmacKey) - .convert(cipher) - .bytes) + .encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes) .replaceAll(RegExp(r'=+$'), ''); if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) { throw 'Bad MAC'; @@ -322,7 +320,8 @@ class SSSS { return; // the secret wasn't a string....wut? } // let's validate if the secret is, well, valid - if (_validators.containsKey(request.type) && !(await _validators[request.type](secret))) { + if (_validators.containsKey(request.type) && + !(await _validators[request.type](secret))) { print('[SSSS] The received secret was invalid'); return; // didn't pass the validator } @@ -335,8 +334,8 @@ class SSSS { if (client.database != null) { final keyId = keyIdFromType(request.type); if (keyId != null) { - await client.database.storeSSSSCache( - client.id, request.type, keyId, secret); + await client.database + .storeSSSSCache(client.id, request.type, keyId, secret); } } } diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 79e4c63..18b3327 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -29,8 +29,8 @@ class DeviceKeysList { return null; } - CrossSigningKey getCrossSigningKey(String type) => - crossSigningKeys.values.firstWhere((k) => k.usage.contains(type), orElse: () => null); + CrossSigningKey getCrossSigningKey(String type) => crossSigningKeys.values + .firstWhere((k) => k.usage.contains(type), orElse: () => null); CrossSigningKey get masterKey => getCrossSigningKey('master'); CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing'); diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 1088ff4..195a6a6 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -52,6 +52,8 @@ enum KeyVerificationState { error } +enum KeyVerificationMethod { emoji, numbers } + List _intersect(List a, List b) { final res = []; for (final v in a) { @@ -82,8 +84,6 @@ List _bytesToInt(Uint8List bytes, int totalBits) { return ret; } -final VERIFICATION_METHODS = ['m.sas.v1']; - _KeyVerificationMethod _makeVerificationMethod( String type, KeyVerification request) { if (type == 'm.sas.v1') { @@ -134,9 +134,18 @@ class KeyVerification { : null); } + List get knownVerificationMethods { + final methods = []; + if (client.verificationMethods.contains(KeyVerificationMethod.numbers) || + client.verificationMethods.contains(KeyVerificationMethod.emoji)) { + methods.add('m.sas.v1'); + } + return methods; + } + Future sendStart() async { await send('m.key.verification.request', { - 'methods': VERIFICATION_METHODS, + 'methods': knownVerificationMethods, if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch, }); startedVerification = true; @@ -178,7 +187,7 @@ class KeyVerification { } // verify it has a method we can use possibleMethods = - _intersect(VERIFICATION_METHODS, payload['methods']); + _intersect(knownVerificationMethods, payload['methods']); if (possibleMethods.isEmpty) { // reject it outright await cancel('m.unknown_method'); @@ -189,7 +198,7 @@ class KeyVerification { case 'm.key.verification.ready': _deviceId ??= payload['from_device']; possibleMethods = - _intersect(VERIFICATION_METHODS, payload['methods']); + _intersect(knownVerificationMethods, payload['methods']); if (possibleMethods.isEmpty) { // reject it outright await cancel('m.unknown_method'); @@ -233,7 +242,7 @@ class KeyVerification { if (!(await verifyLastStep(['m.key.verification.request', null]))) { return; // abort } - if (!VERIFICATION_METHODS.contains(payload['method'])) { + if (!knownVerificationMethods.contains(payload['method'])) { await cancel('m.unknown_method'); return; } @@ -525,7 +534,6 @@ abstract class _KeyVerificationMethod { const KNOWN_KEY_AGREEMENT_PROTOCOLS = ['curve25519-hkdf-sha256', 'curve25519']; const KNOWN_HASHES = ['sha256']; const KNOWN_MESSAGE_AUTHENTIFICATION_CODES = ['hkdf-hmac-sha256']; -const KNOWN_AUTHENTICATION_TYPES = ['emoji', 'decimal']; class _KeyVerificationMethodSas extends _KeyVerificationMethod { _KeyVerificationMethodSas({KeyVerification request}) @@ -549,6 +557,19 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { sas?.free(); } + List get knownAuthentificationTypes { + final types = []; + if (request.client.verificationMethods + .contains(KeyVerificationMethod.emoji)) { + types.add('emoji'); + } + if (request.client.verificationMethods + .contains(KeyVerificationMethod.numbers)) { + types.add('decimal'); + } + return types; + } + @override Future handlePayload(String type, Map payload) async { try { @@ -630,7 +651,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { 'key_agreement_protocols': KNOWN_KEY_AGREEMENT_PROTOCOLS, 'hashes': KNOWN_HASHES, 'message_authentication_codes': KNOWN_MESSAGE_AUTHENTIFICATION_CODES, - 'short_authentication_string': KNOWN_AUTHENTICATION_TYPES, + 'short_authentication_string': knownAuthentificationTypes, }; request.makePayload(payload); // We just store the canonical json in here for later verification @@ -662,7 +683,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { } messageAuthenticationCode = possibleMessageAuthenticationCodes.first; final possibleAuthenticationTypes = _intersect( - KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']); + knownAuthentificationTypes, payload['short_authentication_string']); if (possibleAuthenticationTypes.isEmpty) { return false; } @@ -700,7 +721,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { } messageAuthenticationCode = payload['message_authentication_code']; final possibleAuthenticationTypes = _intersect( - KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']); + knownAuthentificationTypes, payload['short_authentication_string']); if (possibleAuthenticationTypes.isEmpty) { return false; } diff --git a/pubspec.lock b/pubspec.lock index 1d8863b..232c25e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -410,11 +410,9 @@ packages: olm: dependency: "direct main" description: - path: "." - ref: "0c612a525511652a7760126b058de8c924fe8900" - resolved-ref: "0c612a525511652a7760126b058de8c924fe8900" - url: "https://gitlab.com/famedly/libraries/dart-olm.git" - source: git + path: "/home/sorunome/repos/famedly/dart-olm" + relative: false + source: path version: "1.1.1" package_config: dependency: transitive From 03beffbb468782abd9b26950c2687d7c71eae815 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 30 May 2020 14:09:47 +0200 Subject: [PATCH 32/64] also load session keys when requesting history --- lib/src/room.dart | 93 ++++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/lib/src/room.dart b/lib/src/room.dart index 4c7048a..23981f3 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -1024,58 +1024,61 @@ class Room { if (onHistoryReceived != null) onHistoryReceived(); prev_batch = resp['end']; - final dbActions = Function()>[]; - if (client.database != null) { - dbActions.add( - () => client.database.setRoomPrevBatch(prev_batch, client.id, id)); - } + final loadFn = () async { + if (!(resp['chunk'] is List && + resp['chunk'].length > 0 && + resp['end'] is String)) return; - if (!(resp['chunk'] is List && - resp['chunk'].length > 0 && - resp['end'] is String)) return; - - if (resp['state'] is List) { - for (final state in resp['state']) { - var eventUpdate = EventUpdate( - type: 'state', - roomID: id, - eventType: state['type'], - content: state, - sortOrder: oldSortOrder, - ).decrypt(this); - client.onEvent.add(eventUpdate); - if (client.database != null) { - dbActions.add( - () => client.database.storeEventUpdate(client.id, eventUpdate)); + if (resp['state'] is List) { + for (final state in resp['state']) { + var eventUpdate = EventUpdate( + type: 'state', + roomID: id, + eventType: state['type'], + content: state, + sortOrder: oldSortOrder, + ).decrypt(this); + if (eventUpdate.eventType == 'm.room.encrypted' && + client.database != null) { + await loadInboundGroupSessionKey( + state['content']['session_id'], state['content']['sender_key']); + eventUpdate = eventUpdate.decrypt(this); + } + client.onEvent.add(eventUpdate); + await client.database?.storeEventUpdate(client.id, eventUpdate); } } - } - List history = resp['chunk']; - for (final hist in history) { - var eventUpdate = EventUpdate( - type: 'history', - roomID: id, - eventType: hist['type'], - content: hist, - sortOrder: oldSortOrder, - ).decrypt(this); - client.onEvent.add(eventUpdate); - if (client.database != null) { - dbActions.add( - () => client.database.storeEventUpdate(client.id, eventUpdate)); + List history = resp['chunk']; + for (final hist in history) { + var eventUpdate = EventUpdate( + type: 'history', + roomID: id, + eventType: hist['type'], + content: hist, + sortOrder: oldSortOrder, + ).decrypt(this); + if (eventUpdate.eventType == 'm.room.encrypted' && + client.database != null) { + await loadInboundGroupSessionKey( + hist['content']['session_id'], hist['content']['sender_key']); + eventUpdate = eventUpdate.decrypt(this); + } + client.onEvent.add(eventUpdate); + await client.database?.storeEventUpdate(client.id, eventUpdate); } - } + }; + if (client.database != null) { - dbActions.add( - () => client.database.setRoomPrevBatch(resp['end'], client.id, id)); + await client.database?.transaction(() async { + await client.database.setRoomPrevBatch(prev_batch, client.id, id); + await loadFn(); + await client.database.setRoomPrevBatch(resp['end'], client.id, id); + await updateSortOrder(); + }); + } else { + await loadFn(); } - await client.database?.transaction(() async { - for (final f in dbActions) { - await f(); - } - await updateSortOrder(); - }); client.onRoomUpdate.add( RoomUpdate( id: id, From 4c60369b8d5d85e755c40f5af9b9d916b8e1082e Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 22:03:28 +0200 Subject: [PATCH 33/64] migrate to new thingy! --- lib/{src => encryption}/cross_signing.dart | 49 ++- lib/encryption/encryption.dart | 16 + lib/encryption/key_manager.dart | 132 ++++++- lib/encryption/key_verification_manager.dart | 48 ++- lib/encryption/olm_manager.dart | 4 + lib/{src => encryption}/ssss.dart | 38 +- lib/encryption/utils/key_verification.dart | 21 +- lib/matrix_api.dart | 1 + lib/matrix_api/model/keys_query_response.dart | 54 +++ .../model/matrix_cross_signing_key.dart | 65 ++++ lib/src/client.dart | 105 +++--- lib/src/key_manager.dart | 349 ------------------ lib/src/utils/device_keys_list.dart | 115 ++---- test/client_test.dart | 2 +- test/device_keys_list_test.dart | 24 +- .../encrypt_decrypt_to_device_test.dart | 14 +- test/encryption/key_request_test.dart | 4 +- 17 files changed, 489 insertions(+), 552 deletions(-) rename lib/{src => encryption}/cross_signing.dart (77%) rename lib/{src => encryption}/ssss.dart (92%) create mode 100644 lib/matrix_api/model/matrix_cross_signing_key.dart delete mode 100644 lib/src/key_manager.dart diff --git a/lib/src/cross_signing.dart b/lib/encryption/cross_signing.dart similarity index 77% rename from lib/src/cross_signing.dart rename to lib/encryption/cross_signing.dart index 779e03e..a163c51 100644 --- a/lib/src/cross_signing.dart +++ b/lib/encryption/cross_signing.dart @@ -1,19 +1,38 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import 'dart:typed_data'; import 'dart:convert'; import 'package:olm/olm.dart' as olm; +import 'package:famedlysdk/famedlysdk.dart'; -import 'client.dart'; -import 'utils/device_keys_list.dart'; +import 'encryption.dart'; const SELF_SIGNING_KEY = 'm.cross_signing.self_signing'; const USER_SIGNING_KEY = 'm.cross_signing.user_signing'; const MASTER_KEY = 'm.cross_signing.master'; class CrossSigning { - final Client client; - CrossSigning(this.client) { - client.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async { + final Encryption encryption; + Client get client => encryption.client; + CrossSigning(this.encryption) { + encryption.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async { final keyObj = olm.PkSigning(); try { return keyObj.init_with_seed(base64.decode(secret)) == @@ -24,7 +43,7 @@ class CrossSigning { keyObj.free(); } }); - client.ssss.setValidator(USER_SIGNING_KEY, (String secret) async { + encryption.ssss.setValidator(USER_SIGNING_KEY, (String secret) async { final keyObj = olm.PkSigning(); try { return keyObj.init_with_seed(base64.decode(secret)) == @@ -46,12 +65,12 @@ class CrossSigning { if (!enabled) { return false; } - return (await client.ssss.getCached(SELF_SIGNING_KEY)) != null && - (await client.ssss.getCached(USER_SIGNING_KEY)) != null; + return (await encryption.ssss.getCached(SELF_SIGNING_KEY)) != null && + (await encryption.ssss.getCached(USER_SIGNING_KEY)) != null; } Future selfSign({String password, String recoveryKey}) async { - final handle = client.ssss.open(MASTER_KEY); + final handle = encryption.ssss.open(MASTER_KEY); await handle.unlock(password: password, recoveryKey: recoveryKey); await handle.maybeCacheAll(); final masterPrivateKey = base64.decode(await handle.getStored(MASTER_KEY)); @@ -133,7 +152,7 @@ class CrossSigning { if (key is CrossSigningKey) { if (key.usage.contains('master')) { // okay, we'll sign our own master key - final signature = client.signString(key.signingContent); + final signature = encryption.olmManager.signString(key.signingContent); addSignature( key, client @@ -144,8 +163,8 @@ class CrossSigning { } else { // okay, we'll sign a device key with our self signing key selfSigningKey ??= base64 - .decode(await client.ssss.getCached(SELF_SIGNING_KEY) ?? ''); - if (selfSigningKey != null) { + .decode(await encryption.ssss.getCached(SELF_SIGNING_KEY) ?? ''); + if (selfSigningKey.isNotEmpty) { final signature = _sign(key.signingContent, selfSigningKey); addSignature(key, client.userDeviceKeys[client.userID].selfSigningKey, signature); @@ -154,8 +173,8 @@ class CrossSigning { } else if (key is CrossSigningKey && key.usage.contains('master')) { // we are signing someone elses master key userSigningKey ??= - base64.decode(await client.ssss.getCached(USER_SIGNING_KEY) ?? ''); - if (userSigningKey != null) { + base64.decode(await encryption.ssss.getCached(USER_SIGNING_KEY) ?? ''); + if (userSigningKey.isNotEmpty) { final signature = _sign(key.signingContent, userSigningKey); addSignature(key, client.userDeviceKeys[client.userID].userSigningKey, signature); @@ -165,7 +184,7 @@ class CrossSigning { if (signedKey) { // post our new keys! await client.jsonRequest( - type: HTTPType.POST, + type: RequestType.POST, action: '/client/r0/keys/signatures/upload', data: signatures, ); diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart index 072d0c8..4cd54f0 100644 --- a/lib/encryption/encryption.dart +++ b/lib/encryption/encryption.dart @@ -24,6 +24,8 @@ import 'package:pedantic/pedantic.dart'; import 'key_manager.dart'; import 'olm_manager.dart'; import 'key_verification_manager.dart'; +import 'cross_signing.dart'; +import 'ssss.dart'; class Encryption { final Client client; @@ -42,15 +44,19 @@ class Encryption { KeyManager keyManager; OlmManager olmManager; KeyVerificationManager keyVerificationManager; + CrossSigning crossSigning; + SSSS ssss; Encryption({ this.client, this.debug, this.enableE2eeRecovery, }) { + ssss = SSSS(this); keyManager = KeyManager(this); olmManager = OlmManager(this); keyVerificationManager = KeyVerificationManager(this); + crossSigning = CrossSigning(this); } Future init(String olmAccount) async { @@ -79,6 +85,16 @@ class Encryption { } } + Future handleEventUpdate(EventUpdate update) async { + if (update.type == 'ephemeral') { + return; + } + if (update.eventType.startsWith('m.key.verification.') || (update.eventType == 'm.room.message' && (update.content['content']['msgtype'] is String) && update.content['content']['msgtype'].startsWith('m.key.verification.'))) { + // "just" key verification, no need to do this in sync + unawaited(keyVerificationManager.handleEventUpdate(update)); + } + } + Future decryptToDeviceEvent(ToDeviceEvent event) async { return await olmManager.decryptToDeviceEvent(event); } diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 4121c11..7855de3 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -27,6 +27,8 @@ import './encryption.dart'; import './utils/session_key.dart'; import './utils/outbound_group_session.dart'; +const MEGOLM_KEY = 'm.megolm_backup.v1'; + class KeyManager { final Encryption encryption; Client get client => encryption.client; @@ -37,7 +39,22 @@ class KeyManager { final Set _loadedOutboundGroupSessions = {}; final Set _requestedSessionIds = {}; - KeyManager(this.encryption); + KeyManager(this.encryption) { + encryption.ssss.setValidator(MEGOLM_KEY, (String secret) async { + final keyObj = olm.PkDecryption(); + try { + final info = await getRoomKeysInfo(); + return keyObj.init_with_private_key(base64.decode(secret)) == + info['auth_data']['public_key']; + } catch (_) { + return false; + } finally { + keyObj.free(); + } + }); + } + + bool get enabled => client.accountData[MEGOLM_KEY] != null; /// clear all cached inbound group sessions. useful for testing void clearInboundGroupSessions() { @@ -283,8 +300,120 @@ class KeyManager { _outboundGroupSessions[roomId] = sess; } + Future> getRoomKeysInfo() async { + return await client.jsonRequest( + type: RequestType.GET, + action: '/client/r0/room_keys/version', + ); + } + + Future isCached() async { + if (!enabled) { + return false; + } + return (await encryption.ssss.getCached(MEGOLM_KEY)) != null; + } + + Future loadFromResponse(Map payload) async { + if (!(await isCached())) { + return; + } + if (!(payload['rooms'] is Map)) { + return; + } + final privateKey = base64.decode(await encryption.ssss.getCached(MEGOLM_KEY)); + final decryption = olm.PkDecryption(); + final info = await getRoomKeysInfo(); + String backupPubKey; + try { + backupPubKey = decryption.init_with_private_key(privateKey); + + if (backupPubKey == null || + !info.containsKey('auth_data') || + !(info['auth_data'] is Map) || + info['auth_data']['public_key'] != backupPubKey) { + return; + } + for (final roomEntries in payload['rooms'].entries) { + final roomId = roomEntries.key; + if (!(roomEntries.value is Map) || + !(roomEntries.value['sessions'] is Map)) { + continue; + } + for (final sessionEntries in roomEntries.value['sessions'].entries) { + final sessionId = sessionEntries.key; + final rawEncryptedSession = sessionEntries.value; + if (!(rawEncryptedSession is Map)) { + continue; + } + final firstMessageIndex = + rawEncryptedSession['first_message_index'] is int + ? rawEncryptedSession['first_message_index'] + : null; + final forwardedCount = rawEncryptedSession['forwarded_count'] is int + ? rawEncryptedSession['forwarded_count'] + : null; + final isVerified = rawEncryptedSession['is_verified'] is bool + ? rawEncryptedSession['is_verified'] + : null; + final sessionData = rawEncryptedSession['session_data']; + if (firstMessageIndex == null || + forwardedCount == null || + isVerified == null || + !(sessionData is Map)) { + continue; + } + Map decrypted; + try { + decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], + sessionData['mac'], sessionData['ciphertext'])); + } catch (err) { + print('[LibOlm] Error decrypting room key: ' + err.toString()); + } + if (decrypted != null) { + decrypted['session_id'] = sessionId; + decrypted['room_id'] = roomId; + setInboundGroupSession(roomId, sessionId, decrypted['sender_key'], decrypted, forwarded: true); + } + } + } + } finally { + decryption.free(); + } + } + + Future loadSingleKey(String roomId, String sessionId) async { + final info = await getRoomKeysInfo(); + final ret = await client.jsonRequest( + type: RequestType.GET, + action: + '/client/r0/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${info['version']}', + ); + await loadFromResponse({ + 'rooms': { + roomId: { + 'sessions': { + sessionId: ret, + }, + }, + }, + }); + } + /// Request a certain key from another device Future request(Room room, String sessionId, String senderKey) async { + // let's first check our online key backup store thingy... + var hadPreviously = getInboundGroupSession(room.id, sessionId, senderKey) != null; + try { + await loadSingleKey(room.id, sessionId); + } catch (err, stacktrace) { + print('++++++++++++++++++'); + print(err.toString()); + print(stacktrace); + } + if (!hadPreviously && getInboundGroupSession(room.id, sessionId, senderKey) != null) { + return; // we managed to load the session from online backup, no need to care about it now + } // while we just send the to-device event to '*', we still need to save the // devices themself to know where to send the cancel to after receiving a reply final devices = await room.getUserDeviceKeys(); @@ -500,7 +629,6 @@ class RoomKeyRequest extends ToDeviceEvent { for (final key in session.forwardingCurve25519KeyChain) { forwardedKeys.add(key); } - await requestingDevice.setVerified(true, keyManager.client); var message = session.content; message['forwarding_curve25519_key_chain'] = forwardedKeys; diff --git a/lib/encryption/key_verification_manager.dart b/lib/encryption/key_verification_manager.dart index f8720f4..6170e24 100644 --- a/lib/encryption/key_verification_manager.dart +++ b/lib/encryption/key_verification_manager.dart @@ -51,7 +51,7 @@ class KeyVerificationManager { } Future handleToDeviceEvent(ToDeviceEvent event) async { - if (!event.type.startsWith('m.key.verification')) { + if (!event.type.startsWith('m.key.verification') || client.verificationMethods.isEmpty) { return; } // we have key verification going on! @@ -75,6 +75,52 @@ class KeyVerificationManager { } } + Future handleEventUpdate(EventUpdate update) async { + final event = update.content; + final type = event['type'].startsWith('m.key.verification.') + ? event['type'] + : event['content']['msgtype']; + if (type == null || !type.startsWith('m.key.verification.') || client.verificationMethods.isEmpty) { + return; + } + if (type == 'm.key.verification.request') { + event['content']['timestamp'] = event['origin_server_ts']; + } + + final transactionId = + KeyVerification.getTransactionId(event['content']) ?? event['event_id']; + + if (_requests.containsKey(transactionId)) { + final req = _requests[transactionId]; + if (event['sender'] != client.userID) { + req.handlePayload(type, event['content'], event['event_id']); + } else if (req.userId == client.userID && req.deviceId == null) { + // okay, maybe another of our devices answered + await req.handlePayload(type, event['content'], event['event_id']); + if (req.deviceId != client.deviceID) { + req.otherDeviceAccepted(); + req.dispose(); + _requests.remove(transactionId); + } + } + } else if (event['sender'] != client.userID) { + final room = + client.getRoomById(update.roomID) ?? Room(id: update.roomID, client: client); + final newKeyRequest = + KeyVerification(encryption: encryption, userId: event['sender'], room: room); + await newKeyRequest + .handlePayload(type, event['content'], event['event_id']); + if (newKeyRequest.state != KeyVerificationState.askAccept) { + // something went wrong, let's just dispose the request + newKeyRequest.dispose(); + } else { + // new request! Let's notify it and stuff + _requests[transactionId] = newKeyRequest; + client.onKeyVerificationRequest.add(newKeyRequest); + } + } + } + void dispose() { for (final req in _requests.values) { req.dispose(); diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart index e116dc7..5db0b78 100644 --- a/lib/encryption/olm_manager.dart +++ b/lib/encryption/olm_manager.dart @@ -96,6 +96,10 @@ class OlmManager { return payload; } + String signString(String s) { + return _olmAccount.sign(s); + } + /// Checks the signature of a signed json object. bool checkJsonSignature(String key, Map signedJson, String userId, String deviceId) { diff --git a/lib/src/ssss.dart b/lib/encryption/ssss.dart similarity index 92% rename from lib/src/ssss.dart rename to lib/encryption/ssss.dart index a151ab4..0d9bc0d 100644 --- a/lib/src/ssss.dart +++ b/lib/encryption/ssss.dart @@ -1,3 +1,21 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import 'dart:typed_data'; import 'dart:convert'; @@ -5,11 +23,10 @@ import 'package:encrypt/encrypt.dart'; import 'package:crypto/crypto.dart'; import 'package:base58check/base58.dart'; import 'package:password_hash/password_hash.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/matrix_api.dart'; -import 'client.dart'; -import 'account_data.dart'; -import 'utils/device_keys_list.dart'; -import 'utils/to_device_event.dart'; +import 'encryption.dart'; const CACHE_TYPES = [ 'm.cross_signing.self_signing', @@ -25,10 +42,11 @@ const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm class SSSS { - final Client client; + final Encryption encryption; + Client get client => encryption.client; final pendingShareRequests = {}; final _validators = Function(String)>{}; - SSSS(this.client); + SSSS(this.encryption); static _DerivedKeys deriveKeys(Uint8List key, String name) { final zerosalt = Uint8List(8); @@ -129,11 +147,11 @@ class SSSS { return keyData.content['key']; } - AccountData getKey(String keyId) { + BasicEvent getKey(String keyId) { return client.accountData['m.secret_storage.key.${keyId}']; } - bool checkKey(Uint8List key, AccountData keyData) { + bool checkKey(Uint8List key, BasicEvent keyData) { final info = keyData.content; if (info['algorithm'] == 'm.secret_storage.v1.aes-hmac-sha2') { if ((info['mac'] is String) && (info['iv'] is String)) { @@ -200,7 +218,7 @@ class SSSS { }; // store the thing in your account data await client.jsonRequest( - type: HTTPType.PUT, + type: RequestType.PUT, action: '/client/r0/user/${client.userID}/account_data/${type}', data: content, ); @@ -421,7 +439,7 @@ class _PasswordInfo { class OpenSSSS { final SSSS ssss; final String keyId; - final AccountData keyData; + final BasicEvent keyData; OpenSSSS({this.ssss, this.keyId, this.keyData}); Uint8List privateKey; diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart index 12247aa..d21b638 100644 --- a/lib/encryption/utils/key_verification.dart +++ b/lib/encryption/utils/key_verification.dart @@ -184,8 +184,8 @@ class KeyVerification { if (room == null) { transactionId = client.generateUniqueTransactionId(); } - if (client.crossSigning.enabled && - !(await client.crossSigning.isCached()) && + if (encryption.crossSigning.enabled && + !(await encryption.crossSigning.isCached()) && !client.isUnknownSession) { setState(KeyVerificationState.askSSSS); _nextAction = 'request'; @@ -241,7 +241,6 @@ class KeyVerification { print('Setting device id start: ' + _deviceId.toString()); transactionId ??= eventId ?? payload['transaction_id']; if (method != null) { - print('DUPLICATE START'); // the other side sent us a start, even though we already sent one if (payload['method'] == method.type) { // same method. Determine priority @@ -250,10 +249,8 @@ class KeyVerification { entries.sort(); if (entries.first == ourEntry) { // our start won, nothing to do - print('we won, nothing to do'); return; } else { - print('They won, handing off'); // the other start won, let's hand off startedVerification = false; // it is now as if they started lastStep = @@ -324,7 +321,7 @@ class KeyVerification { } else if (_nextAction == 'done') { if (_verifiedDevices != null) { // and now let's sign them all in the background - client.crossSigning.sign(_verifiedDevices); + encryption.crossSigning.sign(_verifiedDevices); } setState(KeyVerificationState.done); } @@ -333,7 +330,7 @@ class KeyVerification { next(); return; } - final handle = client.ssss.open('m.cross_signing.user_signing'); + final handle = encryption.ssss.open('m.cross_signing.user_signing'); await handle.unlock(password: password, recoveryKey: recoveryKey); await handle.maybeCacheAll(); next(); @@ -437,18 +434,18 @@ class KeyVerification { if (verifiedMasterKey && userId == client.userID) { // it was our own master key, let's request the cross signing keys // we do it in the background, thus no await needed here - unawaited(client.ssss + unawaited(encryption.ssss .maybeRequestAll(_verifiedDevices.whereType().toList())); } await send('m.key.verification.done', {}); var askingSSSS = false; - if (client.crossSigning.enabled && - client.crossSigning.signable(_verifiedDevices)) { + if (encryption.crossSigning.enabled && + encryption.crossSigning.signable(_verifiedDevices)) { // these keys can be signed! Let's do so - if (await client.crossSigning.isCached()) { + if (await encryption.crossSigning.isCached()) { // and now let's sign them all in the background - unawaited(client.crossSigning.sign(_verifiedDevices)); + unawaited(encryption.crossSigning.sign(_verifiedDevices)); } else if (!wasUnknownSession) { askingSSSS = true; } diff --git a/lib/matrix_api.dart b/lib/matrix_api.dart index d364cf8..b12702b 100644 --- a/lib/matrix_api.dart +++ b/lib/matrix_api.dart @@ -31,6 +31,7 @@ export 'package:famedlysdk/matrix_api/model/filter.dart'; export 'package:famedlysdk/matrix_api/model/keys_query_response.dart'; export 'package:famedlysdk/matrix_api/model/login_response.dart'; export 'package:famedlysdk/matrix_api/model/login_types.dart'; +export 'package:famedlysdk/matrix_api/model/matrix_cross_signing_key.dart'; export 'package:famedlysdk/matrix_api/model/matrix_device_keys.dart'; export 'package:famedlysdk/matrix_api/model/matrix_exception.dart'; export 'package:famedlysdk/matrix_api/model/message_types.dart'; diff --git a/lib/matrix_api/model/keys_query_response.dart b/lib/matrix_api/model/keys_query_response.dart index 8c6bb32..3e4f2a1 100644 --- a/lib/matrix_api/model/keys_query_response.dart +++ b/lib/matrix_api/model/keys_query_response.dart @@ -17,10 +17,14 @@ */ import 'matrix_device_keys.dart'; +import 'matrix_cross_signing_key.dart'; class KeysQueryResponse { Map failures; Map> deviceKeys; + Map masterKeys; + Map selfSigningKeys; + Map userSigningKeys; KeysQueryResponse.fromJson(Map json) { failures = Map.from(json['failures']); @@ -37,6 +41,32 @@ class KeysQueryResponse { ), ) : null; + masterKeys = json['master_keys'] != null ? + (json['master_keys'] as Map).map( + (k, v) => MapEntry( + k, + MatrixCrossSigningKey.fromJson(v), + ), + ) + : null; + + selfSigningKeys = json['self_signing_keys'] != null ? + (json['self_signing_keys'] as Map).map( + (k, v) => MapEntry( + k, + MatrixCrossSigningKey.fromJson(v), + ), + ) + : null; + + userSigningKeys = json['user_signing_keys'] != null ? + (json['user_signing_keys'] as Map).map( + (k, v) => MapEntry( + k, + MatrixCrossSigningKey.fromJson(v), + ), + ) + : null; } Map toJson() { @@ -57,6 +87,30 @@ class KeysQueryResponse { ), ); } + if (masterKeys != null) { + data['master_keys'] = masterKeys.map( + (k, v) => MapEntry( + k, + v.toJson(), + ), + ); + } + if (selfSigningKeys != null) { + data['self_signing_keys'] = selfSigningKeys.map( + (k, v) => MapEntry( + k, + v.toJson(), + ), + ); + } + if (userSigningKeys != null) { + data['user_signing_keys'] = userSigningKeys.map( + (k, v) => MapEntry( + k, + v.toJson(), + ), + ); + } return data; } } diff --git a/lib/matrix_api/model/matrix_cross_signing_key.dart b/lib/matrix_api/model/matrix_cross_signing_key.dart new file mode 100644 index 0000000..2a852a1 --- /dev/null +++ b/lib/matrix_api/model/matrix_cross_signing_key.dart @@ -0,0 +1,65 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +class MatrixCrossSigningKey { + String userId; + List usage; + Map keys; + Map> signatures; + Map unsigned; + String get publicKey => keys?.values?.first; + + MatrixCrossSigningKey( + this.userId, + this.usage, + this.keys, + this.signatures, { + this.unsigned, + }); + + // This object is used for signing so we need the raw json too + Map _json; + + MatrixCrossSigningKey.fromJson(Map json) { + _json = json; + userId = json['user_id']; + usage = List.from(json['usage']); + keys = Map.from(json['keys']); + signatures = Map>.from( + (json['signatures'] as Map) + .map((k, v) => MapEntry(k, Map.from(v)))); + unsigned = json['unsigned'] != null + ? Map.from(json['unsigned']) + : null; + } + + Map toJson() { + final data = _json ?? {}; + data['user_id'] = userId; + data['usage'] = usage; + data['keys'] = keys; + + if (signatures != null) { + data['signatures'] = signatures; + } + if (unsigned != null) { + data['unsigned'] = unsigned; + } + return data; + } +} diff --git a/lib/src/client.dart b/lib/src/client.dart index 935420d..9102f55 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -41,11 +41,6 @@ typedef RoomSorter = int Function(Room a, Room b); enum LoginState { logged, loggedOut } -class GenericException implements Exception { - final dynamic content; - GenericException(this.content); -} - /// Represents a Matrix client to communicate with a /// [Matrix](https://matrix.org) homeserver and is the entry point for this /// SDK. @@ -61,6 +56,8 @@ class Client { Encryption encryption; + Set verificationMethods; + /// Create a client /// clientName = unique identifier of this client /// debug: Print debug output? @@ -73,7 +70,9 @@ class Client { {this.debug = false, this.database, this.enableE2eeRecovery = false, + this.verificationMethods, http.Client httpClient}) { + verificationMethods ??= {}; api = MatrixApi(debug: debug, httpClient: httpClient); onLoginStateChanged.stream.listen((loginState) { if (debug) { @@ -642,7 +641,7 @@ class Client { encryption?.pickledOlmAccount, ); } - _userDeviceKeys = await database.getUserDeviceKeys(id); + _userDeviceKeys = await database.getUserDeviceKeys(this); _rooms = await database.getRoomList(this, onlyLeft: false); _sortRooms(); accountData = await database.getAccountData(id); @@ -813,9 +812,6 @@ class Client { if (encryptionEnabled) { await encryption.handleToDeviceEvent(toDeviceEvent); } - if (toDeviceEvent.type.startsWith('m.secret.')) { - ssss.handleToDeviceEvent(toDeviceEvent); - } onToDeviceEvent.add(toDeviceEvent); } } @@ -969,11 +965,8 @@ class Client { await database.storeEventUpdate(id, update); } _updateRoomsByEventUpdate(update); - if (event['type'].startsWith('m.key.verification.') || - (event['type'] == 'm.room.message' && - (event['content']['msgtype'] is String) && - event['content']['msgtype'].startsWith('m.key.verification.'))) { - _handleRoomKeyVerificationRequest(update); + if (encryptionEnabled) { + await encryption.handleEventUpdate(update); } onEvent.add(update); @@ -1167,14 +1160,21 @@ class Client { final deviceId = rawDeviceKeyEntry.key; // Set the new device key for this device - - if (!oldKeys.containsKey(deviceId)) { - final entry = - DeviceKeys.fromMatrixDeviceKeys(rawDeviceKeyEntry.value); - if (entry.isValid) { + final entry = DeviceKeys.fromMatrixDeviceKeys(rawDeviceKeyEntry.value, this); + if (entry.isValid) { + // is this a new key or the same one as an old one? + // better store an update - the signatures might have changed! + if (!oldKeys.containsKey(deviceId) || + oldKeys[deviceId].ed25519Key == entry.ed25519Key) { + if (oldKeys.containsKey(deviceId)) { + // be sure to save the verified status + entry.setDirectVerified(oldKeys[deviceId].directVerified); + entry.blocked = oldKeys[deviceId].blocked; + entry.validSignatures = oldKeys[deviceId].validSignatures; + } _userDeviceKeys[userId].deviceKeys[deviceId] = entry; if (deviceId == deviceID && - entry.ed25519Key == encryption?.fingerprintKey) { + entry.ed25519Key == fingerprintKey) { // Always trust the own device entry.setDirectVerified(true); } @@ -1185,16 +1185,16 @@ class Client { _userDeviceKeys[userId].deviceKeys[deviceId] = oldKeys[deviceId]; } - if (database != null) { - dbActions.add(() => database.storeUserDeviceKey( - id, - userId, - deviceId, - json.encode(entry.toJson()), - entry.directVerified, - entry.blocked, - )); - } + } + if (database != null) { + dbActions.add(() => database.storeUserDeviceKey( + id, + userId, + deviceId, + json.encode(entry.toJson()), + entry.directVerified, + entry.blocked, + )); } } // delete old/unused entries @@ -1215,29 +1215,33 @@ class Client { } } // next we parse and persist the cross signing keys - for (final keyType in [ - 'master_keys', - 'self_signing_keys', - 'user_signing_keys' - ]) { - if (!(response[keyType] is Map)) { + final crossSigningTypes = { + 'master': response.masterKeys, + 'self_signing': response.selfSigningKeys, + 'user_signing': response.userSigningKeys, + }; + for (final crossSigningKeysEntry in crossSigningTypes.entries) { + final keyType = crossSigningKeysEntry.key; + final keys = crossSigningKeysEntry.value; + if (keys == null) { continue; } - for (final rawDeviceKeyListEntry in response[keyType].entries) { - final String userId = rawDeviceKeyListEntry.key; - final oldKeys = Map.from( - _userDeviceKeys[userId].crossSigningKeys); + for (final crossSigningKeyListEntry in keys.entries) { + final userId = crossSigningKeyListEntry.key; + if (!userDeviceKeys.containsKey(userId)) { + _userDeviceKeys[userId] = DeviceKeysList(userId); + } + final oldKeys = Map.from(_userDeviceKeys[userId].crossSigningKeys); _userDeviceKeys[userId].crossSigningKeys = {}; - // add the types we arne't handling atm back + // add the types we aren't handling atm back for (final oldEntry in oldKeys.entries) { - if (!oldEntry.value.usage.contains( - keyType.substring(0, keyType.length - '_keys'.length))) { + if (!oldEntry.value.usage.contains(keyType)) { _userDeviceKeys[userId].crossSigningKeys[oldEntry.key] = oldEntry.value; } } final entry = - CrossSigningKey.fromJson(rawDeviceKeyListEntry.value, this); + CrossSigningKey.fromMatrixCrossSigningKey(crossSigningKeyListEntry.value, this); if (entry.isValid) { final publicKey = entry.publicKey; if (!oldKeys.containsKey(publicKey) || @@ -1267,19 +1271,6 @@ class Client { )); } } - // delete old/unused entries - if (database != null) { - for (final oldCrossSigningKeyEntry in oldKeys.entries) { - final publicKey = oldCrossSigningKeyEntry.key; - if (!_userDeviceKeys[userId] - .crossSigningKeys - .containsKey(publicKey)) { - // we need to remove an old key - dbActions.add(() => database.removeUserCrossSigningKey( - id, userId, publicKey)); - } - } - } _userDeviceKeys[userId].outdated = false; if (database != null) { dbActions.add( diff --git a/lib/src/key_manager.dart b/lib/src/key_manager.dart deleted file mode 100644 index ef24233..0000000 --- a/lib/src/key_manager.dart +++ /dev/null @@ -1,349 +0,0 @@ -import 'dart:core'; -import 'dart:convert'; - -import 'package:olm/olm.dart' as olm; - -import 'client.dart'; -import 'room.dart'; -import 'utils/to_device_event.dart'; -import 'utils/device_keys_list.dart'; - -const MEGOLM_KEY = 'm.megolm_backup.v1'; - -class KeyManager { - final Client client; - final outgoingShareRequests = {}; - final incomingShareRequests = {}; - - KeyManager(this.client) { - client.ssss.setValidator(MEGOLM_KEY, (String secret) async { - final keyObj = olm.PkDecryption(); - try { - final info = await getRoomKeysInfo(); - return keyObj.init_with_private_key(base64.decode(secret)) == - info['auth_data']['public_key']; - } catch (_) { - return false; - } finally { - keyObj.free(); - } - }); - } - - bool get enabled => client.accountData[MEGOLM_KEY] != null; - - Future> getRoomKeysInfo() async { - return await client.jsonRequest( - type: HTTPType.GET, - action: '/client/r0/room_keys/version', - ); - } - - Future isCached() async { - if (!enabled) { - return false; - } - return (await client.ssss.getCached(MEGOLM_KEY)) != null; - } - - Future loadFromResponse(Map payload) async { - if (!(await isCached())) { - return; - } - if (!(payload['rooms'] is Map)) { - return; - } - final privateKey = base64.decode(await client.ssss.getCached(MEGOLM_KEY)); - final decryption = olm.PkDecryption(); - final info = await getRoomKeysInfo(); - String backupPubKey; - try { - backupPubKey = decryption.init_with_private_key(privateKey); - - if (backupPubKey == null || - !info.containsKey('auth_data') || - !(info['auth_data'] is Map) || - info['auth_data']['public_key'] != backupPubKey) { - return; - } - for (final roomEntries in payload['rooms'].entries) { - final roomId = roomEntries.key; - if (!(roomEntries.value is Map) || - !(roomEntries.value['sessions'] is Map)) { - continue; - } - for (final sessionEntries in roomEntries.value['sessions'].entries) { - final sessionId = sessionEntries.key; - final rawEncryptedSession = sessionEntries.value; - if (!(rawEncryptedSession is Map)) { - continue; - } - final firstMessageIndex = - rawEncryptedSession['first_message_index'] is int - ? rawEncryptedSession['first_message_index'] - : null; - final forwardedCount = rawEncryptedSession['forwarded_count'] is int - ? rawEncryptedSession['forwarded_count'] - : null; - final isVerified = rawEncryptedSession['is_verified'] is bool - ? rawEncryptedSession['is_verified'] - : null; - final sessionData = rawEncryptedSession['session_data']; - if (firstMessageIndex == null || - forwardedCount == null || - isVerified == null || - !(sessionData is Map)) { - continue; - } - Map decrypted; - try { - decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], - sessionData['mac'], sessionData['ciphertext'])); - } catch (err) { - print('[LibOlm] Error decrypting room key: ' + err.toString()); - } - if (decrypted != null) { - decrypted['session_id'] = sessionId; - decrypted['room_id'] = roomId; - final room = - client.getRoomById(roomId) ?? Room(id: roomId, client: client); - room.setInboundGroupSession(sessionId, decrypted, forwarded: true); - } - } - } - } finally { - decryption.free(); - } - } - - Future loadSingleKey(String roomId, String sessionId) async { - final info = await getRoomKeysInfo(); - final ret = await client.jsonRequest( - type: HTTPType.GET, - action: - '/client/r0/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${info['version']}', - ); - await loadFromResponse({ - 'rooms': { - roomId: { - 'sessions': { - sessionId: ret, - }, - }, - }, - }); - } - - /// Request a certain key from another device - Future request(Room room, String sessionId, String senderKey) async { - // let's first check our online key backup store thingy... - var hadPreviously = room.inboundGroupSessions.containsKey(sessionId); - try { - await loadSingleKey(room.id, sessionId); - } catch (err, stacktrace) { - print('++++++++++++++++++'); - print(err.toString()); - print(stacktrace); - } - if (!hadPreviously && room.inboundGroupSessions.containsKey(sessionId)) { - return; // we managed to load the session from online backup, no need to care about it now - } - // while we just send the to-device event to '*', we still need to save the - // devices themself to know where to send the cancel to after receiving a reply - final devices = await room.getUserDeviceKeys(); - final requestId = client.generateUniqueTransactionId(); - final request = KeyManagerKeyShareRequest( - requestId: requestId, - devices: devices, - room: room, - sessionId: sessionId, - senderKey: senderKey, - ); - await client.sendToDevice( - [], - 'm.room_key_request', - { - 'action': 'request', - 'body': { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': room.id, - 'sender_key': senderKey, - 'session_id': sessionId, - }, - 'request_id': requestId, - 'requesting_device_id': client.deviceID, - }, - encrypted: false, - toUsers: await room.requestParticipants()); - outgoingShareRequests[request.requestId] = request; - } - - /// Handle an incoming to_device event that is related to key sharing - Future handleToDeviceEvent(ToDeviceEvent event) async { - if (event.type == 'm.room_key_request') { - if (!event.content.containsKey('request_id')) { - return; // invalid event - } - if (event.content['action'] == 'request') { - // we are *receiving* a request - if (!event.content.containsKey('body')) { - return; // no body - } - if (!client.userDeviceKeys.containsKey(event.sender) || - !client.userDeviceKeys[event.sender].deviceKeys - .containsKey(event.content['requesting_device_id'])) { - return; // device not found - } - final device = client.userDeviceKeys[event.sender] - .deviceKeys[event.content['requesting_device_id']]; - if (device.userId == client.userID && - device.deviceId == client.deviceID) { - return; // ignore requests by ourself - } - final room = client.getRoomById(event.content['body']['room_id']); - if (room == null) { - return; // unknown room - } - final sessionId = event.content['body']['session_id']; - // okay, let's see if we have this session at all - await room.loadInboundGroupSessionKey(sessionId); - if (!room.inboundGroupSessions.containsKey(sessionId)) { - return; // we don't have this session anyways - } - final request = KeyManagerKeyShareRequest( - requestId: event.content['request_id'], - devices: [device], - room: room, - sessionId: event.content['body']['session_id'], - senderKey: event.content['body']['sender_key'], - ); - if (incomingShareRequests.containsKey(request.requestId)) { - return; // we don't want to process one and the same request multiple times - } - incomingShareRequests[request.requestId] = request; - final roomKeyRequest = - RoomKeyRequest.fromToDeviceEvent(event, this, request); - if (device.userId == client.userID && - device.verified && - !device.blocked) { - // alright, we can forward the key - await roomKeyRequest.forwardKey(); - } else { - client.onRoomKeyRequest - .add(roomKeyRequest); // let the client handle this - } - } else if (event.content['action'] == 'request_cancellation') { - // we got told to cancel an incoming request - if (!incomingShareRequests.containsKey(event.content['request_id'])) { - return; // we don't know this request anyways - } - // alright, let's just cancel this request - final request = incomingShareRequests[event.content['request_id']]; - request.canceled = true; - incomingShareRequests.remove(request.requestId); - } - } else if (event.type == 'm.forwarded_room_key') { - // we *received* an incoming key request - if (event.encryptedContent == null) { - return; // event wasn't encrypted, this is a security risk - } - final request = outgoingShareRequests.values.firstWhere( - (r) => - r.room.id == event.content['room_id'] && - r.sessionId == event.content['session_id'] && - r.senderKey == event.content['sender_key'], - orElse: () => null); - if (request == null || request.canceled) { - return; // no associated request found or it got canceled - } - final device = request.devices.firstWhere( - (d) => - d.userId == event.sender && - d.curve25519Key == event.encryptedContent['sender_key'], - orElse: () => null); - if (device == null) { - return; // someone we didn't send our request to replied....better ignore this - } - // TODO: verify that the keys work to decrypt a message - // alright, all checks out, let's go ahead and store this session - request.room.setInboundGroupSession(request.sessionId, event.content, - forwarded: true); - request.devices.removeWhere( - (k) => k.userId == device.userId && k.deviceId == device.deviceId); - outgoingShareRequests.remove(request.requestId); - // send cancel to all other devices - if (request.devices.isEmpty) { - return; // no need to send any cancellation - } - await client.sendToDevice( - request.devices, - 'm.room_key_request', - { - 'action': 'request_cancellation', - 'request_id': request.requestId, - 'requesting_device_id': client.deviceID, - }, - encrypted: false); - } - } -} - -class KeyManagerKeyShareRequest { - final String requestId; - final List devices; - final Room room; - final String sessionId; - final String senderKey; - bool canceled; - - KeyManagerKeyShareRequest( - {this.requestId, - this.devices, - this.room, - this.sessionId, - this.senderKey, - this.canceled = false}); -} - -class RoomKeyRequest extends ToDeviceEvent { - KeyManager keyManager; - KeyManagerKeyShareRequest request; - RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent, - KeyManager keyManager, KeyManagerKeyShareRequest request) { - this.keyManager = keyManager; - this.request = request; - sender = toDeviceEvent.sender; - content = toDeviceEvent.content; - type = toDeviceEvent.type; - } - - Room get room => request.room; - - DeviceKeys get requestingDevice => request.devices.first; - - Future forwardKey() async { - if (request.canceled) { - keyManager.incomingShareRequests.remove(request.requestId); - return; // request is canceled, don't send anything - } - var room = this.room; - await room.loadInboundGroupSessionKey(request.sessionId); - final session = room.inboundGroupSessions[request.sessionId]; - var forwardedKeys = [keyManager.client.identityKey]; - for (final key in session.forwardingCurve25519KeyChain) { - forwardedKeys.add(key); - } - var message = session.content; - message['forwarding_curve25519_key_chain'] = forwardedKeys; - - message['session_key'] = session.inboundGroupSession - .export_session(session.inboundGroupSession.first_known_index()); - // send the actual reply of the key back to the requester - await keyManager.client.sendToDevice( - [requestingDevice], - 'm.forwarded_room_key', - message, - ); - keyManager.incomingShareRequests.remove(request.requestId); - } -} diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index bfc3b40..6f31e81 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -60,7 +60,7 @@ class DeviceKeysList { throw 'Unable to start new room'; } final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client); - final request = KeyVerification(client: client, room: room, userId: userId); + final request = KeyVerification(encryption: client.encryption, room: room, userId: userId); await request.start(); // no need to add to the request client object. As we are doing a room // verification request that'll happen automatically once we know the transaction id @@ -94,34 +94,6 @@ class DeviceKeysList { } } - DeviceKeysList.fromJson(Map json, Client cl) { - client = cl; - userId = json['user_id']; - outdated = json['outdated']; - deviceKeys = {}; - for (final rawDeviceKeyEntry in json['device_keys'].entries) { - deviceKeys[rawDeviceKeyEntry.key] = - DeviceKeys.fromJson(rawDeviceKeyEntry.value, client); - } - } - - Map toJson() { - var map = {}; - final data = map; - data['user_id'] = userId; - data['outdated'] = outdated ?? true; - - var rawDeviceKeys = {}; - for (final deviceKeyEntry in deviceKeys.entries) { - rawDeviceKeys[deviceKeyEntry.key] = deviceKeyEntry.value.toJson(); - } - data['device_keys'] = rawDeviceKeys; - return data; - } - - @override - String toString() => json.encode(toJson()); - DeviceKeysList(this.userId); } @@ -137,7 +109,6 @@ abstract class SignedKey { bool blocked; String get ed25519Key => keys['ed25519:$identifier']; - bool get verified => (directVerified || crossVerified) && !blocked; void setDirectVerified(bool v) { @@ -145,9 +116,7 @@ abstract class SignedKey { } bool get directVerified => _verified; - bool get crossVerified => hasValidSignatureChain(); - bool get signed => hasValidSignatureChain(verifiedOnly: false); String get signingContent { @@ -255,9 +224,9 @@ abstract class SignedKey { Future setVerified(bool newVerified, [bool sign = true]) { _verified = newVerified; - if (sign && client.crossSigning.signable([this])) { + if (sign && client.encryptionEnabled && client.encryption.crossSigning.signable([this])) { // sign the key! - client.crossSigning.sign([this]); + client.encryption.crossSigning.sign([this]); } return Future.value(); } @@ -297,6 +266,20 @@ class CrossSigningKey extends SignedKey { newBlocked, client.id, userId, publicKey); } + CrossSigningKey.fromMatrixCrossSigningKey(MatrixCrossSigningKey k, Client cl) { + client = cl; + content = Map.from(k.toJson()); + userId = k.userId; + identifier = k.publicKey; + usage = content['usage'].cast(); + keys = content['keys'] != null ? Map.from(content['keys']) : null; + signatures = content['signatures'] != null + ? Map.from(content['signatures']) + : null; + _verified = false; + blocked = false; + } + CrossSigningKey.fromDb(DbUserCrossSigningKey dbEntry, Client cl) { client = cl; final json = Event.getMapFromPayload(dbEntry.content); @@ -346,17 +329,36 @@ class DeviceKeys extends SignedKey { @override Future setVerified(bool newVerified, [bool sign = true]) { super.setVerified(newVerified, sign); - return client.database + return client?.database ?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId); } @override Future setBlocked(bool newBlocked) { blocked = newBlocked; - return client.database + return client?.database ?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId); } + DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys k, Client cl) { + client = cl; + content = Map.from(k.toJson()); + userId = k.userId; + identifier = k.deviceId; + algorithms = content['algorithms'].cast(); + keys = content['keys'] != null + ? Map.from(content['keys']) + : null; + signatures = content['signatures'] != null + ? Map.from(content['signatures']) + : null; + unsigned = content['unsigned'] != null + ? Map.from(content['unsigned']) + : null; + _verified = false; + blocked = false; + } + DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry, Client cl) { client = cl; final json = Event.getMapFromPayload(dbEntry.content); @@ -365,45 +367,10 @@ class DeviceKeys extends SignedKey { identifier = dbEntry.deviceId; algorithms = content['algorithms'].cast(); keys = content['keys'] != null - }) : super(userId, deviceId, algorithms, keys, signatures, - unsigned: unsigned); - - DeviceKeys({ - String userId, - String deviceId, - List algorithms, - Map keys, - Map> signatures, - Map unsigned, - this.verified, - this.blocked, - }) : super(userId, deviceId, algorithms, keys, signatures, - unsigned: unsigned); - - factory DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys matrixDeviceKeys) => - DeviceKeys( - userId: matrixDeviceKeys.userId, - deviceId: matrixDeviceKeys.deviceId, - algorithms: matrixDeviceKeys.algorithms, - keys: matrixDeviceKeys.keys, - signatures: matrixDeviceKeys.signatures, - unsigned: matrixDeviceKeys.unsigned, - verified: false, - blocked: false, - ); - - static DeviceKeys fromDb(DbUserDeviceKeysKey dbEntry) { - var deviceKeys = DeviceKeys(); - final content = Event.getMapFromPayload(dbEntry.content); - deviceKeys.userId = dbEntry.userId; - deviceKeys.deviceId = dbEntry.deviceId; - deviceKeys.algorithms = content['algorithms'].cast(); - deviceKeys.keys = content['keys'] != null ? Map.from(content['keys']) : null; - deviceKeys.signatures = content['signatures'] != null - ? Map>.from((content['signatures'] as Map) - .map((k, v) => MapEntry(k, Map.from(v)))) + signatures = content['signatures'] != null + ? Map.from(content['signatures']) : null; unsigned = json['unsigned'] != null ? Map.from(json['unsigned']) @@ -431,7 +398,7 @@ class DeviceKeys extends SignedKey { KeyVerification startVerification() { final request = - KeyVerification(client: client, userId: userId, deviceId: deviceId); + KeyVerification(encryption: client.encryption, userId: userId, deviceId: deviceId); request.start(); client.encryption.keyVerificationManager.addRequest(request); diff --git a/test/client_test.dart b/test/client_test.dart index 3963400..8c93b6d 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -388,7 +388,7 @@ void main() { 'dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA' } } - }); + }, matrix); test('sendToDevice', () async { await matrix.sendToDevice( [deviceKeys], diff --git a/test/device_keys_list_test.dart b/test/device_keys_list_test.dart index b16e2f2..54e3e1f 100644 --- a/test/device_keys_list_test.dart +++ b/test/device_keys_list_test.dart @@ -41,8 +41,6 @@ void main() { } }, 'unsigned': {'device_display_name': "Alice's mobile phone"}, - 'verified': false, - 'blocked': true, }; var rawListJson = { 'user_id': '@alice:example.com', @@ -50,28 +48,12 @@ void main() { 'device_keys': {'JLAFKJWSCS': rawJson}, }; - var userDeviceKeys = { - '@alice:example.com': DeviceKeysList.fromJson(rawListJson, null), - }; - var userDeviceKeyRaw = { - '@alice:example.com': rawListJson, - }; - final key = DeviceKeys.fromJson(rawJson, null); - rawJson.remove('verified'); - rawJson.remove('blocked'); + key.setVerified(false, false); + key.setBlocked(true); expect(json.encode(key.toJson()), json.encode(rawJson)); - expect(key.verified, false); + expect(key.directVerified, false); expect(key.blocked, true); - expect(json.encode(DeviceKeysList.fromJson(rawListJson, null).toJson()), - json.encode(rawListJson)); - - var mapFromRaw = {}; - for (final rawListEntry in userDeviceKeyRaw.entries) { - mapFromRaw[rawListEntry.key] = - DeviceKeysList.fromJson(rawListEntry.value, null); - } - expect(mapFromRaw.toString(), userDeviceKeys.toString()); }); }); } diff --git a/test/encryption/encrypt_decrypt_to_device_test.dart b/test/encryption/encrypt_decrypt_to_device_test.dart index 0be0a19..5636e8b 100644 --- a/test/encryption/encrypt_decrypt_to_device_test.dart +++ b/test/encryption/encrypt_decrypt_to_device_test.dart @@ -61,17 +61,15 @@ void main() { ); await Future.delayed(Duration(milliseconds: 10)); - device = DeviceKeys( - userId: client.userID, - deviceId: client.deviceID, - algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'], - keys: { + device = DeviceKeys.fromJson({ + 'user_id': client.userID, + 'device_id': client.deviceID, + 'algorithms': ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'], + 'keys': { 'curve25519:${client.deviceID}': client.identityKey, 'ed25519:${client.deviceID}': client.fingerprintKey, }, - verified: true, - blocked: false, - ); + }, client); }); test('encryptToDeviceMessage', () async { diff --git a/test/encryption/key_request_test.dart b/test/encryption/key_request_test.dart index 1ccf510..168a167 100644 --- a/test/encryption/key_request_test.dart +++ b/test/encryption/key_request_test.dart @@ -85,10 +85,10 @@ void main() { FakeMatrixApi.calledEndpoints.clear(); await matrix .userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE'] - .setBlocked(false, matrix); + .setBlocked(false); await matrix .userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE'] - .setVerified(true, matrix); + .setVerified(true); // test a successful share var event = ToDeviceEvent( sender: '@alice:example.com', From 115cd9e5b3b0621c8290e23f2ce50bac424fab84 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 22:22:07 +0200 Subject: [PATCH 34/64] better cache invalidation of ssss cache --- lib/encryption/ssss.dart | 9 +++-- lib/src/database/database.g.dart | 64 ++++++++++++++++++++++++++++---- lib/src/database/database.moor | 3 +- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/lib/encryption/ssss.dart b/lib/encryption/ssss.dart index 0d9bc0d..43da3ca 100644 --- a/lib/encryption/ssss.dart +++ b/lib/encryption/ssss.dart @@ -177,7 +177,7 @@ class SSSS { } // check if it is still valid final keys = keyIdsFromType(type); - if (keys.contains(ret.keyId)) { + if (keys.contains(ret.keyId) && client.accountData[type].content['encrypted'][ret.keyId]['ciphertext'] == ret.ciphertext) { return ret.content; } return null; @@ -200,7 +200,7 @@ class SSSS { final decrypted = decryptAes(encryptInfo, key, type); if (CACHE_TYPES.contains(type) && client.database != null) { // cache the thing - await client.database.storeSSSSCache(client.id, type, keyId, decrypted); + await client.database.storeSSSSCache(client.id, type, keyId, enc['ciphertext'], decrypted); } return decrypted; } @@ -224,7 +224,7 @@ class SSSS { ); if (CACHE_TYPES.contains(type) && client.database != null) { // cache the thing - await client.database.storeSSSSCache(client.id, type, keyId, secret); + await client.database.storeSSSSCache(client.id, type, keyId, encrypted.ciphertext, secret); } } @@ -352,8 +352,9 @@ class SSSS { if (client.database != null) { final keyId = keyIdFromType(request.type); if (keyId != null) { + final ciphertext = client.accountData[request.type].content['encrypted'][keyId]['ciphertext']; await client.database - .storeSSSSCache(client.id, request.type, keyId, secret); + .storeSSSSCache(client.id, request.type, keyId, ciphertext, secret); } } } diff --git a/lib/src/database/database.g.dart b/lib/src/database/database.g.dart index 6cfaac8..0c50118 100644 --- a/lib/src/database/database.g.dart +++ b/lib/src/database/database.g.dart @@ -4805,11 +4805,13 @@ class DbSSSSCache extends DataClass implements Insertable { final int clientId; final String type; final String keyId; + final String ciphertext; final String content; DbSSSSCache( {@required this.clientId, @required this.type, @required this.keyId, + @required this.ciphertext, @required this.content}); factory DbSSSSCache.fromData(Map data, GeneratedDatabase db, {String prefix}) { @@ -4822,6 +4824,8 @@ class DbSSSSCache extends DataClass implements Insertable { type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type']), keyId: stringType.mapFromDatabaseResponse(data['${effectivePrefix}key_id']), + ciphertext: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}ciphertext']), content: stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']), ); @@ -4838,6 +4842,9 @@ class DbSSSSCache extends DataClass implements Insertable { if (!nullToAbsent || keyId != null) { map['key_id'] = Variable(keyId); } + if (!nullToAbsent || ciphertext != null) { + map['ciphertext'] = Variable(ciphertext); + } if (!nullToAbsent || content != null) { map['content'] = Variable(content); } @@ -4851,6 +4858,7 @@ class DbSSSSCache extends DataClass implements Insertable { clientId: serializer.fromJson(json['client_id']), type: serializer.fromJson(json['type']), keyId: serializer.fromJson(json['key_id']), + ciphertext: serializer.fromJson(json['ciphertext']), content: serializer.fromJson(json['content']), ); } @@ -4861,16 +4869,22 @@ class DbSSSSCache extends DataClass implements Insertable { 'client_id': serializer.toJson(clientId), 'type': serializer.toJson(type), 'key_id': serializer.toJson(keyId), + 'ciphertext': serializer.toJson(ciphertext), 'content': serializer.toJson(content), }; } DbSSSSCache copyWith( - {int clientId, String type, String keyId, String content}) => + {int clientId, + String type, + String keyId, + String ciphertext, + String content}) => DbSSSSCache( clientId: clientId ?? this.clientId, type: type ?? this.type, keyId: keyId ?? this.keyId, + ciphertext: ciphertext ?? this.ciphertext, content: content ?? this.content, ); @override @@ -4879,14 +4893,19 @@ class DbSSSSCache extends DataClass implements Insertable { ..write('clientId: $clientId, ') ..write('type: $type, ') ..write('keyId: $keyId, ') + ..write('ciphertext: $ciphertext, ') ..write('content: $content') ..write(')')) .toString(); } @override - int get hashCode => $mrjf($mrjc(clientId.hashCode, - $mrjc(type.hashCode, $mrjc(keyId.hashCode, content.hashCode)))); + int get hashCode => $mrjf($mrjc( + clientId.hashCode, + $mrjc( + type.hashCode, + $mrjc( + keyId.hashCode, $mrjc(ciphertext.hashCode, content.hashCode))))); @override bool operator ==(dynamic other) => identical(this, other) || @@ -4894,6 +4913,7 @@ class DbSSSSCache extends DataClass implements Insertable { other.clientId == this.clientId && other.type == this.type && other.keyId == this.keyId && + other.ciphertext == this.ciphertext && other.content == this.content); } @@ -4901,32 +4921,38 @@ class SsssCacheCompanion extends UpdateCompanion { final Value clientId; final Value type; final Value keyId; + final Value ciphertext; final Value content; const SsssCacheCompanion({ this.clientId = const Value.absent(), this.type = const Value.absent(), this.keyId = const Value.absent(), + this.ciphertext = const Value.absent(), this.content = const Value.absent(), }); SsssCacheCompanion.insert({ @required int clientId, @required String type, @required String keyId, + @required String ciphertext, @required String content, }) : clientId = Value(clientId), type = Value(type), keyId = Value(keyId), + ciphertext = Value(ciphertext), content = Value(content); static Insertable custom({ Expression clientId, Expression type, Expression keyId, + Expression ciphertext, Expression content, }) { return RawValuesInsertable({ if (clientId != null) 'client_id': clientId, if (type != null) 'type': type, if (keyId != null) 'key_id': keyId, + if (ciphertext != null) 'ciphertext': ciphertext, if (content != null) 'content': content, }); } @@ -4935,11 +4961,13 @@ class SsssCacheCompanion extends UpdateCompanion { {Value clientId, Value type, Value keyId, + Value ciphertext, Value content}) { return SsssCacheCompanion( clientId: clientId ?? this.clientId, type: type ?? this.type, keyId: keyId ?? this.keyId, + ciphertext: ciphertext ?? this.ciphertext, content: content ?? this.content, ); } @@ -4956,6 +4984,9 @@ class SsssCacheCompanion extends UpdateCompanion { if (keyId.present) { map['key_id'] = Variable(keyId.value); } + if (ciphertext.present) { + map['ciphertext'] = Variable(ciphertext.value); + } if (content.present) { map['content'] = Variable(content.value); } @@ -4991,6 +5022,14 @@ class SsssCache extends Table with TableInfo { $customConstraints: 'NOT NULL'); } + final VerificationMeta _ciphertextMeta = const VerificationMeta('ciphertext'); + GeneratedTextColumn _ciphertext; + GeneratedTextColumn get ciphertext => _ciphertext ??= _constructCiphertext(); + GeneratedTextColumn _constructCiphertext() { + return GeneratedTextColumn('ciphertext', $tableName, false, + $customConstraints: 'NOT NULL'); + } + final VerificationMeta _contentMeta = const VerificationMeta('content'); GeneratedTextColumn _content; GeneratedTextColumn get content => _content ??= _constructContent(); @@ -5000,7 +5039,8 @@ class SsssCache extends Table with TableInfo { } @override - List get $columns => [clientId, type, keyId, content]; + List get $columns => + [clientId, type, keyId, ciphertext, content]; @override SsssCache get asDslTable => this; @override @@ -5030,6 +5070,14 @@ class SsssCache extends Table with TableInfo { } else if (isInserting) { context.missing(_keyIdMeta); } + if (data.containsKey('ciphertext')) { + context.handle( + _ciphertextMeta, + ciphertext.isAcceptableOrUnknown( + data['ciphertext'], _ciphertextMeta)); + } else if (isInserting) { + context.missing(_ciphertextMeta); + } if (data.containsKey('content')) { context.handle(_contentMeta, content.isAcceptableOrUnknown(data['content'], _contentMeta)); @@ -5770,14 +5818,15 @@ abstract class _$Database extends GeneratedDatabase { ); } - Future storeSSSSCache( - int client_id, String type, String key_id, String content) { + Future storeSSSSCache(int client_id, String type, String key_id, + String ciphertext, String content) { return customInsert( - 'INSERT OR REPLACE INTO ssss_cache (client_id, type, key_id, content) VALUES (:client_id, :type, :key_id, :content)', + 'INSERT OR REPLACE INTO ssss_cache (client_id, type, key_id, ciphertext, content) VALUES (:client_id, :type, :key_id, :ciphertext, :content)', variables: [ Variable.withInt(client_id), Variable.withString(type), Variable.withString(key_id), + Variable.withString(ciphertext), Variable.withString(content) ], updates: {ssssCache}, @@ -5789,6 +5838,7 @@ abstract class _$Database extends GeneratedDatabase { clientId: row.readInt('client_id'), type: row.readString('type'), keyId: row.readString('key_id'), + ciphertext: row.readString('ciphertext'), content: row.readString('content'), ); } diff --git a/lib/src/database/database.moor b/lib/src/database/database.moor index 2601b6c..68a0ee5 100644 --- a/lib/src/database/database.moor +++ b/lib/src/database/database.moor @@ -78,6 +78,7 @@ CREATE TABLE ssss_cache ( client_id INTEGER NOT NULL REFERENCES clients(client_id), type TEXT NOT NULL, key_id TEXT NOT NULL, + ciphertext TEXT NOT NULL, content TEXT NOT NULL, UNIQUE(client_id, type) ) AS DbSSSSCache; @@ -195,7 +196,7 @@ setVerifiedUserCrossSigningKey: UPDATE user_cross_signing_keys SET verified = :v setBlockedUserCrossSigningKey: UPDATE user_cross_signing_keys SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; storeUserCrossSigningKey: INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :verified, :blocked); removeUserCrossSigningKey: DELETE FROM user_cross_signing_keys WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; -storeSSSSCache: INSERT OR REPLACE INTO ssss_cache (client_id, type, key_id, content) VALUES (:client_id, :type, :key_id, :content); +storeSSSSCache: INSERT OR REPLACE INTO ssss_cache (client_id, type, key_id, ciphertext, content) VALUES (:client_id, :type, :key_id, :ciphertext, :content); dbGetSSSSCache: SELECT * FROM ssss_cache WHERE client_id = :client_id AND type = :type; insertClient: INSERT INTO clients (name, homeserver_url, token, user_id, device_id, device_name, prev_batch, olm_account) VALUES (:name, :homeserver_url, :token, :user_id, :device_id, :device_name, :prev_batch, :olm_account); ensureRoomExists: INSERT OR IGNORE INTO rooms (client_id, room_id, membership) VALUES (:client_id, :room_id, :membership); From d4eabbb756a2cedab1c9cfbb33521f34582b45dd Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 6 Jun 2020 12:40:52 +0200 Subject: [PATCH 35/64] ssss password --> passphrase --- lib/encryption/cross_signing.dart | 4 ++-- lib/encryption/ssss.dart | 18 +++++++++--------- lib/encryption/utils/key_verification.dart | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/encryption/cross_signing.dart b/lib/encryption/cross_signing.dart index a163c51..c3e809b 100644 --- a/lib/encryption/cross_signing.dart +++ b/lib/encryption/cross_signing.dart @@ -69,9 +69,9 @@ class CrossSigning { (await encryption.ssss.getCached(USER_SIGNING_KEY)) != null; } - Future selfSign({String password, String recoveryKey}) async { + Future selfSign({String passphrase, String recoveryKey}) async { final handle = encryption.ssss.open(MASTER_KEY); - await handle.unlock(password: password, recoveryKey: recoveryKey); + await handle.unlock(passphrase: passphrase, recoveryKey: recoveryKey); await handle.maybeCacheAll(); final masterPrivateKey = base64.decode(await handle.getStored(MASTER_KEY)); final keyObj = olm.PkSigning(); diff --git a/lib/encryption/ssss.dart b/lib/encryption/ssss.dart index 43da3ca..6785f3d 100644 --- a/lib/encryption/ssss.dart +++ b/lib/encryption/ssss.dart @@ -126,12 +126,12 @@ class SSSS { OLM_RECOVERY_KEY_PREFIX.length + OLM_PRIVATE_KEY_LENGTH)); } - static Uint8List keyFromPassword(String password, _PasswordInfo info) { + static Uint8List keyFromPassphrase(String passphrase, _PassphraseInfo info) { if (info.algorithm != 'm.pbkdf2') { throw 'Unknown algorithm'; } final generator = PBKDF2(hashAlgorithm: sha512); - return Uint8List.fromList(generator.generateKey(password, info.salt, + return Uint8List.fromList(generator.generateKey(passphrase, info.salt, info.iterations, info.bits != null ? info.bits / 8 : 32)); } @@ -428,13 +428,13 @@ class _DerivedKeys { _DerivedKeys({this.aesKey, this.hmacKey}); } -class _PasswordInfo { +class _PassphraseInfo { final String algorithm; final String salt; final int iterations; final int bits; - _PasswordInfo({this.algorithm, this.salt, this.iterations, this.bits}); + _PassphraseInfo({this.algorithm, this.salt, this.iterations, this.bits}); } class OpenSSSS { @@ -446,11 +446,11 @@ class OpenSSSS { bool get isUnlocked => privateKey != null; - void unlock({String password, String recoveryKey}) { - if (password != null) { - privateKey = SSSS.keyFromPassword( - password, - _PasswordInfo( + void unlock({String passphrase, String recoveryKey}) { + if (passphrase != null) { + privateKey = SSSS.keyFromPassphrase( + passphrase, + _PassphraseInfo( algorithm: keyData.content['passphrase']['algorithm'], salt: keyData.content['passphrase']['salt'], iterations: keyData.content['passphrase']['iterations'], diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart index d21b638..567d0b8 100644 --- a/lib/encryption/utils/key_verification.dart +++ b/lib/encryption/utils/key_verification.dart @@ -314,7 +314,7 @@ class KeyVerification { } Future openSSSS( - {String password, String recoveryKey, bool skip = false}) async { + {String passphrase, String recoveryKey, bool skip = false}) async { final next = () { if (_nextAction == 'request') { sendStart(); @@ -331,7 +331,7 @@ class KeyVerification { return; } final handle = encryption.ssss.open('m.cross_signing.user_signing'); - await handle.unlock(password: password, recoveryKey: recoveryKey); + await handle.unlock(passphrase: passphrase, recoveryKey: recoveryKey); await handle.maybeCacheAll(); next(); } From 060a772bfa542c882a47e0c20947cf6e9c77fa65 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 6 Jun 2020 13:38:19 +0200 Subject: [PATCH 36/64] fix up a few things with key verification --- lib/encryption/encryption.dart | 4 ++++ lib/encryption/key_verification_manager.dart | 6 +++++- lib/encryption/utils/key_verification.dart | 13 ++++++++----- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart index 4cd54f0..ef35840 100644 --- a/lib/encryption/encryption.dart +++ b/lib/encryption/encryption.dart @@ -83,6 +83,10 @@ class Encryption { // do this in the background unawaited(keyVerificationManager.handleToDeviceEvent(event)); } + if (event.type.startsWith('m.secret.')) { + // some ssss thing. We can do this in the background + unawaited(ssss.handleToDeviceEvent(event)); + } } Future handleEventUpdate(EventUpdate update) async { diff --git a/lib/encryption/key_verification_manager.dart b/lib/encryption/key_verification_manager.dart index 6170e24..c2accf5 100644 --- a/lib/encryption/key_verification_manager.dart +++ b/lib/encryption/key_verification_manager.dart @@ -29,6 +29,7 @@ class KeyVerificationManager { final Map _requests = {}; Future cleanup() async { + Set entriesToDispose = {}; for (final entry in _requests.entries) { var dispose = entry.value.canceled || entry.value.state == KeyVerificationState.done || @@ -38,9 +39,12 @@ class KeyVerificationManager { } if (dispose) { entry.value.dispose(); - _requests.remove(entry.key); + entriesToDispose.add(entry.key); } } + for (final k in entriesToDispose) { + _requests.remove(k); + } } void addRequest(KeyVerification request) { diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart index 567d0b8..9c5b1ef 100644 --- a/lib/encryption/utils/key_verification.dart +++ b/lib/encryption/utils/key_verification.dart @@ -146,7 +146,6 @@ class KeyVerification { this.onUpdate}) { lastActivity = DateTime.now(); _deviceId ??= deviceId; - print('Setting device id constructor: ' + _deviceId.toString()); } void dispose() { @@ -198,10 +197,10 @@ class KeyVerification { [String eventId]) async { print('[Key Verification] Received type ${type}: ' + payload.toString()); try { + var thisLastStep = lastStep; switch (type) { case 'm.key.verification.request': _deviceId ??= payload['from_device']; - print('Setting device id request: ' + _deviceId.toString()); transactionId ??= eventId ?? payload['transaction_id']; // verify the timestamp final now = DateTime.now(); @@ -231,6 +230,9 @@ class KeyVerification { await cancel('m.unknown_method'); return; } + // as both parties can send a start, the last step being "ready" is race-condition prone + // as such, we better set it *before* we send our start + lastStep = type; // TODO: Pick method? method = _makeVerificationMethod(possibleMethods.first, this); await method.sendStart(); @@ -238,7 +240,6 @@ class KeyVerification { break; case 'm.key.verification.start': _deviceId ??= payload['from_device']; - print('Setting device id start: ' + _deviceId.toString()); transactionId ??= eventId ?? payload['transaction_id']; if (method != null) { // the other side sent us a start, even though we already sent one @@ -253,7 +254,7 @@ class KeyVerification { } else { // the other start won, let's hand off startedVerification = false; // it is now as if they started - lastStep = + thisLastStep = lastStep = 'm.key.verification.request'; // we fake the last step method.dispose(); // in case anything got created already } @@ -296,7 +297,9 @@ class KeyVerification { await method.handlePayload(type, payload); break; } - lastStep = type; + if (lastStep == thisLastStep) { + lastStep = type; + } } catch (err, stacktrace) { print('[Key Verification] An error occured: ' + err.toString()); print(stacktrace); From 4154c7d0eb20322fafcc0e72e2d6c8926a01cf72 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 6 Jun 2020 13:47:37 +0200 Subject: [PATCH 37/64] format and some analyze --- lib/encryption/cross_signing.dart | 7 +-- lib/encryption/encryption.dart | 6 ++- lib/encryption/key_manager.dart | 13 +++-- lib/encryption/key_verification_manager.dart | 23 +++++---- lib/encryption/ssss.dart | 18 ++++--- lib/matrix_api/model/keys_query_response.dart | 48 +++++++++---------- lib/src/client.dart | 10 ++-- lib/src/utils/device_keys_list.dart | 18 ++++--- test/device_keys_list_test.dart | 9 +--- 9 files changed, 87 insertions(+), 65 deletions(-) diff --git a/lib/encryption/cross_signing.dart b/lib/encryption/cross_signing.dart index c3e809b..b525a7b 100644 --- a/lib/encryption/cross_signing.dart +++ b/lib/encryption/cross_signing.dart @@ -152,7 +152,8 @@ class CrossSigning { if (key is CrossSigningKey) { if (key.usage.contains('master')) { // okay, we'll sign our own master key - final signature = encryption.olmManager.signString(key.signingContent); + final signature = + encryption.olmManager.signString(key.signingContent); addSignature( key, client @@ -172,8 +173,8 @@ class CrossSigning { } } else if (key is CrossSigningKey && key.usage.contains('master')) { // we are signing someone elses master key - userSigningKey ??= - base64.decode(await encryption.ssss.getCached(USER_SIGNING_KEY) ?? ''); + userSigningKey ??= base64 + .decode(await encryption.ssss.getCached(USER_SIGNING_KEY) ?? ''); if (userSigningKey.isNotEmpty) { final signature = _sign(key.signingContent, userSigningKey); addSignature(key, client.userDeviceKeys[client.userID].userSigningKey, diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart index ef35840..bb76912 100644 --- a/lib/encryption/encryption.dart +++ b/lib/encryption/encryption.dart @@ -93,7 +93,11 @@ class Encryption { if (update.type == 'ephemeral') { return; } - if (update.eventType.startsWith('m.key.verification.') || (update.eventType == 'm.room.message' && (update.content['content']['msgtype'] is String) && update.content['content']['msgtype'].startsWith('m.key.verification.'))) { + if (update.eventType.startsWith('m.key.verification.') || + (update.eventType == 'm.room.message' && + (update.content['content']['msgtype'] is String) && + update.content['content']['msgtype'] + .startsWith('m.key.verification.'))) { // "just" key verification, no need to do this in sync unawaited(keyVerificationManager.handleEventUpdate(update)); } diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 7855de3..76f0a4e 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -321,7 +321,8 @@ class KeyManager { if (!(payload['rooms'] is Map)) { return; } - final privateKey = base64.decode(await encryption.ssss.getCached(MEGOLM_KEY)); + final privateKey = + base64.decode(await encryption.ssss.getCached(MEGOLM_KEY)); final decryption = olm.PkDecryption(); final info = await getRoomKeysInfo(); String backupPubKey; @@ -373,7 +374,9 @@ class KeyManager { if (decrypted != null) { decrypted['session_id'] = sessionId; decrypted['room_id'] = roomId; - setInboundGroupSession(roomId, sessionId, decrypted['sender_key'], decrypted, forwarded: true); + setInboundGroupSession( + roomId, sessionId, decrypted['sender_key'], decrypted, + forwarded: true); } } } @@ -403,7 +406,8 @@ class KeyManager { /// Request a certain key from another device Future request(Room room, String sessionId, String senderKey) async { // let's first check our online key backup store thingy... - var hadPreviously = getInboundGroupSession(room.id, sessionId, senderKey) != null; + var hadPreviously = + getInboundGroupSession(room.id, sessionId, senderKey) != null; try { await loadSingleKey(room.id, sessionId); } catch (err, stacktrace) { @@ -411,7 +415,8 @@ class KeyManager { print(err.toString()); print(stacktrace); } - if (!hadPreviously && getInboundGroupSession(room.id, sessionId, senderKey) != null) { + if (!hadPreviously && + getInboundGroupSession(room.id, sessionId, senderKey) != null) { return; // we managed to load the session from online backup, no need to care about it now } // while we just send the to-device event to '*', we still need to save the diff --git a/lib/encryption/key_verification_manager.dart b/lib/encryption/key_verification_manager.dart index c2accf5..ba80770 100644 --- a/lib/encryption/key_verification_manager.dart +++ b/lib/encryption/key_verification_manager.dart @@ -29,7 +29,7 @@ class KeyVerificationManager { final Map _requests = {}; Future cleanup() async { - Set entriesToDispose = {}; + final Set entriesToDispose = {}; for (final entry in _requests.entries) { var dispose = entry.value.canceled || entry.value.state == KeyVerificationState.done || @@ -55,7 +55,8 @@ class KeyVerificationManager { } Future handleToDeviceEvent(ToDeviceEvent event) async { - if (!event.type.startsWith('m.key.verification') || client.verificationMethods.isEmpty) { + if (!event.type.startsWith('m.key.verification') || + client.verificationMethods.isEmpty) { return; } // we have key verification going on! @@ -84,7 +85,9 @@ class KeyVerificationManager { final type = event['type'].startsWith('m.key.verification.') ? event['type'] : event['content']['msgtype']; - if (type == null || !type.startsWith('m.key.verification.') || client.verificationMethods.isEmpty) { + if (type == null || + !type.startsWith('m.key.verification.') || + client.verificationMethods.isEmpty) { return; } if (type == 'm.key.verification.request') { @@ -97,7 +100,7 @@ class KeyVerificationManager { if (_requests.containsKey(transactionId)) { final req = _requests[transactionId]; if (event['sender'] != client.userID) { - req.handlePayload(type, event['content'], event['event_id']); + await req.handlePayload(type, event['content'], event['event_id']); } else if (req.userId == client.userID && req.deviceId == null) { // okay, maybe another of our devices answered await req.handlePayload(type, event['content'], event['event_id']); @@ -108,12 +111,12 @@ class KeyVerificationManager { } } } else if (event['sender'] != client.userID) { - final room = - client.getRoomById(update.roomID) ?? Room(id: update.roomID, client: client); - final newKeyRequest = - KeyVerification(encryption: encryption, userId: event['sender'], room: room); - await newKeyRequest - .handlePayload(type, event['content'], event['event_id']); + final room = client.getRoomById(update.roomID) ?? + Room(id: update.roomID, client: client); + final newKeyRequest = KeyVerification( + encryption: encryption, userId: event['sender'], room: room); + await newKeyRequest.handlePayload( + type, event['content'], event['event_id']); if (newKeyRequest.state != KeyVerificationState.askAccept) { // something went wrong, let's just dispose the request newKeyRequest.dispose(); diff --git a/lib/encryption/ssss.dart b/lib/encryption/ssss.dart index 6785f3d..ae792f5 100644 --- a/lib/encryption/ssss.dart +++ b/lib/encryption/ssss.dart @@ -177,7 +177,10 @@ class SSSS { } // check if it is still valid final keys = keyIdsFromType(type); - if (keys.contains(ret.keyId) && client.accountData[type].content['encrypted'][ret.keyId]['ciphertext'] == ret.ciphertext) { + if (keys.contains(ret.keyId) && + client.accountData[type].content['encrypted'][ret.keyId] + ['ciphertext'] == + ret.ciphertext) { return ret.content; } return null; @@ -200,7 +203,8 @@ class SSSS { final decrypted = decryptAes(encryptInfo, key, type); if (CACHE_TYPES.contains(type) && client.database != null) { // cache the thing - await client.database.storeSSSSCache(client.id, type, keyId, enc['ciphertext'], decrypted); + await client.database + .storeSSSSCache(client.id, type, keyId, enc['ciphertext'], decrypted); } return decrypted; } @@ -224,7 +228,8 @@ class SSSS { ); if (CACHE_TYPES.contains(type) && client.database != null) { // cache the thing - await client.database.storeSSSSCache(client.id, type, keyId, encrypted.ciphertext, secret); + await client.database + .storeSSSSCache(client.id, type, keyId, encrypted.ciphertext, secret); } } @@ -352,9 +357,10 @@ class SSSS { if (client.database != null) { final keyId = keyIdFromType(request.type); if (keyId != null) { - final ciphertext = client.accountData[request.type].content['encrypted'][keyId]['ciphertext']; - await client.database - .storeSSSSCache(client.id, request.type, keyId, ciphertext, secret); + final ciphertext = client.accountData[request.type] + .content['encrypted'][keyId]['ciphertext']; + await client.database.storeSSSSCache( + client.id, request.type, keyId, ciphertext, secret); } } } diff --git a/lib/matrix_api/model/keys_query_response.dart b/lib/matrix_api/model/keys_query_response.dart index 3e4f2a1..4fef3f3 100644 --- a/lib/matrix_api/model/keys_query_response.dart +++ b/lib/matrix_api/model/keys_query_response.dart @@ -41,32 +41,32 @@ class KeysQueryResponse { ), ) : null; - masterKeys = json['master_keys'] != null ? - (json['master_keys'] as Map).map( - (k, v) => MapEntry( - k, - MatrixCrossSigningKey.fromJson(v), - ), - ) - : null; + masterKeys = json['master_keys'] != null + ? (json['master_keys'] as Map).map( + (k, v) => MapEntry( + k, + MatrixCrossSigningKey.fromJson(v), + ), + ) + : null; - selfSigningKeys = json['self_signing_keys'] != null ? - (json['self_signing_keys'] as Map).map( - (k, v) => MapEntry( - k, - MatrixCrossSigningKey.fromJson(v), - ), - ) - : null; + selfSigningKeys = json['self_signing_keys'] != null + ? (json['self_signing_keys'] as Map).map( + (k, v) => MapEntry( + k, + MatrixCrossSigningKey.fromJson(v), + ), + ) + : null; - userSigningKeys = json['user_signing_keys'] != null ? - (json['user_signing_keys'] as Map).map( - (k, v) => MapEntry( - k, - MatrixCrossSigningKey.fromJson(v), - ), - ) - : null; + userSigningKeys = json['user_signing_keys'] != null + ? (json['user_signing_keys'] as Map).map( + (k, v) => MapEntry( + k, + MatrixCrossSigningKey.fromJson(v), + ), + ) + : null; } Map toJson() { diff --git a/lib/src/client.dart b/lib/src/client.dart index 9102f55..6e324e8 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1160,7 +1160,8 @@ class Client { final deviceId = rawDeviceKeyEntry.key; // Set the new device key for this device - final entry = DeviceKeys.fromMatrixDeviceKeys(rawDeviceKeyEntry.value, this); + final entry = + DeviceKeys.fromMatrixDeviceKeys(rawDeviceKeyEntry.value, this); if (entry.isValid) { // is this a new key or the same one as an old one? // better store an update - the signatures might have changed! @@ -1231,7 +1232,8 @@ class Client { if (!userDeviceKeys.containsKey(userId)) { _userDeviceKeys[userId] = DeviceKeysList(userId); } - final oldKeys = Map.from(_userDeviceKeys[userId].crossSigningKeys); + final oldKeys = Map.from( + _userDeviceKeys[userId].crossSigningKeys); _userDeviceKeys[userId].crossSigningKeys = {}; // add the types we aren't handling atm back for (final oldEntry in oldKeys.entries) { @@ -1240,8 +1242,8 @@ class Client { oldEntry.value; } } - final entry = - CrossSigningKey.fromMatrixCrossSigningKey(crossSigningKeyListEntry.value, this); + final entry = CrossSigningKey.fromMatrixCrossSigningKey( + crossSigningKeyListEntry.value, this); if (entry.isValid) { final publicKey = entry.publicKey; if (!oldKeys.containsKey(publicKey) || diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 6f31e81..0297668 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -60,7 +60,8 @@ class DeviceKeysList { throw 'Unable to start new room'; } final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client); - final request = KeyVerification(encryption: client.encryption, room: room, userId: userId); + final request = KeyVerification( + encryption: client.encryption, room: room, userId: userId); await request.start(); // no need to add to the request client object. As we are doing a room // verification request that'll happen automatically once we know the transaction id @@ -224,7 +225,9 @@ abstract class SignedKey { Future setVerified(bool newVerified, [bool sign = true]) { _verified = newVerified; - if (sign && client.encryptionEnabled && client.encryption.crossSigning.signable([this])) { + if (sign && + client.encryptionEnabled && + client.encryption.crossSigning.signable([this])) { // sign the key! client.encryption.crossSigning.sign([this]); } @@ -266,13 +269,16 @@ class CrossSigningKey extends SignedKey { newBlocked, client.id, userId, publicKey); } - CrossSigningKey.fromMatrixCrossSigningKey(MatrixCrossSigningKey k, Client cl) { + CrossSigningKey.fromMatrixCrossSigningKey( + MatrixCrossSigningKey k, Client cl) { client = cl; content = Map.from(k.toJson()); userId = k.userId; identifier = k.publicKey; usage = content['usage'].cast(); - keys = content['keys'] != null ? Map.from(content['keys']) : null; + keys = content['keys'] != null + ? Map.from(content['keys']) + : null; signatures = content['signatures'] != null ? Map.from(content['signatures']) : null; @@ -397,8 +403,8 @@ class DeviceKeys extends SignedKey { } KeyVerification startVerification() { - final request = - KeyVerification(encryption: client.encryption, userId: userId, deviceId: deviceId); + final request = KeyVerification( + encryption: client.encryption, userId: userId, deviceId: deviceId); request.start(); client.encryption.keyVerificationManager.addRequest(request); diff --git a/test/device_keys_list_test.dart b/test/device_keys_list_test.dart index 54e3e1f..c1f3204 100644 --- a/test/device_keys_list_test.dart +++ b/test/device_keys_list_test.dart @@ -42,15 +42,10 @@ void main() { }, 'unsigned': {'device_display_name': "Alice's mobile phone"}, }; - var rawListJson = { - 'user_id': '@alice:example.com', - 'outdated': true, - 'device_keys': {'JLAFKJWSCS': rawJson}, - }; final key = DeviceKeys.fromJson(rawJson, null); - key.setVerified(false, false); - key.setBlocked(true); + await key.setVerified(false, false); + await key.setBlocked(true); expect(json.encode(key.toJson()), json.encode(rawJson)); expect(key.directVerified, false); expect(key.blocked, true); From e1679d59be9c277493fc6b3a80f8196a030faeb0 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 6 Jun 2020 14:28:18 +0200 Subject: [PATCH 38/64] better smoothen out keys --- lib/encryption.dart | 1 + lib/encryption/cross_signing.dart | 6 +- lib/encryption/ssss.dart | 6 +- lib/encryption/utils/key_verification.dart | 8 +- lib/matrix_api.dart | 3 +- lib/matrix_api/matrix_api.dart | 2 +- lib/matrix_api/model/keys_query_response.dart | 3 +- .../model/matrix_cross_signing_key.dart | 65 --------- ...trix_device_keys.dart => matrix_keys.dart} | 85 +++++++++--- lib/src/utils/device_keys_list.dart | 127 +++++------------- test/matrix_api_test.dart | 2 +- 11 files changed, 113 insertions(+), 195 deletions(-) delete mode 100644 lib/matrix_api/model/matrix_cross_signing_key.dart rename lib/matrix_api/model/{matrix_device_keys.dart => matrix_keys.dart} (58%) diff --git a/lib/encryption.dart b/lib/encryption.dart index ef4f347..2239ee2 100644 --- a/lib/encryption.dart +++ b/lib/encryption.dart @@ -20,4 +20,5 @@ library encryption; export './encryption/encryption.dart'; export './encryption/key_manager.dart'; +export './encryption/ssss.dart'; export './encryption/utils/key_verification.dart'; diff --git a/lib/encryption/cross_signing.dart b/lib/encryption/cross_signing.dart index b525a7b..164b183 100644 --- a/lib/encryption/cross_signing.dart +++ b/lib/encryption/cross_signing.dart @@ -100,7 +100,7 @@ class CrossSigning { ]); } - bool signable(List keys) { + bool signable(List keys) { for (final key in keys) { if (key is CrossSigningKey && key.usage.contains('master')) { return true; @@ -114,13 +114,13 @@ class CrossSigning { return false; } - Future sign(List keys) async { + Future sign(List keys) async { Uint8List selfSigningKey; Uint8List userSigningKey; final signatures = {}; var signedKey = false; final addSignature = - (SignedKey key, SignedKey signedWith, String signature) { + (SignableKey key, SignableKey signedWith, String signature) { if (key == null || signedWith == null || signature == null) { return; } diff --git a/lib/encryption/ssss.dart b/lib/encryption/ssss.dart index ae792f5..9349a14 100644 --- a/lib/encryption/ssss.dart +++ b/lib/encryption/ssss.dart @@ -221,11 +221,7 @@ class SSSS { 'mac': encrypted.mac, }; // store the thing in your account data - await client.jsonRequest( - type: RequestType.PUT, - action: '/client/r0/user/${client.userID}/account_data/${type}', - data: content, - ); + await client.api.setAccountData(client.userID, type, content); if (CACHE_TYPES.contains(type) && client.database != null) { // cache the thing await client.database diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart index 9c5b1ef..638c724 100644 --- a/lib/encryption/utils/key_verification.dart +++ b/lib/encryption/utils/key_verification.dart @@ -128,7 +128,7 @@ class KeyVerification { List possibleMethods; Map startPaylaod; String _nextAction; - List _verifiedDevices; + List _verifiedDevices; DateTime lastActivity; String lastStep; @@ -404,8 +404,8 @@ class KeyVerification { } Future verifyKeys(Map keys, - Future Function(String, SignedKey) verifier) async { - _verifiedDevices = []; + Future Function(String, SignableKey) verifier) async { + _verifiedDevices = []; if (!client.userDeviceKeys.containsKey(userId)) { await cancel('m.key_mismatch'); @@ -863,7 +863,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { mac[entry.key] = entry.value; } } - await request.verifyKeys(mac, (String mac, SignedKey key) async { + await request.verifyKeys(mac, (String mac, SignableKey key) async { return mac == _calculateMac(key.ed25519Key, baseInfo + 'ed25519:' + key.identifier); }); diff --git a/lib/matrix_api.dart b/lib/matrix_api.dart index b12702b..5832d4f 100644 --- a/lib/matrix_api.dart +++ b/lib/matrix_api.dart @@ -31,9 +31,8 @@ export 'package:famedlysdk/matrix_api/model/filter.dart'; export 'package:famedlysdk/matrix_api/model/keys_query_response.dart'; export 'package:famedlysdk/matrix_api/model/login_response.dart'; export 'package:famedlysdk/matrix_api/model/login_types.dart'; -export 'package:famedlysdk/matrix_api/model/matrix_cross_signing_key.dart'; -export 'package:famedlysdk/matrix_api/model/matrix_device_keys.dart'; export 'package:famedlysdk/matrix_api/model/matrix_exception.dart'; +export 'package:famedlysdk/matrix_api/model/matrix_keys.dart'; export 'package:famedlysdk/matrix_api/model/message_types.dart'; export 'package:famedlysdk/matrix_api/model/presence_content.dart'; export 'package:famedlysdk/matrix_api/model/notifications_query_response.dart'; diff --git a/lib/matrix_api/matrix_api.dart b/lib/matrix_api/matrix_api.dart index 70b2e2a..4c3ddcb 100644 --- a/lib/matrix_api/matrix_api.dart +++ b/lib/matrix_api/matrix_api.dart @@ -36,8 +36,8 @@ import 'package:mime_type/mime_type.dart'; import 'package:moor/moor.dart'; import 'model/device.dart'; -import 'model/matrix_device_keys.dart'; import 'model/matrix_event.dart'; +import 'model/matrix_keys.dart'; import 'model/event_context.dart'; import 'model/events_sync_update.dart'; import 'model/login_response.dart'; diff --git a/lib/matrix_api/model/keys_query_response.dart b/lib/matrix_api/model/keys_query_response.dart index 4fef3f3..57bc16e 100644 --- a/lib/matrix_api/model/keys_query_response.dart +++ b/lib/matrix_api/model/keys_query_response.dart @@ -16,8 +16,7 @@ * along with this program. If not, see . */ -import 'matrix_device_keys.dart'; -import 'matrix_cross_signing_key.dart'; +import 'matrix_keys.dart'; class KeysQueryResponse { Map failures; diff --git a/lib/matrix_api/model/matrix_cross_signing_key.dart b/lib/matrix_api/model/matrix_cross_signing_key.dart deleted file mode 100644 index 2a852a1..0000000 --- a/lib/matrix_api/model/matrix_cross_signing_key.dart +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Famedly Matrix SDK - * Copyright (C) 2020 Famedly GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -class MatrixCrossSigningKey { - String userId; - List usage; - Map keys; - Map> signatures; - Map unsigned; - String get publicKey => keys?.values?.first; - - MatrixCrossSigningKey( - this.userId, - this.usage, - this.keys, - this.signatures, { - this.unsigned, - }); - - // This object is used for signing so we need the raw json too - Map _json; - - MatrixCrossSigningKey.fromJson(Map json) { - _json = json; - userId = json['user_id']; - usage = List.from(json['usage']); - keys = Map.from(json['keys']); - signatures = Map>.from( - (json['signatures'] as Map) - .map((k, v) => MapEntry(k, Map.from(v)))); - unsigned = json['unsigned'] != null - ? Map.from(json['unsigned']) - : null; - } - - Map toJson() { - final data = _json ?? {}; - data['user_id'] = userId; - data['usage'] = usage; - data['keys'] = keys; - - if (signatures != null) { - data['signatures'] = signatures; - } - if (unsigned != null) { - data['unsigned'] = unsigned; - } - return data; - } -} diff --git a/lib/matrix_api/model/matrix_device_keys.dart b/lib/matrix_api/model/matrix_keys.dart similarity index 58% rename from lib/matrix_api/model/matrix_device_keys.dart rename to lib/matrix_api/model/matrix_keys.dart index 95a225b..05ffa28 100644 --- a/lib/matrix_api/model/matrix_device_keys.dart +++ b/lib/matrix_api/model/matrix_keys.dart @@ -16,38 +16,25 @@ * along with this program. If not, see . */ -class MatrixDeviceKeys { +class MatrixSignableKey { String userId; - String deviceId; - List algorithms; Map keys; Map> signatures; Map unsigned; - String get deviceDisplayName => - unsigned != null ? unsigned['device_display_name'] : null; + + MatrixSignableKey(this.userId, this.keys, this.signatures, {this.unsigned}); // This object is used for signing so we need the raw json too Map _json; - MatrixDeviceKeys( - this.userId, - this.deviceId, - this.algorithms, - this.keys, - this.signatures, { - this.unsigned, - }); - - MatrixDeviceKeys.fromJson(Map json) { + MatrixSignableKey.fromJson(Map json) { _json = json; userId = json['user_id']; - deviceId = json['device_id']; - algorithms = json['algorithms'].cast(); keys = Map.from(json['keys']); - signatures = Map>.from( + signatures = json['signatures'] is Map ? Map>.from( (json['signatures'] as Map) - .map((k, v) => MapEntry(k, Map.from(v)))); - unsigned = json['unsigned'] != null + .map((k, v) => MapEntry(k, Map.from(v)))) : null; + unsigned = json['unsigned'] is Map ? Map.from(json['unsigned']) : null; } @@ -55,8 +42,6 @@ class MatrixDeviceKeys { Map toJson() { final data = _json ?? {}; data['user_id'] = userId; - data['device_id'] = deviceId; - data['algorithms'] = algorithms; data['keys'] = keys; if (signatures != null) { @@ -68,3 +53,59 @@ class MatrixDeviceKeys { return data; } } + +class MatrixCrossSigningKey extends MatrixSignableKey { + List usage; + String get publicKey => keys?.values?.first; + + MatrixCrossSigningKey( + String userId, + this.usage, + Map keys, + Map> signatures, { + Map unsigned, + }) : super(userId, keys, signatures, unsigned: unsigned); + + @override + MatrixCrossSigningKey.fromJson(Map json) + : super.fromJson(json) { + usage = List.from(json['usage']); + } + + @override + Map toJson() { + final data = super.toJson(); + data['usage'] = usage; + return data; + } +} + +class MatrixDeviceKeys extends MatrixSignableKey { + String deviceId; + List algorithms; + String get deviceDisplayName => + unsigned != null ? unsigned['device_display_name'] : null; + + MatrixDeviceKeys( + String userId, + this.deviceId, + this.algorithms, + Map keys, + Map> signatures, { + Map unsigned, + }) : super(userId, keys, signatures, unsigned: unsigned); + + @override + MatrixDeviceKeys.fromJson(Map json) : super.fromJson(json) { + deviceId = json['device_id']; + algorithms = json['algorithms'].cast(); + } + + @override + Map toJson() { + final data = super.toJson(); + data['device_id'] = deviceId; + data['algorithms'] = algorithms; + return data; + } +} diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 0297668..a39fda2 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -21,7 +21,7 @@ class DeviceKeysList { Map deviceKeys = {}; Map crossSigningKeys = {}; - SignedKey getKey(String id) { + SignableKey getKey(String id) { if (deviceKeys.containsKey(id)) { return deviceKeys[id]; } @@ -98,13 +98,9 @@ class DeviceKeysList { DeviceKeysList(this.userId); } -abstract class SignedKey { +abstract class SignableKey extends MatrixSignableKey { Client client; - String userId; String identifier; - Map content; - Map keys; - Map signatures; Map validSignatures; bool _verified; bool blocked; @@ -120,8 +116,15 @@ abstract class SignedKey { bool get crossVerified => hasValidSignatureChain(); bool get signed => hasValidSignatureChain(verifiedOnly: false); + SignableKey.fromJson(Map json, Client cl) + : client = cl, + super.fromJson(json) { + _verified = false; + blocked = false; + } + String get signingContent { - final data = Map.from(content); + final data = Map.from(super.toJson()); // some old data might have the custom verified and blocked keys data.remove('verified'); data.remove('blocked'); @@ -166,7 +169,7 @@ abstract class SignedKey { continue; } final keyId = fullKeyId.substring('ed25519:'.length); - SignedKey key; + SignableKey key; if (client.userDeviceKeys[otherUserId].deviceKeys.containsKey(keyId)) { key = client.userDeviceKeys[otherUserId].deviceKeys[keyId]; } else if (client.userDeviceKeys[otherUserId].crossSigningKeys @@ -236,8 +239,9 @@ abstract class SignedKey { Future setBlocked(bool newBlocked); + @override Map toJson() { - final data = Map.from(content); + final data = Map.from(super.toJson()); // some old data may have the verified and blocked keys which are unneeded now data.remove('verified'); data.remove('blocked'); @@ -248,7 +252,7 @@ abstract class SignedKey { String toString() => json.encode(toJson()); } -class CrossSigningKey extends SignedKey { +class CrossSigningKey extends SignableKey { String get publicKey => identifier; List usage; @@ -269,59 +273,35 @@ class CrossSigningKey extends SignedKey { newBlocked, client.id, userId, publicKey); } - CrossSigningKey.fromMatrixCrossSigningKey( - MatrixCrossSigningKey k, Client cl) { - client = cl; - content = Map.from(k.toJson()); - userId = k.userId; + CrossSigningKey.fromMatrixCrossSigningKey(MatrixCrossSigningKey k, Client cl) + : super.fromJson(Map.from(k.toJson()), cl) { + final json = toJson(); identifier = k.publicKey; - usage = content['usage'].cast(); - keys = content['keys'] != null - ? Map.from(content['keys']) - : null; - signatures = content['signatures'] != null - ? Map.from(content['signatures']) - : null; - _verified = false; - blocked = false; + usage = json['usage'].cast(); } - CrossSigningKey.fromDb(DbUserCrossSigningKey dbEntry, Client cl) { - client = cl; - final json = Event.getMapFromPayload(dbEntry.content); - content = Map.from(json); - userId = dbEntry.userId; + CrossSigningKey.fromDb(DbUserCrossSigningKey dbEntry, Client cl) + : super.fromJson(Event.getMapFromPayload(dbEntry.content), cl) { + final json = toJson(); identifier = dbEntry.publicKey; usage = json['usage'].cast(); - keys = json['keys'] != null ? Map.from(json['keys']) : null; - signatures = json['signatures'] != null - ? Map.from(json['signatures']) - : null; _verified = dbEntry.verified; blocked = dbEntry.blocked; } - CrossSigningKey.fromJson(Map json, Client cl) { - client = cl; - content = Map.from(json); - userId = json['user_id']; + CrossSigningKey.fromJson(Map json, Client cl) + : super.fromJson(Map.from(json), cl) { + final json = toJson(); usage = json['usage'].cast(); - keys = json['keys'] != null ? Map.from(json['keys']) : null; - signatures = json['signatures'] != null - ? Map.from(json['signatures']) - : null; - _verified = json['verified'] ?? false; - blocked = json['blocked'] ?? false; - if (keys != null) { + if (keys != null && keys.isNotEmpty) { identifier = keys.values.first; } } } -class DeviceKeys extends SignedKey { +class DeviceKeys extends SignableKey { String get deviceId => identifier; List algorithms; - Map unsigned; String get curve25519Key => keys['curve25519:$deviceId']; @@ -346,60 +326,27 @@ class DeviceKeys extends SignedKey { ?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId); } - DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys k, Client cl) { - client = cl; - content = Map.from(k.toJson()); - userId = k.userId; + DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys k, Client cl) + : super.fromJson(Map.from(k.toJson()), cl) { + final json = toJson(); identifier = k.deviceId; - algorithms = content['algorithms'].cast(); - keys = content['keys'] != null - ? Map.from(content['keys']) - : null; - signatures = content['signatures'] != null - ? Map.from(content['signatures']) - : null; - unsigned = content['unsigned'] != null - ? Map.from(content['unsigned']) - : null; - _verified = false; - blocked = false; + algorithms = json['algorithms'].cast(); } - DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry, Client cl) { - client = cl; - final json = Event.getMapFromPayload(dbEntry.content); - content = Map.from(json); - userId = dbEntry.userId; + DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry, Client cl) + : super.fromJson(Event.getMapFromPayload(dbEntry.content), cl) { + final json = toJson(); identifier = dbEntry.deviceId; - algorithms = content['algorithms'].cast(); - keys = content['keys'] != null - ? Map.from(content['keys']) - : null; - signatures = content['signatures'] != null - ? Map.from(content['signatures']) - : null; - unsigned = json['unsigned'] != null - ? Map.from(json['unsigned']) - : null; + algorithms = json['algorithms'].cast(); _verified = dbEntry.verified; blocked = dbEntry.blocked; } - DeviceKeys.fromJson(Map json, Client cl) { - client = cl; - content = Map.from(json); - userId = json['user_id']; + DeviceKeys.fromJson(Map json, Client cl) + : super.fromJson(Map.from(json), cl) { + final json = toJson(); identifier = json['device_id']; algorithms = json['algorithms'].cast(); - keys = json['keys'] != null ? Map.from(json['keys']) : null; - signatures = json['signatures'] != null - ? Map.from(json['signatures']) - : null; - unsigned = json['unsigned'] != null - ? Map.from(json['unsigned']) - : null; - _verified = json['verified'] ?? false; - blocked = json['blocked'] ?? false; } KeyVerification startVerification() { diff --git a/test/matrix_api_test.dart b/test/matrix_api_test.dart index 0d3fa02..317bae5 100644 --- a/test/matrix_api_test.dart +++ b/test/matrix_api_test.dart @@ -18,7 +18,7 @@ import 'dart:typed_data'; import 'package:famedlysdk/matrix_api.dart'; -import 'package:famedlysdk/matrix_api/model/matrix_device_keys.dart'; +import 'package:famedlysdk/matrix_api/model/matrix_keys.dart'; import 'package:famedlysdk/matrix_api/model/filter.dart'; import 'package:famedlysdk/matrix_api/model/matrix_exception.dart'; import 'package:famedlysdk/matrix_api/model/presence_content.dart'; From b4e83caa89d87c0541e472ddb3813ffbc7ed5710 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 6 Jun 2020 15:17:05 +0200 Subject: [PATCH 39/64] requestify cross-signing endpoints --- lib/encryption/cross_signing.dart | 39 ++++--------- lib/matrix_api.dart | 1 + lib/matrix_api/matrix_api.dart | 50 +++++++++++++++++ lib/matrix_api/model/matrix_keys.dart | 24 ++++---- .../model/upload_key_signatures_response.dart | 55 +++++++++++++++++++ lib/src/utils/device_keys_list.dart | 8 ++- 6 files changed, 139 insertions(+), 38 deletions(-) create mode 100644 lib/matrix_api/model/upload_key_signatures_response.dart diff --git a/lib/encryption/cross_signing.dart b/lib/encryption/cross_signing.dart index 164b183..e4d8bc9 100644 --- a/lib/encryption/cross_signing.dart +++ b/lib/encryption/cross_signing.dart @@ -117,34 +117,23 @@ class CrossSigning { Future sign(List keys) async { Uint8List selfSigningKey; Uint8List userSigningKey; - final signatures = {}; - var signedKey = false; + final signedKeys = []; final addSignature = (SignableKey key, SignableKey signedWith, String signature) { if (key == null || signedWith == null || signature == null) { return; } - if (!signatures.containsKey(key.userId)) { - signatures[key.userId] = {}; + final signedKey = signedKeys.firstWhere( + (k) => k.userId == key.userId && k.identifier == key.identifier, + orElse: () => null) ?? + key.cloneForSigning(); + signedKey.signatures ??= >{}; + if (!signedKey.signatures.containsKey(signedWith.userId)) { + signedKey.signatures[signedWith.userId] = {}; } - if (!signatures[key.userId].containsKey(key.identifier)) { - signatures[key.userId][key.identifier] = - Map.from(key.toJson()); - // we don't need to send all old signatures, so let's just remove them - signatures[key.userId][key.identifier].remove('signatures'); - } - if (!signatures[key.userId][key.identifier].containsKey('signatures')) { - signatures[key.userId][key.identifier] - ['signatures'] = {}; - } - if (!signatures[key.userId][key.identifier]['signatures'] - .containsKey(signedWith.userId)) { - signatures[key.userId][key.identifier]['signatures'] - [signedWith.userId] = {}; - } - signatures[key.userId][key.identifier]['signatures'][signedWith.userId] + signedKey.signatures[signedWith.userId] ['ed25519:${signedWith.identifier}'] = signature; - signedKey = true; + signedKeys.add(signedKey); }; for (final key in keys) { if (key.userId == client.userID) { @@ -182,13 +171,9 @@ class CrossSigning { } } } - if (signedKey) { + if (signedKeys.isNotEmpty) { // post our new keys! - await client.jsonRequest( - type: RequestType.POST, - action: '/client/r0/keys/signatures/upload', - data: signatures, - ); + await client.api.uploadKeySignatures(signedKeys); } } diff --git a/lib/matrix_api.dart b/lib/matrix_api.dart index 5832d4f..e1a22d5 100644 --- a/lib/matrix_api.dart +++ b/lib/matrix_api.dart @@ -58,6 +58,7 @@ export 'package:famedlysdk/matrix_api/model/third_party_location.dart'; export 'package:famedlysdk/matrix_api/model/third_party_user.dart'; export 'package:famedlysdk/matrix_api/model/timeline_history_response.dart'; export 'package:famedlysdk/matrix_api/model/turn_server_credentials.dart'; +export 'package:famedlysdk/matrix_api/model/upload_key_signatures_response.dart'; export 'package:famedlysdk/matrix_api/model/user_search_result.dart'; export 'package:famedlysdk/matrix_api/model/well_known_informations.dart'; export 'package:famedlysdk/matrix_api/model/who_is_info.dart'; diff --git a/lib/matrix_api/matrix_api.dart b/lib/matrix_api/matrix_api.dart index 4c3ddcb..60155fd 100644 --- a/lib/matrix_api/matrix_api.dart +++ b/lib/matrix_api/matrix_api.dart @@ -54,6 +54,7 @@ import 'model/tag.dart'; import 'model/third_party_identifier.dart'; import 'model/third_party_user.dart'; import 'model/turn_server_credentials.dart'; +import 'model/upload_key_signatures_response.dart'; import 'model/well_known_informations.dart'; import 'model/who_is_info.dart'; @@ -1503,6 +1504,55 @@ class MatrixApi { return DeviceListsUpdate.fromJson(response); } + /// Uploads your own cross-signing keys. + /// https://12682-24998719-gh.circle-artifacts.com/0/scripts/gen/client_server/unstable.html#post-matrix-client-r0-keys-device-signing-upload + Future uploadDeviceSigningKeys({ + MatrixCrossSigningKey masterKey, + MatrixCrossSigningKey selfSigningKey, + MatrixCrossSigningKey userSigningKey, + }) async { + await request( + RequestType.POST, + '/client/r0/keys/device_signing/upload', + data: { + 'master_key': masterKey.toJson(), + 'self_signing_key': selfSigningKey.toJson(), + 'user_signing_key': userSigningKey.toJson(), + }, + ); + } + + /// Uploads new signatures of keys + /// https://12682-24998719-gh.circle-artifacts.com/0/scripts/gen/client_server/unstable.html#post-matrix-client-r0-keys-signatures-upload + Future uploadKeySignatures( + List keys) async { + final payload = {}; + for (final key in keys) { + if (key.identifier == null || + key.signatures == null || + key.signatures.isEmpty) { + continue; + } + if (!payload.containsKey(key.userId)) { + payload[key.userId] = {}; + } + if (payload[key.userId].containsKey(key.identifier)) { + // we need to merge signature objects + payload[key.userId][key.identifier]['signatures'] + .addAll(key.signatures); + } else { + // we can just add signatures + payload[key.userId][key.identifier] = key.toJson(); + } + } + final response = await request( + RequestType.POST, + '/client/r0/keys/signatures/upload', + data: payload, + ); + return UploadKeySignaturesResponse.fromJson(response); + } + /// Gets all currently active pushers for the authenticated user. /// https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-pushers Future> requestPushers() async { diff --git a/lib/matrix_api/model/matrix_keys.dart b/lib/matrix_api/model/matrix_keys.dart index 05ffa28..c0f0038 100644 --- a/lib/matrix_api/model/matrix_keys.dart +++ b/lib/matrix_api/model/matrix_keys.dart @@ -18,11 +18,13 @@ class MatrixSignableKey { String userId; + String identifier; Map keys; Map> signatures; Map unsigned; - MatrixSignableKey(this.userId, this.keys, this.signatures, {this.unsigned}); + MatrixSignableKey(this.userId, this.identifier, this.keys, this.signatures, + {this.unsigned}); // This object is used for signing so we need the raw json too Map _json; @@ -31,9 +33,10 @@ class MatrixSignableKey { _json = json; userId = json['user_id']; keys = Map.from(json['keys']); - signatures = json['signatures'] is Map ? Map>.from( - (json['signatures'] as Map) - .map((k, v) => MapEntry(k, Map.from(v)))) : null; + signatures = json['signatures'] is Map + ? Map>.from((json['signatures'] as Map) + .map((k, v) => MapEntry(k, Map.from(v)))) + : null; unsigned = json['unsigned'] is Map ? Map.from(json['unsigned']) : null; @@ -56,7 +59,7 @@ class MatrixSignableKey { class MatrixCrossSigningKey extends MatrixSignableKey { List usage; - String get publicKey => keys?.values?.first; + String get publicKey => identifier; MatrixCrossSigningKey( String userId, @@ -64,12 +67,13 @@ class MatrixCrossSigningKey extends MatrixSignableKey { Map keys, Map> signatures, { Map unsigned, - }) : super(userId, keys, signatures, unsigned: unsigned); + }) : super(userId, keys?.values?.first, keys, signatures, unsigned: unsigned); @override MatrixCrossSigningKey.fromJson(Map json) : super.fromJson(json) { usage = List.from(json['usage']); + identifier = keys?.values?.first; } @override @@ -81,23 +85,23 @@ class MatrixCrossSigningKey extends MatrixSignableKey { } class MatrixDeviceKeys extends MatrixSignableKey { - String deviceId; + String get deviceId => identifier; List algorithms; String get deviceDisplayName => unsigned != null ? unsigned['device_display_name'] : null; MatrixDeviceKeys( String userId, - this.deviceId, + String deviceId, this.algorithms, Map keys, Map> signatures, { Map unsigned, - }) : super(userId, keys, signatures, unsigned: unsigned); + }) : super(userId, deviceId, keys, signatures, unsigned: unsigned); @override MatrixDeviceKeys.fromJson(Map json) : super.fromJson(json) { - deviceId = json['device_id']; + identifier = json['device_id']; algorithms = json['algorithms'].cast(); } diff --git a/lib/matrix_api/model/upload_key_signatures_response.dart b/lib/matrix_api/model/upload_key_signatures_response.dart new file mode 100644 index 0000000..325aa5e --- /dev/null +++ b/lib/matrix_api/model/upload_key_signatures_response.dart @@ -0,0 +1,55 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'matrix_exception.dart'; + +class UploadKeySignaturesResponse { + Map> failures; + + UploadKeySignaturesResponse.fromJson(Map json) { + failures = json['failures'] != null + ? (json['failures'] as Map).map( + (k, v) => MapEntry( + k, + (v as Map).map((k, v) => MapEntry( + k, + MatrixException.fromJson(v), + )), + ), + ) + : null; + } + + Map toJson() { + final data = {}; + if (failures != null) { + data['failures'] = failures.map( + (k, v) => MapEntry( + k, + v.map( + (k, v) => MapEntry( + k, + v.raw, + ), + ), + ), + ); + } + return data; + } +} diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index a39fda2..27ee9f7 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -100,7 +100,6 @@ class DeviceKeysList { abstract class SignableKey extends MatrixSignableKey { Client client; - String identifier; Map validSignatures; bool _verified; bool blocked; @@ -123,6 +122,13 @@ abstract class SignableKey extends MatrixSignableKey { blocked = false; } + MatrixSignableKey cloneForSigning() { + final newKey = + MatrixSignableKey.fromJson(Map.from(toJson())); + newKey.signatures.clear(); + return newKey; + } + String get signingContent { final data = Map.from(super.toJson()); // some old data might have the custom verified and blocked keys From 45232be3a05812a29116db6705d547d6a958c257 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 6 Jun 2020 15:19:44 +0200 Subject: [PATCH 40/64] simplify key signing a tad --- lib/encryption/cross_signing.dart | 10 ++-------- lib/src/utils/device_keys_list.dart | 1 + 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/encryption/cross_signing.dart b/lib/encryption/cross_signing.dart index e4d8bc9..6154088 100644 --- a/lib/encryption/cross_signing.dart +++ b/lib/encryption/cross_signing.dart @@ -123,14 +123,8 @@ class CrossSigning { if (key == null || signedWith == null || signature == null) { return; } - final signedKey = signedKeys.firstWhere( - (k) => k.userId == key.userId && k.identifier == key.identifier, - orElse: () => null) ?? - key.cloneForSigning(); - signedKey.signatures ??= >{}; - if (!signedKey.signatures.containsKey(signedWith.userId)) { - signedKey.signatures[signedWith.userId] = {}; - } + final signedKey = key.cloneForSigning(); + signedKey.signatures[signedWith.userId] = {}; signedKey.signatures[signedWith.userId] ['ed25519:${signedWith.identifier}'] = signature; signedKeys.add(signedKey); diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 27ee9f7..2159d51 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -125,6 +125,7 @@ abstract class SignableKey extends MatrixSignableKey { MatrixSignableKey cloneForSigning() { final newKey = MatrixSignableKey.fromJson(Map.from(toJson())); + newKey.signatures ??= >{}; newKey.signatures.clear(); return newKey; } From 51584ad748056a5ea646214eb05a8297f56ddb8f Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 6 Jun 2020 15:43:18 +0200 Subject: [PATCH 41/64] fix signature upload --- lib/src/utils/device_keys_list.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 2159d51..52af46a 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -125,6 +125,7 @@ abstract class SignableKey extends MatrixSignableKey { MatrixSignableKey cloneForSigning() { final newKey = MatrixSignableKey.fromJson(Map.from(toJson())); + newKey.identifier = identifier; newKey.signatures ??= >{}; newKey.signatures.clear(); return newKey; From 1fdd0a7db870743606413ba9bc475676ebc7d457 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 6 Jun 2020 15:48:57 +0200 Subject: [PATCH 42/64] aslo clear ssss cache when clearing a user id --- lib/src/database/database.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index 77f7c57..0d4ec7e 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -455,6 +455,7 @@ class Database extends _$Database { await (delete(inboundGroupSessions) ..where((r) => r.clientId.equals(clientId))) .go(); + await (delete(ssssCache)..where((r) => r.clientId.equals(clientId))).go(); await (delete(olmSessions)..where((r) => r.clientId.equals(clientId))).go(); await (delete(userCrossSigningKeys) ..where((r) => r.clientId.equals(clientId))) From 2de03bc0e86760d8323804c83a533ff60a9fd927 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sun, 7 Jun 2020 15:09:11 +0200 Subject: [PATCH 43/64] properly reply to room key requests --- lib/encryption/key_manager.dart | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 76f0a4e..3409b94 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -457,22 +457,27 @@ class KeyManager { } if (event.content['action'] == 'request') { // we are *receiving* a request + print('[KeyManager] Received key sharing request...'); if (!event.content.containsKey('body')) { + print('[KeyManager] No body, doing nothing'); return; // no body } if (!client.userDeviceKeys.containsKey(event.sender) || !client.userDeviceKeys[event.sender].deviceKeys .containsKey(event.content['requesting_device_id'])) { + print('[KeyManager] Device not found, doing nothing'); return; // device not found } final device = client.userDeviceKeys[event.sender] .deviceKeys[event.content['requesting_device_id']]; if (device.userId == client.userID && device.deviceId == client.deviceID) { + print('[KeyManager] Request is by ourself, ignoring'); return; // ignore requests by ourself } final room = client.getRoomById(event.content['body']['room_id']); if (room == null) { + print('[KeyManager] Unknown room, ignoring'); return; // unknown room } final sessionId = event.content['body']['session_id']; @@ -480,6 +485,7 @@ class KeyManager { // okay, let's see if we have this session at all if ((await loadInboundGroupSession(room.id, sessionId, senderKey)) == null) { + print('[KeyManager] Unknown session, ignoring'); return; // we don't have this session anyways } final request = KeyManagerKeyShareRequest( @@ -490,6 +496,7 @@ class KeyManager { senderKey: senderKey, ); if (incomingShareRequests.containsKey(request.requestId)) { + print('[KeyManager] Already processed this request, ignoring'); return; // we don't want to process one and the same request multiple times } incomingShareRequests[request.requestId] = request; @@ -498,9 +505,11 @@ class KeyManager { if (device.userId == client.userID && device.verified && !device.blocked) { + print('[KeyManager] All checks out, forwarding key...'); // alright, we can forward the key await roomKeyRequest.forwardKey(); } else { + print('[KeyManager] Asking client, if the key should be forwarded'); client.onRoomKeyRequest .add(roomKeyRequest); // let the client handle this } @@ -637,6 +646,20 @@ class RoomKeyRequest extends ToDeviceEvent { var message = session.content; message['forwarding_curve25519_key_chain'] = forwardedKeys; + message['sender_key'] = request.senderKey; + message['sender_claimed_ed25519_key'] = forwardedKeys.isEmpty ? keyManager.encryption.fingerprintKey : null; + if (message['sender_claimed_ed25519_key'] == null) { + for (final value in keyManager.client.userDeviceKeys.values) { + for (final key in value.deviceKeys.values) { + if (key.curve25519Key == forwardedKeys.first) { + message['sender_claimed_ed25519_key'] = key.ed25519Key; + } + } + if (message['sender_claimed_ed25519_key'] != null) { + break; + } + } + } message['session_key'] = session.inboundGroupSession .export_session(session.inboundGroupSession.first_known_index()); // send the actual reply of the key back to the requester From 2a6a19e2b00fdb9da2b80726200693d1d5ae2865 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 12 Jun 2020 16:17:00 +0200 Subject: [PATCH 44/64] Request-ify room key store stuff --- lib/encryption/key_manager.dart | 73 ++++-------- lib/matrix_api.dart | 2 + lib/matrix_api/matrix_api.dart | 146 +++++++++++++++++++++++ lib/matrix_api/model/room_keys_info.dart | 107 +++++++++++++++++ lib/matrix_api/model/room_keys_keys.dart | 85 +++++++++++++ pubspec.lock | 18 +-- pubspec.yaml | 14 +-- 7 files changed, 382 insertions(+), 63 deletions(-) create mode 100644 lib/matrix_api/model/room_keys_info.dart create mode 100644 lib/matrix_api/model/room_keys_keys.dart diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 99314e8..bd87275 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -43,9 +43,12 @@ class KeyManager { encryption.ssss.setValidator(MEGOLM_KEY, (String secret) async { final keyObj = olm.PkDecryption(); try { - final info = await getRoomKeysInfo(); + final info = await client.api.getRoomKeysBackup(); + if (!(info.authData is RoomKeysAuthDataV1Curve25519AesSha2)) { + return false; + } return keyObj.init_with_private_key(base64.decode(secret)) == - info['auth_data']['public_key']; + (info.authData as RoomKeysAuthDataV1Curve25519AesSha2).publicKey; } catch (_) { return false; } finally { @@ -313,13 +316,6 @@ class KeyManager { _outboundGroupSessions[roomId] = sess; } - Future> getRoomKeysInfo() async { - return await client.jsonRequest( - type: RequestType.GET, - action: '/client/r0/room_keys/version', - ); - } - Future isCached() async { if (!enabled) { return false; @@ -327,50 +323,32 @@ class KeyManager { return (await encryption.ssss.getCached(MEGOLM_KEY)) != null; } - Future loadFromResponse(Map payload) async { + Future loadFromResponse(RoomKeys keys) async { if (!(await isCached())) { return; } - if (!(payload['rooms'] is Map)) { - return; - } final privateKey = base64.decode(await encryption.ssss.getCached(MEGOLM_KEY)); final decryption = olm.PkDecryption(); - final info = await getRoomKeysInfo(); + final info = await client.api.getRoomKeysBackup(); String backupPubKey; try { backupPubKey = decryption.init_with_private_key(privateKey); if (backupPubKey == null || - !info.containsKey('auth_data') || - !(info['auth_data'] is Map) || - info['auth_data']['public_key'] != backupPubKey) { + !(info.authData is RoomKeysAuthDataV1Curve25519AesSha2) || + (info.authData as RoomKeysAuthDataV1Curve25519AesSha2).publicKey != backupPubKey) { return; } - for (final roomEntries in payload['rooms'].entries) { - final roomId = roomEntries.key; - if (!(roomEntries.value is Map) || - !(roomEntries.value['sessions'] is Map)) { - continue; - } - for (final sessionEntries in roomEntries.value['sessions'].entries) { - final sessionId = sessionEntries.key; - final rawEncryptedSession = sessionEntries.value; - if (!(rawEncryptedSession is Map)) { - continue; - } - final firstMessageIndex = - rawEncryptedSession['first_message_index'] is int - ? rawEncryptedSession['first_message_index'] - : null; - final forwardedCount = rawEncryptedSession['forwarded_count'] is int - ? rawEncryptedSession['forwarded_count'] - : null; - final isVerified = rawEncryptedSession['is_verified'] is bool - ? rawEncryptedSession['is_verified'] - : null; - final sessionData = rawEncryptedSession['session_data']; + for (final roomEntry in keys.rooms.entries) { + final roomId = roomEntry.key; + for (final sessionEntry in roomEntry.value.sessions.entries) { + final sessionId = sessionEntry.key; + final session = sessionEntry.value; + final firstMessageIndex = session.firstMessageIndex; + final forwardedCount = session.forwardedCount; + final isVerified = session.isVerified; + final sessionData = session.sessionData; if (firstMessageIndex == null || forwardedCount == null || isVerified == null || @@ -399,21 +377,18 @@ class KeyManager { } Future loadSingleKey(String roomId, String sessionId) async { - final info = await getRoomKeysInfo(); - final ret = await client.jsonRequest( - type: RequestType.GET, - action: - '/client/r0/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${info['version']}', - ); - await loadFromResponse({ + final info = await client.api.getRoomKeysBackup(); + final ret = await client.api.getRoomKeysSingleKey(roomId, sessionId, info.version); + final keys = RoomKeys.fromJson({ 'rooms': { roomId: { 'sessions': { - sessionId: ret, + sessionId: ret.toJson(), }, }, }, }); + await loadFromResponse(keys); } /// Request a certain key from another device @@ -422,6 +397,7 @@ class KeyManager { var hadPreviously = getInboundGroupSession(room.id, sessionId, senderKey) != null; try { + print('FETCHING FROM KEY STORE...'); await loadSingleKey(room.id, sessionId); } catch (err, stacktrace) { print('++++++++++++++++++'); @@ -430,6 +406,7 @@ class KeyManager { } if (!hadPreviously && getInboundGroupSession(room.id, sessionId, senderKey) != null) { + print('GOT FROM KEY STORE, SUCCESS!!!!!'); return; // we managed to load the session from online backup, no need to care about it now } // while we just send the to-device event to '*', we still need to save the diff --git a/lib/matrix_api.dart b/lib/matrix_api.dart index e1a22d5..be120a3 100644 --- a/lib/matrix_api.dart +++ b/lib/matrix_api.dart @@ -46,6 +46,8 @@ export 'package:famedlysdk/matrix_api/model/push_rule_set.dart'; export 'package:famedlysdk/matrix_api/model/pusher.dart'; export 'package:famedlysdk/matrix_api/model/request_token_response.dart'; export 'package:famedlysdk/matrix_api/model/room_alias_informations.dart'; +export 'package:famedlysdk/matrix_api/model/room_keys_info.dart'; +export 'package:famedlysdk/matrix_api/model/room_keys_keys.dart'; export 'package:famedlysdk/matrix_api/model/room_summary.dart'; export 'package:famedlysdk/matrix_api/model/server_capabilities.dart'; export 'package:famedlysdk/matrix_api/model/stripped_state_event.dart'; diff --git a/lib/matrix_api/matrix_api.dart b/lib/matrix_api/matrix_api.dart index 6f3bb81..081d779 100644 --- a/lib/matrix_api/matrix_api.dart +++ b/lib/matrix_api/matrix_api.dart @@ -49,6 +49,8 @@ import 'model/public_rooms_response.dart'; import 'model/push_rule_set.dart'; import 'model/pusher.dart'; import 'model/room_alias_informations.dart'; +import 'model/room_keys_info.dart'; +import 'model/room_keys_keys.dart'; import 'model/supported_protocol.dart'; import 'model/tag.dart'; import 'model/third_party_identifier.dart'; @@ -2036,4 +2038,148 @@ class MatrixApi { ); return; } + + /// Create room keys backup + /// https://matrix.org/docs/spec/client_server/unstable#post-matrix-client-r0-room-keys-version + Future createRoomKeysBackup(RoomKeysAlgorithmType algorithm, RoomKeysAuthData authData) async { + final ret = await request( + RequestType.POST, + '/client/unstable/room_keys/version', + data: { + 'algorithm': algorithm.algorithmString, + 'auth_data': authData.toJson(), + }, + ); + return ret['version']; + } + + /// Gets a room key backup + /// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-version + Future getRoomKeysBackup([String version]) async { + var url = '/client/unstable/room_keys/version'; + if (version != null) { + url += '/${Uri.encodeComponent(version)}'; + } + final ret = await request( + RequestType.GET, + url, + ); + return RoomKeysVersionResponse.fromJson(ret); + } + + /// Updates a room key backup + /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-version-version + Future updateRoomKeysBackup(String version, RoomKeysAlgorithmType algorithm, RoomKeysAuthData authData) async { + await request( + RequestType.PUT, + '/client/unstable/room_keys/version/${Uri.encodeComponent(version)}', + data: { + 'algorithm': algorithm.algorithmString, + 'auth_data': authData.toJson, + 'version': version, + }, + ); + } + + /// Deletes a room key backup + /// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-version-version + Future deleteRoomKeysBackup(String version) async { + await request( + RequestType.DELETE, + '/client/unstable/room_keys/version/${Uri.encodeComponent(version)}', + ); + } + + /// Stores a single room key + /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys-roomid-sessionid + Future storeRoomKeysSingleKey(String roomId, String sessionId, String version, RoomKeysSingleKey session) async { + final ret = await request( + RequestType.PUT, + '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}', + data: session.toJson(), + ); + return RoomKeysUpdateResponse.fromJson(ret); + } + + /// Gets a single room key + /// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-keys-roomid-sessionid + Future getRoomKeysSingleKey(String roomId, String sessionId, String version) async { + final ret = await request( + RequestType.GET, + '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}', + ); + return RoomKeysSingleKey.fromJson(ret); + } + + /// Deletes a single room key + /// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid-sessionid + Future deleteRoomKeysSingleKey(String roomId, String sessionId, String version) async { + final ret = await request( + RequestType.DELETE, + '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}', + ); + return RoomKeysUpdateResponse.fromJson(ret); + } + + /// Stores room keys for a room + /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys-roomid + Future storeRoomKeysRoom(String roomId, String version, RoomKeysRoom keys) async { + final ret = await request( + RequestType.PUT, + '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}?version=${Uri.encodeComponent(version)}', + data: keys.toJson(), + ); + return RoomKeysUpdateResponse.fromJson(ret); + } + + /// Gets room keys for a room + /// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-keys-roomid + Future getRoomKeysRoom(String roomId, String version) async { + final ret = await request( + RequestType.GET, + '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}?version=${Uri.encodeComponent(version)}', + ); + return RoomKeysRoom.fromJson(ret); + } + + /// Deletes room ekys for a room + /// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid + Future deleteRoomKeysRoom(String roomId, String version) async { + final ret = await request( + RequestType.DELETE, + '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}?version=${Uri.encodeComponent(version)}', + ); + return RoomKeysUpdateResponse.fromJson(ret); + } + + /// Store multiple room keys + /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys + Future storeRoomKeys(String version, RoomKeys keys) async { + final ret = await request( + RequestType.PUT, + '/client/unstable/room_keys/keys?version=${Uri.encodeComponent(version)}', + data: keys.toJson(), + ); + return RoomKeysUpdateResponse.fromJson(ret); + } + + /// get all room keys + /// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-keys + Future getRoomKeys(String version) async { + final ret = await request( + RequestType.GET, + '/client/unstable/room_keys/keys?version=${Uri.encodeComponent(version)}', + ); + return RoomKeys.fromJson(ret); + } + + /// delete all room keys + /// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys + Future deleteRoomKeys(String version) async { + final ret = await request( + RequestType.DELETE, + '/client/unstable/room_keys/keys?version=${Uri.encodeComponent(version)}', + ); + return RoomKeysUpdateResponse.fromJson(ret); + } } diff --git a/lib/matrix_api/model/room_keys_info.dart b/lib/matrix_api/model/room_keys_info.dart new file mode 100644 index 0000000..7ed4513 --- /dev/null +++ b/lib/matrix_api/model/room_keys_info.dart @@ -0,0 +1,107 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +enum RoomKeysAlgorithmType { v1Curve25519AesSha2 } + +extension RoomKeysAlgorithmTypeExtension on RoomKeysAlgorithmType { + String get algorithmString { + switch (this) { + case RoomKeysAlgorithmType.v1Curve25519AesSha2: + return 'm.megolm_backup.v1.curve25519-aes-sha2'; + default: + return null; + } + } + + static RoomKeysAlgorithmType fromAlgorithmString(String s) { + switch (s) { + case 'm.megolm_backup.v1.curve25519-aes-sha2': + return RoomKeysAlgorithmType.v1Curve25519AesSha2; + default: + return null; + } + } +} + +abstract class RoomKeysAuthData { + // This object is used for signing so we need the raw json too + Map _json; + + RoomKeysAuthData.fromJson(Map json) { + _json = json; + } + + Map toJson() { + return _json; + } +} + +class RoomKeysAuthDataV1Curve25519AesSha2 extends RoomKeysAuthData { + String publicKey; + Map> signatures; + + RoomKeysAuthDataV1Curve25519AesSha2.fromJson(Map json) : super.fromJson(json) { + publicKey = json['public_key']; + signatures = json['signatures'] is Map + ? Map>.from((json['signatures'] as Map) + .map((k, v) => MapEntry(k, Map.from(v)))) + : null; + } + + @override + Map toJson() { + final data = super.toJson(); + data['public_key'] = publicKey; + if (signatures != null) { + data['signatures'] = signatures; + } + return data; + } +} + +class RoomKeysVersionResponse { + RoomKeysAlgorithmType algorithm; + RoomKeysAuthData authData; + int count; + String etag; + String version; + + RoomKeysVersionResponse.fromJson(Map json) { + algorithm = RoomKeysAlgorithmTypeExtension.fromAlgorithmString(json['algorithm']); + switch (algorithm) { + case RoomKeysAlgorithmType.v1Curve25519AesSha2: + authData = RoomKeysAuthDataV1Curve25519AesSha2.fromJson(json['auth_data']); + break; + default: + authData = null; + } + count = json['count']; + etag = json['etag'].toString(); // synapse replies an int but docs say string? + version = json['version']; + } + + Map toJson() { + final data = {}; + data['algorithm'] = algorithm?.algorithmString; + data['auth_data'] = authData?.toJson(); + data['count'] = count; + data['etag'] = etag; + data['version'] = version; + return data; + } +} diff --git a/lib/matrix_api/model/room_keys_keys.dart b/lib/matrix_api/model/room_keys_keys.dart new file mode 100644 index 0000000..7170f0e --- /dev/null +++ b/lib/matrix_api/model/room_keys_keys.dart @@ -0,0 +1,85 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +class RoomKeysSingleKey { + int firstMessageIndex; + int forwardedCount; + bool isVerified; + Map sessionData; + + RoomKeysSingleKey.fromJson(Map json) { + firstMessageIndex = json['first_message_index']; + forwardedCount = json['forwarded_count']; + isVerified = json['is_verified']; + sessionData = json['session_data']; + } + + Map toJson() { + final data = {}; + data['first_message_index'] = firstMessageIndex; + data['forwarded_count'] = forwardedCount; + data['is_verified'] = isVerified; + data['session_data'] = sessionData; + return data; + } +} + +class RoomKeysRoom { + Map sessions; + + RoomKeysRoom.fromJson(Map json) { + sessions = (json['sessions'] as Map).map((k, v) => MapEntry(k, RoomKeysSingleKey.fromJson(v))); + } + + Map toJson() { + final data = {}; + data['sessions'] = sessions.map((k, v) => MapEntry(k, v.toJson())); + return data; + } +} + +class RoomKeys { + Map rooms; + + RoomKeys.fromJson(Map json) { + rooms = (json['rooms'] as Map).map((k, v) => MapEntry(k, RoomKeysRoom.fromJson(v))); + } + + Map toJson() { + final data = {}; + data['rooms'] = rooms.map((k, v) => MapEntry(k, v.toJson())); + return data; + } +} + +class RoomKeysUpdateResponse { + String etag; + int count; + + RoomKeysUpdateResponse.fromJson(Map json) { + etag = json['etag']; // synapse replies an int but docs say string? + count = json['count']; + } + + Map toJson() { + final data = {}; + data['etag'] = etag; + data['count'] = count; + return data; + } +} diff --git a/pubspec.lock b/pubspec.lock index 232c25e..5c8e154 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -333,9 +333,11 @@ packages: matrix_file_e2ee: dependency: "direct main" description: - path: "/home/sorunome/repos/famedly/matrix_file_e2ee" - relative: false - source: path + path: "." + ref: "1.x.y" + resolved-ref: "32edeff765369a7a77a0822f4b19302ca24a017b" + url: "https://gitlab.com/famedly/libraries/matrix_file_e2ee.git" + source: git version: "1.0.3" meta: dependency: transitive @@ -410,10 +412,12 @@ packages: olm: dependency: "direct main" description: - path: "/home/sorunome/repos/famedly/dart-olm" - relative: false - source: path - version: "1.1.1" + path: "." + ref: "1.x.y" + resolved-ref: "8e4fcccff7a2d4d0bd5142964db092bf45061905" + url: "https://gitlab.com/famedly/libraries/dart-olm.git" + source: git + version: "1.2.0" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c37e405..324dcd4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,16 +22,14 @@ dependencies: password_hash: ^2.0.0 olm: - path: /home/sorunome/repos/famedly/dart-olm -# git: -# url: https://gitlab.com/famedly/libraries/dart-olm.git -# ref: 0c612a525511652a7760126b058de8c924fe8900 + git: + url: https://gitlab.com/famedly/libraries/dart-olm.git + ref: 1.x.y matrix_file_e2ee: - path: /home/sorunome/repos/famedly/matrix_file_e2ee -# git: -# url: https://gitlab.com/famedly/libraries/matrix_file_e2ee.git -# ref: 1.x.y + git: + url: https://gitlab.com/famedly/libraries/matrix_file_e2ee.git + ref: 1.x.y dev_dependencies: test: ^1.0.0 From 3825f7292fd9064a860da0226e9bd35507e65f79 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 12 Jun 2020 16:17:28 +0200 Subject: [PATCH 45/64] format --- lib/encryption/key_manager.dart | 6 ++++-- lib/matrix_api/matrix_api.dart | 24 ++++++++++++++++-------- lib/matrix_api/model/room_keys_info.dart | 12 ++++++++---- lib/matrix_api/model/room_keys_keys.dart | 6 ++++-- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index bd87275..32ab983 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -337,7 +337,8 @@ class KeyManager { if (backupPubKey == null || !(info.authData is RoomKeysAuthDataV1Curve25519AesSha2) || - (info.authData as RoomKeysAuthDataV1Curve25519AesSha2).publicKey != backupPubKey) { + (info.authData as RoomKeysAuthDataV1Curve25519AesSha2).publicKey != + backupPubKey) { return; } for (final roomEntry in keys.rooms.entries) { @@ -378,7 +379,8 @@ class KeyManager { Future loadSingleKey(String roomId, String sessionId) async { final info = await client.api.getRoomKeysBackup(); - final ret = await client.api.getRoomKeysSingleKey(roomId, sessionId, info.version); + final ret = + await client.api.getRoomKeysSingleKey(roomId, sessionId, info.version); final keys = RoomKeys.fromJson({ 'rooms': { roomId: { diff --git a/lib/matrix_api/matrix_api.dart b/lib/matrix_api/matrix_api.dart index 081d779..3699f07 100644 --- a/lib/matrix_api/matrix_api.dart +++ b/lib/matrix_api/matrix_api.dart @@ -2041,7 +2041,8 @@ class MatrixApi { /// Create room keys backup /// https://matrix.org/docs/spec/client_server/unstable#post-matrix-client-r0-room-keys-version - Future createRoomKeysBackup(RoomKeysAlgorithmType algorithm, RoomKeysAuthData authData) async { + Future createRoomKeysBackup( + RoomKeysAlgorithmType algorithm, RoomKeysAuthData authData) async { final ret = await request( RequestType.POST, '/client/unstable/room_keys/version', @@ -2069,7 +2070,8 @@ class MatrixApi { /// Updates a room key backup /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-version-version - Future updateRoomKeysBackup(String version, RoomKeysAlgorithmType algorithm, RoomKeysAuthData authData) async { + Future updateRoomKeysBackup(String version, + RoomKeysAlgorithmType algorithm, RoomKeysAuthData authData) async { await request( RequestType.PUT, '/client/unstable/room_keys/version/${Uri.encodeComponent(version)}', @@ -2092,7 +2094,8 @@ class MatrixApi { /// Stores a single room key /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys-roomid-sessionid - Future storeRoomKeysSingleKey(String roomId, String sessionId, String version, RoomKeysSingleKey session) async { + Future storeRoomKeysSingleKey(String roomId, + String sessionId, String version, RoomKeysSingleKey session) async { final ret = await request( RequestType.PUT, '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}', @@ -2103,7 +2106,8 @@ class MatrixApi { /// Gets a single room key /// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-keys-roomid-sessionid - Future getRoomKeysSingleKey(String roomId, String sessionId, String version) async { + Future getRoomKeysSingleKey( + String roomId, String sessionId, String version) async { final ret = await request( RequestType.GET, '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}', @@ -2113,7 +2117,8 @@ class MatrixApi { /// Deletes a single room key /// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid-sessionid - Future deleteRoomKeysSingleKey(String roomId, String sessionId, String version) async { + Future deleteRoomKeysSingleKey( + String roomId, String sessionId, String version) async { final ret = await request( RequestType.DELETE, '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}', @@ -2123,7 +2128,8 @@ class MatrixApi { /// Stores room keys for a room /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys-roomid - Future storeRoomKeysRoom(String roomId, String version, RoomKeysRoom keys) async { + Future storeRoomKeysRoom( + String roomId, String version, RoomKeysRoom keys) async { final ret = await request( RequestType.PUT, '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}?version=${Uri.encodeComponent(version)}', @@ -2144,7 +2150,8 @@ class MatrixApi { /// Deletes room ekys for a room /// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid - Future deleteRoomKeysRoom(String roomId, String version) async { + Future deleteRoomKeysRoom( + String roomId, String version) async { final ret = await request( RequestType.DELETE, '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}?version=${Uri.encodeComponent(version)}', @@ -2154,7 +2161,8 @@ class MatrixApi { /// Store multiple room keys /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys - Future storeRoomKeys(String version, RoomKeys keys) async { + Future storeRoomKeys( + String version, RoomKeys keys) async { final ret = await request( RequestType.PUT, '/client/unstable/room_keys/keys?version=${Uri.encodeComponent(version)}', diff --git a/lib/matrix_api/model/room_keys_info.dart b/lib/matrix_api/model/room_keys_info.dart index 7ed4513..42e2398 100644 --- a/lib/matrix_api/model/room_keys_info.dart +++ b/lib/matrix_api/model/room_keys_info.dart @@ -55,7 +55,8 @@ class RoomKeysAuthDataV1Curve25519AesSha2 extends RoomKeysAuthData { String publicKey; Map> signatures; - RoomKeysAuthDataV1Curve25519AesSha2.fromJson(Map json) : super.fromJson(json) { + RoomKeysAuthDataV1Curve25519AesSha2.fromJson(Map json) + : super.fromJson(json) { publicKey = json['public_key']; signatures = json['signatures'] is Map ? Map>.from((json['signatures'] as Map) @@ -82,16 +83,19 @@ class RoomKeysVersionResponse { String version; RoomKeysVersionResponse.fromJson(Map json) { - algorithm = RoomKeysAlgorithmTypeExtension.fromAlgorithmString(json['algorithm']); + algorithm = + RoomKeysAlgorithmTypeExtension.fromAlgorithmString(json['algorithm']); switch (algorithm) { case RoomKeysAlgorithmType.v1Curve25519AesSha2: - authData = RoomKeysAuthDataV1Curve25519AesSha2.fromJson(json['auth_data']); + authData = + RoomKeysAuthDataV1Curve25519AesSha2.fromJson(json['auth_data']); break; default: authData = null; } count = json['count']; - etag = json['etag'].toString(); // synapse replies an int but docs say string? + etag = + json['etag'].toString(); // synapse replies an int but docs say string? version = json['version']; } diff --git a/lib/matrix_api/model/room_keys_keys.dart b/lib/matrix_api/model/room_keys_keys.dart index 7170f0e..3b2c88e 100644 --- a/lib/matrix_api/model/room_keys_keys.dart +++ b/lib/matrix_api/model/room_keys_keys.dart @@ -43,7 +43,8 @@ class RoomKeysRoom { Map sessions; RoomKeysRoom.fromJson(Map json) { - sessions = (json['sessions'] as Map).map((k, v) => MapEntry(k, RoomKeysSingleKey.fromJson(v))); + sessions = (json['sessions'] as Map) + .map((k, v) => MapEntry(k, RoomKeysSingleKey.fromJson(v))); } Map toJson() { @@ -57,7 +58,8 @@ class RoomKeys { Map rooms; RoomKeys.fromJson(Map json) { - rooms = (json['rooms'] as Map).map((k, v) => MapEntry(k, RoomKeysRoom.fromJson(v))); + rooms = (json['rooms'] as Map) + .map((k, v) => MapEntry(k, RoomKeysRoom.fromJson(v))); } Map toJson() { From 6a36bb2d01d3061f78097394b5f3a5347be67abe Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 12 Jun 2020 16:25:26 +0200 Subject: [PATCH 46/64] fix pipeline --- lib/src/utils/device_keys_list.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 52af46a..c32f713 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -158,6 +158,9 @@ abstract class SignableKey extends MatrixSignableKey { } bool hasValidSignatureChain({bool verifiedOnly = true, Set visited}) { + if (!client.encryptionEnabled) { + return false; + } visited ??= {}; final setKey = '${userId};${identifier}'; if (visited.contains(setKey)) { From aed1cf1270a833eb6091dfe739ba6c5fa75bfb43 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 12 Jun 2020 17:15:26 +0200 Subject: [PATCH 47/64] handle ssss cache fetching better --- lib/encryption/key_manager.dart | 10 +++++---- lib/encryption/utils/key_verification.dart | 25 ++++++++++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 32ab983..e17faf9 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -47,8 +47,12 @@ class KeyManager { if (!(info.authData is RoomKeysAuthDataV1Curve25519AesSha2)) { return false; } - return keyObj.init_with_private_key(base64.decode(secret)) == - (info.authData as RoomKeysAuthDataV1Curve25519AesSha2).publicKey; + if (keyObj.init_with_private_key(base64.decode(secret)) == + (info.authData as RoomKeysAuthDataV1Curve25519AesSha2).publicKey) { + _requestedSessionIds.clear(); + return true; + } + return false; } catch (_) { return false; } finally { @@ -399,7 +403,6 @@ class KeyManager { var hadPreviously = getInboundGroupSession(room.id, sessionId, senderKey) != null; try { - print('FETCHING FROM KEY STORE...'); await loadSingleKey(room.id, sessionId); } catch (err, stacktrace) { print('++++++++++++++++++'); @@ -408,7 +411,6 @@ class KeyManager { } if (!hadPreviously && getInboundGroupSession(room.id, sessionId, senderKey) != null) { - print('GOT FROM KEY STORE, SUCCESS!!!!!'); return; // we managed to load the session from online backup, no need to care about it now } // while we just send the to-device event to '*', we still need to save the diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart index 638c724..dacc4e5 100644 --- a/lib/encryption/utils/key_verification.dart +++ b/lib/encryption/utils/key_verification.dart @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import 'dart:async'; import 'dart:typed_data'; import 'package:canonical_json/canonical_json.dart'; import 'package:pedantic/pedantic.dart'; @@ -403,6 +404,27 @@ class KeyVerification { return []; } + Future maybeRequestSSSSSecrets([int i = 0]) async { + final requestInterval = [10, 60]; + print('Attempting to request ssss secrets...'); + if ((!encryption.crossSigning.enabled || + (encryption.crossSigning.enabled && + (await encryption.crossSigning.isCached()))) && + (!encryption.keyManager.enabled || + (encryption.keyManager.enabled && + (await encryption.keyManager.isCached())))) { + // no need to request cache, we already have it + print('Not needed, we already have them'); + return; + } + unawaited(encryption.ssss.maybeRequestAll( + _verifiedDevices.whereType().toList())); + if (requestInterval.length >= i) { + return; + } + Timer(Duration(seconds: requestInterval[i]), () => maybeRequestSSSSSecrets(i + 1)); + } + Future verifyKeys(Map keys, Future Function(String, SignableKey) verifier) async { _verifiedDevices = []; @@ -437,8 +459,7 @@ class KeyVerification { if (verifiedMasterKey && userId == client.userID) { // it was our own master key, let's request the cross signing keys // we do it in the background, thus no await needed here - unawaited(encryption.ssss - .maybeRequestAll(_verifiedDevices.whereType().toList())); + unawaited(maybeRequestSSSSSecrets()); } await send('m.key.verification.done', {}); From 221d6c275be456807350ab57bead3f6145a7723a Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 12 Jun 2020 17:32:35 +0200 Subject: [PATCH 48/64] fix timer stuffs --- lib/encryption/utils/key_verification.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart index dacc4e5..1a376e9 100644 --- a/lib/encryption/utils/key_verification.dart +++ b/lib/encryption/utils/key_verification.dart @@ -406,7 +406,6 @@ class KeyVerification { Future maybeRequestSSSSSecrets([int i = 0]) async { final requestInterval = [10, 60]; - print('Attempting to request ssss secrets...'); if ((!encryption.crossSigning.enabled || (encryption.crossSigning.enabled && (await encryption.crossSigning.isCached()))) && @@ -414,12 +413,11 @@ class KeyVerification { (encryption.keyManager.enabled && (await encryption.keyManager.isCached())))) { // no need to request cache, we already have it - print('Not needed, we already have them'); return; } unawaited(encryption.ssss.maybeRequestAll( _verifiedDevices.whereType().toList())); - if (requestInterval.length >= i) { + if (requestInterval.length <= i) { return; } Timer(Duration(seconds: requestInterval[i]), () => maybeRequestSSSSSecrets(i + 1)); From 34619c065b0bbfe02098650d67f06858e0d0be3e Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 12 Jun 2020 17:40:08 +0200 Subject: [PATCH 49/64] format --- lib/encryption/utils/key_verification.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart index 1a376e9..90a2479 100644 --- a/lib/encryption/utils/key_verification.dart +++ b/lib/encryption/utils/key_verification.dart @@ -415,12 +415,13 @@ class KeyVerification { // no need to request cache, we already have it return; } - unawaited(encryption.ssss.maybeRequestAll( - _verifiedDevices.whereType().toList())); + unawaited(encryption.ssss + .maybeRequestAll(_verifiedDevices.whereType().toList())); if (requestInterval.length <= i) { return; } - Timer(Duration(seconds: requestInterval[i]), () => maybeRequestSSSSSecrets(i + 1)); + Timer(Duration(seconds: requestInterval[i]), + () => maybeRequestSSSSSecrets(i + 1)); } Future verifyKeys(Map keys, From 7803dc4b936dbf36843c361217a84e81535eefde Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 13 Jun 2020 10:56:39 +0200 Subject: [PATCH 50/64] add more tests --- lib/encryption/key_manager.dart | 3 +- test/client_test.dart | 4 +- test/encryption/ssss_test.dart | 207 ++++++++++++++++++++++++++++++++ test/fake_client.dart | 3 + test/fake_matrix_api.dart | 117 ++++++++++++++++++ 5 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 test/encryption/ssss_test.dart diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index e17faf9..a284d19 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -405,8 +405,7 @@ class KeyManager { try { await loadSingleKey(room.id, sessionId); } catch (err, stacktrace) { - print('++++++++++++++++++'); - print(err.toString()); + print('[KeyManager] Failed to access online key backup: ' + err.toString()); print(stacktrace); } if (!hadPreviously && diff --git a/test/client_test.dart b/test/client_test.dart index 8c93b6d..f48a2e3 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -127,7 +127,7 @@ void main() { } expect(sync.nextBatch == matrix.prevBatch, true); - expect(matrix.accountData.length, 3); + expect(matrix.accountData.length, 9); expect(matrix.getDirectChatFromUserId('@bob:example.com'), '!726s6s6q:example.com'); expect(matrix.rooms[1].directChatMatrixID, '@bob:example.com'); @@ -157,7 +157,7 @@ void main() { expect(matrix.presences['@alice:example.com'].presence.presence, PresenceType.online); expect(presenceCounter, 1); - expect(accountDataCounter, 3); + expect(accountDataCounter, 9); await Future.delayed(Duration(milliseconds: 50)); expect(matrix.userDeviceKeys.length, 4); expect(matrix.userDeviceKeys['@alice:example.com'].outdated, false); diff --git a/test/encryption/ssss_test.dart b/test/encryption/ssss_test.dart new file mode 100644 index 0000000..55d0d8f --- /dev/null +++ b/test/encryption/ssss_test.dart @@ -0,0 +1,207 @@ +/* + * Ansible inventory script used at Famedly GmbH for managing many hosts + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:typed_data'; +import 'dart:convert'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/matrix_api.dart'; +import 'package:famedlysdk/encryption.dart'; +import 'package:test/test.dart'; +import 'package:encrypt/encrypt.dart'; +import 'package:olm/olm.dart' as olm; + +import '../fake_client.dart'; +import '../fake_matrix_api.dart'; + +void main() { + group('SSSS', () { + var olmEnabled = true; + try { + olm.init(); + olm.Account(); + } catch (_) { + olmEnabled = false; + print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + } + print('[LibOlm] Enabled: $olmEnabled'); + + if (!olmEnabled) return; + + Client client; + + test('setupClient', () async { + client = await getClient(); + }); + + test('basic things', () async { + expect(client.encryption.ssss.defaultKeyId, '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'); + }); + + test('encrypt / decrypt', () { + final signing = olm.PkSigning(); + final key = Uint8List.fromList(SecureRandom(32).bytes); + + final enc = SSSS.encryptAes('secret foxies', key, 'name'); + final dec = SSSS.decryptAes(enc, key, 'name'); + expect(dec, 'secret foxies'); + }); + + test('store', () async { + final handle = client.encryption.ssss.open(); + var failed = false; + try { + handle.unlock(passphrase: 'invalid'); + } catch (_) { + failed = true; + } + expect(failed, true); + expect(handle.isUnlocked, false); + failed = false; + try { + handle.unlock(recoveryKey: 'invalid'); + } catch (_) { + failed = true; + } + expect(failed, true); + expect(handle.isUnlocked, false); + handle.unlock(passphrase: SSSS_PASSPHRASE); + handle.unlock(recoveryKey: SSSS_KEY); + expect(handle.isUnlocked, true); + FakeMatrixApi.calledEndpoints.clear(); + await handle.store('best animal', 'foxies'); + // alright, since we don't properly sync we will manually have to update + // account_data for this test + final content = FakeMatrixApi.calledEndpoints['/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal'].first; + client.accountData['best animal'] = BasicEvent.fromJson({ + 'type': 'best animal', + 'content': json.decode(content), + }); + expect(await handle.getStored('best animal'), 'foxies'); + }); + + test('cache', () async { + final handle = client.encryption.ssss.open('m.cross_signing.self_signing'); + handle.unlock(recoveryKey: SSSS_KEY); + expect((await client.encryption.ssss.getCached('m.cross_signing.self_signing')) != null, false); + expect((await client.encryption.ssss.getCached('m.cross_signing.user_signing')) != null, false); + await handle.getStored('m.cross_signing.self_signing'); + expect((await client.encryption.ssss.getCached('m.cross_signing.self_signing')) != null, true); + await handle.maybeCacheAll(); + expect((await client.encryption.ssss.getCached('m.cross_signing.user_signing')) != null, true); + expect((await client.encryption.ssss.getCached('m.megolm_backup.v1')) != null, true); + }); + + test('make share requests', () async { + final key = client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(true); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.request('some.type', [key]); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), true); + }); + + test('answer to share requests', () async { + var event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), true); + + // now test some fail scenarios + + // not by us + event = ToDeviceEvent( + sender: '@someotheruser:example.org', + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + + // secret not cached + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.unknown.secret', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + + // is a cancelation + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request_cancellation', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + + // device not verified + final key = client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(false); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + key.setDirectVerified(true); + }); + +// test('fail', () { +// expect(true, false); +// }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); + }); + }); +} diff --git a/test/fake_client.dart b/test/fake_client.dart index 9fb3837..af2c39a 100644 --- a/test/fake_client.dart +++ b/test/fake_client.dart @@ -21,6 +21,9 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'fake_matrix_api.dart'; import 'fake_database.dart'; +const SSSS_PASSPHRASE = 'nae7ahDiequ7ohniufah3ieS2je1thohX4xeeka7aixohsho9O'; +const SSSS_KEY = 'EsT9 RzbW VhPW yqNp cC7j ViiW 5TZB LuY4 ryyv 9guN Ysmr WDPH'; + // key @test:fakeServer.notExisting const pickledOlmAccount = 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw'; diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 4e3eb08..e53e7a8 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -516,6 +516,71 @@ class FakeMatrixApi extends MockClient { }, 'type': 'm.direct' }, + { + 'type': 'm.secret_storage.default_key', + 'content': {'key': '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'} + }, + { + 'type': 'm.secret_storage.key.0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3', + 'content': { + 'algorithm': 'm.secret_storage.v1.aes-hmac-sha2', + 'passphrase': { + 'algorithm': 'm.pbkdf2', + 'iterations': 500000, + 'salt': 'F4jJ80mr0Fc8mRwU9JgA3lQDyjPuZXQL' + }, + 'iv': 'HjbTgIoQH2pI7jQo19NUzA==', + 'mac': 'QbJjQzDnAggU0cM4RBnDxw2XyarRGjdahcKukP9xVlk=' + } + }, + { + 'type': 'm.cross_signing.master', + 'content': { + 'encrypted': { + '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { + 'iv': 'eIb2IITxtmcq+1TrT8D5eQ==', + 'ciphertext': 'lWRTPo5qxf4LAVwVPzGHOyMcP181n7bb9/B0lvkLDC2Oy4DvAL0eLx2x3bY=', + 'mac': 'Ynx89tIxPkx0o6ljMgxszww17JOgB4tg4etmNnMC9XI=' + } + } + } + }, + { + 'type': 'm.cross_signing.self_signing', + 'content': { + 'encrypted': { + '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { + 'iv': 'YqU2XIjYulYZl+bkZtGgVw==', + 'ciphertext': 'kM2TSoy/jR/4d357ZoRPbpPypxQl6XRLo3FsEXz+f7vIOp82GeRp28RYb3k=', + 'mac': 'F+DZa5tAFmWsYSryw5EuEpzTmmABRab4GETkM85bGGo=' + } + } + } + }, + { + 'type': 'm.cross_signing.user_signing', + 'content': { + 'encrypted': { + '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { + 'iv': 'D7AM3LXFu7ZlyGOkR+OeqQ==', + 'ciphertext': 'bYA2+OMgsO6QB1E31aY+ESAWrT0fUBTXqajy4qmL7bVDSZY4Uj64EXNbHuA=', + 'mac': 'j2UtyPo/UBSoiaQCWfzCiRZXp3IRt0ZZujuXgUMjnw4=' + } + } + } + }, + { + 'type': 'm.megolm_backup.v1', + 'content': { + 'encrypted': { + '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { + 'iv': 'cL/0MJZaiEd3fNU+I9oJrw==', + 'ciphertext': 'WL73Pzdk5wZdaaSpaeRH0uZYKcxkuV8IS6Qa2FEfA1+vMeRLuHcWlXbMX0w=', + 'mac': '+xozp909S6oDX8KRV8D8ZFVRyh7eEYQpPP76f+DOsnw=' + } + } + } + } ] }, 'to_device': { @@ -1461,6 +1526,13 @@ class FakeMatrixApi extends MockClient { 'event_format': 'client', 'event_fields': ['type', 'content', 'sender'] }, + '/client/unstable/room_keys/version': (var req) => { + 'algorithm': 'm.megolm_backup.v1.curve25519-aes-sha2', + 'auth_data': {'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM'}, + 'count': 0, + 'etag': '0', + 'version': '5', + }, }, 'POST': { '/client/r0/delete_devices': (var req) => {}, @@ -1673,6 +1745,19 @@ class FakeMatrixApi extends MockClient { }, 'signatures': {}, }, + 'OTHERDEVICE': { + 'user_id': '@test:fakeServer.notExisting', + 'device_id': 'OTHERDEVICE', + 'algorithms': [ + 'm.olm.v1.curve25519-aes-sha2', + 'm.megolm.v1.aes-sha2' + ], + 'keys': { + 'curve25519:OTHERDEVICE': 'blah', + 'ed25519:OTHERDEVICE': 'blah' + }, + 'signatures': {}, + }, }, '@othertest:fakeServer.notExisting': { 'FOXDEVICE': { @@ -1692,6 +1777,36 @@ class FakeMatrixApi extends MockClient { }, }, }, + 'master_keys': { + '@test:fakeServer.notExisting': { + 'user_id': '@test:fakeServer.notExisting', + 'usage': ['master'], + 'keys': { + 'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8': '82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8', + }, + 'signatures': {}, + }, + }, + 'self_signing_keys': { + '@test:fakeServer.notExisting': { + 'user_id': '@test:fakeServer.notExisting', + 'usage': ['self_signing'], + 'keys': { + 'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY': 'F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY', + }, + 'signatures': {}, + }, + }, + 'user_signing_keys': { + '@test:fakeServer.notExisting': { + 'user_id': '@test:fakeServer.notExisting', + 'usage': ['user_signing'], + 'keys': { + 'ed25519:0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g': '0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g', + }, + 'signatures': {}, + }, + }, }, '/client/r0/register': (var req) => { 'user_id': '@testuser:example.com', @@ -1783,6 +1898,8 @@ class FakeMatrixApi extends MockClient { (var req) => {}, '/client/r0/user/%40alice%3Aexample.com/account_data/test.account.data': (var req) => {}, + '/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal': + (var req) => {}, '/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data': (var req) => {}, '/client/r0/profile/%40alice%3Aexample.com/displayname': (var reqI) => {}, From e0251eda5554df50dd4b75bab1040906190e6f0b Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 13 Jun 2020 18:10:24 +0200 Subject: [PATCH 51/64] finish ssss tests --- lib/encryption/key_manager.dart | 3 +- lib/src/database/database.g.dart | 9 + lib/src/database/database.moor | 1 + test/encryption/ssss_test.dart | 487 +++++++++++++++++++++---------- test/fake_matrix_api.dart | 35 ++- 5 files changed, 373 insertions(+), 162 deletions(-) diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index a284d19..eeeb08d 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -405,7 +405,8 @@ class KeyManager { try { await loadSingleKey(room.id, sessionId); } catch (err, stacktrace) { - print('[KeyManager] Failed to access online key backup: ' + err.toString()); + print( + '[KeyManager] Failed to access online key backup: ' + err.toString()); print(stacktrace); } if (!hadPreviously && diff --git a/lib/src/database/database.g.dart b/lib/src/database/database.g.dart index 0c50118..52bf988 100644 --- a/lib/src/database/database.g.dart +++ b/lib/src/database/database.g.dart @@ -5850,6 +5850,15 @@ abstract class _$Database extends GeneratedDatabase { readsFrom: {ssssCache}).map(_rowToDbSSSSCache); } + Future clearSSSSCache(int client_id) { + return customUpdate( + 'DELETE FROM ssss_cache WHERE client_id = :client_id', + variables: [Variable.withInt(client_id)], + updates: {ssssCache}, + updateKind: UpdateKind.delete, + ); + } + Future insertClient( String name, String homeserver_url, diff --git a/lib/src/database/database.moor b/lib/src/database/database.moor index 68a0ee5..abe513d 100644 --- a/lib/src/database/database.moor +++ b/lib/src/database/database.moor @@ -198,6 +198,7 @@ storeUserCrossSigningKey: INSERT OR REPLACE INTO user_cross_signing_keys (client removeUserCrossSigningKey: DELETE FROM user_cross_signing_keys WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; storeSSSSCache: INSERT OR REPLACE INTO ssss_cache (client_id, type, key_id, ciphertext, content) VALUES (:client_id, :type, :key_id, :ciphertext, :content); dbGetSSSSCache: SELECT * FROM ssss_cache WHERE client_id = :client_id AND type = :type; +clearSSSSCache: DELETE FROM ssss_cache WHERE client_id = :client_id; insertClient: INSERT INTO clients (name, homeserver_url, token, user_id, device_id, device_name, prev_batch, olm_account) VALUES (:name, :homeserver_url, :token, :user_id, :device_id, :device_name, :prev_batch, :olm_account); ensureRoomExists: INSERT OR IGNORE INTO rooms (client_id, room_id, membership) VALUES (:client_id, :room_id, :membership); setRoomPrevBatch: UPDATE rooms SET prev_batch = :prev_batch WHERE client_id = :client_id AND room_id = :room_id; diff --git a/test/encryption/ssss_test.dart b/test/encryption/ssss_test.dart index 55d0d8f..a0d5b94 100644 --- a/test/encryption/ssss_test.dart +++ b/test/encryption/ssss_test.dart @@ -32,176 +32,367 @@ import '../fake_matrix_api.dart'; void main() { group('SSSS', () { var olmEnabled = true; + try { + olm.init(); + olm.Account(); + } catch (_) { + olmEnabled = false; + print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + } + print('[LibOlm] Enabled: $olmEnabled'); + + if (!olmEnabled) return; + + Client client; + + test('setupClient', () async { + client = await getClient(); + }); + + test('basic things', () async { + expect(client.encryption.ssss.defaultKeyId, + '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'); + }); + + test('encrypt / decrypt', () { + final key = Uint8List.fromList(SecureRandom(32).bytes); + + final enc = SSSS.encryptAes('secret foxies', key, 'name'); + final dec = SSSS.decryptAes(enc, key, 'name'); + expect(dec, 'secret foxies'); + }); + + test('store', () async { + final handle = client.encryption.ssss.open(); + var failed = false; try { - olm.init(); - olm.Account(); + handle.unlock(passphrase: 'invalid'); } catch (_) { - olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + failed = true; } - print('[LibOlm] Enabled: $olmEnabled'); - - if (!olmEnabled) return; - - Client client; - - test('setupClient', () async { - client = await getClient(); + expect(failed, true); + expect(handle.isUnlocked, false); + failed = false; + try { + handle.unlock(recoveryKey: 'invalid'); + } catch (_) { + failed = true; + } + expect(failed, true); + expect(handle.isUnlocked, false); + handle.unlock(passphrase: SSSS_PASSPHRASE); + handle.unlock(recoveryKey: SSSS_KEY); + expect(handle.isUnlocked, true); + FakeMatrixApi.calledEndpoints.clear(); + await handle.store('best animal', 'foxies'); + // alright, since we don't properly sync we will manually have to update + // account_data for this test + final content = FakeMatrixApi + .calledEndpoints[ + '/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal'] + .first; + client.accountData['best animal'] = BasicEvent.fromJson({ + 'type': 'best animal', + 'content': json.decode(content), }); + expect(await handle.getStored('best animal'), 'foxies'); + }); - test('basic things', () async { - expect(client.encryption.ssss.defaultKeyId, '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'); - }); + test('cache', () async { + final handle = + client.encryption.ssss.open('m.cross_signing.self_signing'); + handle.unlock(recoveryKey: SSSS_KEY); + expect( + (await client.encryption.ssss + .getCached('m.cross_signing.self_signing')) != + null, + false); + expect( + (await client.encryption.ssss + .getCached('m.cross_signing.user_signing')) != + null, + false); + await handle.getStored('m.cross_signing.self_signing'); + expect( + (await client.encryption.ssss + .getCached('m.cross_signing.self_signing')) != + null, + true); + await handle.maybeCacheAll(); + expect( + (await client.encryption.ssss + .getCached('m.cross_signing.user_signing')) != + null, + true); + expect( + (await client.encryption.ssss.getCached('m.megolm_backup.v1')) != + null, + true); + }); - test('encrypt / decrypt', () { - final signing = olm.PkSigning(); - final key = Uint8List.fromList(SecureRandom(32).bytes); - - final enc = SSSS.encryptAes('secret foxies', key, 'name'); - final dec = SSSS.decryptAes(enc, key, 'name'); - expect(dec, 'secret foxies'); - }); + test('make share requests', () async { + final key = + client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(true); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.request('some.type', [key]); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + true); + }); - test('store', () async { - final handle = client.encryption.ssss.open(); - var failed = false; - try { - handle.unlock(passphrase: 'invalid'); - } catch (_) { - failed = true; - } - expect(failed, true); - expect(handle.isUnlocked, false); - failed = false; - try { - handle.unlock(recoveryKey: 'invalid'); - } catch (_) { - failed = true; - } - expect(failed, true); - expect(handle.isUnlocked, false); - handle.unlock(passphrase: SSSS_PASSPHRASE); - handle.unlock(recoveryKey: SSSS_KEY); - expect(handle.isUnlocked, true); - FakeMatrixApi.calledEndpoints.clear(); - await handle.store('best animal', 'foxies'); - // alright, since we don't properly sync we will manually have to update - // account_data for this test - final content = FakeMatrixApi.calledEndpoints['/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal'].first; - client.accountData['best animal'] = BasicEvent.fromJson({ - 'type': 'best animal', - 'content': json.decode(content), - }); - expect(await handle.getStored('best animal'), 'foxies'); - }); + test('answer to share requests', () async { + var event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + true); - test('cache', () async { - final handle = client.encryption.ssss.open('m.cross_signing.self_signing'); - handle.unlock(recoveryKey: SSSS_KEY); - expect((await client.encryption.ssss.getCached('m.cross_signing.self_signing')) != null, false); - expect((await client.encryption.ssss.getCached('m.cross_signing.user_signing')) != null, false); - await handle.getStored('m.cross_signing.self_signing'); - expect((await client.encryption.ssss.getCached('m.cross_signing.self_signing')) != null, true); - await handle.maybeCacheAll(); - expect((await client.encryption.ssss.getCached('m.cross_signing.user_signing')) != null, true); - expect((await client.encryption.ssss.getCached('m.megolm_backup.v1')) != null, true); - }); + // now test some fail scenarios - test('make share requests', () async { - final key = client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; - key.setDirectVerified(true); - FakeMatrixApi.calledEndpoints.clear(); - await client.encryption.ssss.request('some.type', [key]); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), true); - }); + // not by us + event = ToDeviceEvent( + sender: '@someotheruser:example.org', + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); - test('answer to share requests', () async { - var event = ToDeviceEvent( - sender: client.userID, - type: 'm.secret.request', - content: { - 'action': 'request', - 'requesting_device_id': 'OTHERDEVICE', - 'name': 'm.cross_signing.self_signing', - 'request_id': '1', - }, - ); - FakeMatrixApi.calledEndpoints.clear(); - await client.encryption.ssss.handleToDeviceEvent(event); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), true); + // secret not cached + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.unknown.secret', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); - // now test some fail scenarios + // is a cancelation + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request_cancellation', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); - // not by us - event = ToDeviceEvent( - sender: '@someotheruser:example.org', - type: 'm.secret.request', - content: { - 'action': 'request', - 'requesting_device_id': 'OTHERDEVICE', - 'name': 'm.cross_signing.self_signing', - 'request_id': '1', - }, - ); - FakeMatrixApi.calledEndpoints.clear(); - await client.encryption.ssss.handleToDeviceEvent(event); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + // device not verified + final key = + client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(false); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); + key.setDirectVerified(true); + }); - // secret not cached + test('receive share requests', () async { + final key = + client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(true); + final handle = + client.encryption.ssss.open('m.cross_signing.self_signing'); + handle.unlock(recoveryKey: SSSS_KEY); + + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('best animal', [key]); + var event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': 'foxies!', + }, + encryptedContent: { + 'sender_key': key.curve25519Key, + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(await client.encryption.ssss.getCached('best animal'), 'foxies!'); + + // test the different validators + for (final type in [ + 'm.cross_signing.self_signing', + 'm.cross_signing.user_signing', + 'm.megolm_backup.v1' + ]) { + final secret = await handle.getStored(type); + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request(type, [key]); event = ToDeviceEvent( sender: client.userID, - type: 'm.secret.request', + type: 'm.secret.send', content: { - 'action': 'request', - 'requesting_device_id': 'OTHERDEVICE', - 'name': 'm.unknown.secret', - 'request_id': '1', + 'request_id': + client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': secret, + }, + encryptedContent: { + 'sender_key': key.curve25519Key, }, ); - FakeMatrixApi.calledEndpoints.clear(); await client.encryption.ssss.handleToDeviceEvent(event); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + expect(await client.encryption.ssss.getCached(type), secret); + } - // is a cancelation - event = ToDeviceEvent( - sender: client.userID, - type: 'm.secret.request', - content: { - 'action': 'request_cancellation', - 'requesting_device_id': 'OTHERDEVICE', - 'name': 'm.cross_signing.self_signing', - 'request_id': '1', - }, - ); - FakeMatrixApi.calledEndpoints.clear(); - await client.encryption.ssss.handleToDeviceEvent(event); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + // test different fail scenarios - // device not verified - final key = client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; - key.setDirectVerified(false); - event = ToDeviceEvent( - sender: client.userID, - type: 'm.secret.request', - content: { - 'action': 'request', - 'requesting_device_id': 'OTHERDEVICE', - 'name': 'm.cross_signing.self_signing', - 'request_id': '1', - }, - ); - FakeMatrixApi.calledEndpoints.clear(); - await client.encryption.ssss.handleToDeviceEvent(event); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); - key.setDirectVerified(true); - }); + // not encrypted + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('best animal', [key]); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': 'foxies!', + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(await client.encryption.ssss.getCached('best animal'), null); -// test('fail', () { -// expect(true, false); -// }); + // unknown request id + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('best animal', [key]); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': 'invalid', + 'secret': 'foxies!', + }, + encryptedContent: { + 'sender_key': key.curve25519Key, + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(await client.encryption.ssss.getCached('best animal'), null); - test('dispose client', () async { - await client.dispose(closeDatabase: true); - }); + // not from a device we sent the request to + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('best animal', [key]); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': 'foxies!', + }, + encryptedContent: { + 'sender_key': 'invalid', + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(await client.encryption.ssss.getCached('best animal'), null); + + // secret not a string + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('best animal', [key]); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': 42, + }, + encryptedContent: { + 'sender_key': key.curve25519Key, + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(await client.encryption.ssss.getCached('best animal'), null); + + // validator doesn't check out + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('m.megolm_backup.v1', [key]); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': 'foxies!', + }, + encryptedContent: { + 'sender_key': key.curve25519Key, + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + await client.encryption.ssss.getCached('m.megolm_backup.v1'), null); + }); + + test('request all', () async { + final key = + client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(true); + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.maybeRequestAll([key]); + expect(client.encryption.ssss.pendingShareRequests.length, 3); + }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); + }); }); } diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index e53e7a8..cb33cce 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -539,7 +539,8 @@ class FakeMatrixApi extends MockClient { 'encrypted': { '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { 'iv': 'eIb2IITxtmcq+1TrT8D5eQ==', - 'ciphertext': 'lWRTPo5qxf4LAVwVPzGHOyMcP181n7bb9/B0lvkLDC2Oy4DvAL0eLx2x3bY=', + 'ciphertext': + 'lWRTPo5qxf4LAVwVPzGHOyMcP181n7bb9/B0lvkLDC2Oy4DvAL0eLx2x3bY=', 'mac': 'Ynx89tIxPkx0o6ljMgxszww17JOgB4tg4etmNnMC9XI=' } } @@ -551,7 +552,8 @@ class FakeMatrixApi extends MockClient { 'encrypted': { '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { 'iv': 'YqU2XIjYulYZl+bkZtGgVw==', - 'ciphertext': 'kM2TSoy/jR/4d357ZoRPbpPypxQl6XRLo3FsEXz+f7vIOp82GeRp28RYb3k=', + 'ciphertext': + 'kM2TSoy/jR/4d357ZoRPbpPypxQl6XRLo3FsEXz+f7vIOp82GeRp28RYb3k=', 'mac': 'F+DZa5tAFmWsYSryw5EuEpzTmmABRab4GETkM85bGGo=' } } @@ -563,7 +565,8 @@ class FakeMatrixApi extends MockClient { 'encrypted': { '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { 'iv': 'D7AM3LXFu7ZlyGOkR+OeqQ==', - 'ciphertext': 'bYA2+OMgsO6QB1E31aY+ESAWrT0fUBTXqajy4qmL7bVDSZY4Uj64EXNbHuA=', + 'ciphertext': + 'bYA2+OMgsO6QB1E31aY+ESAWrT0fUBTXqajy4qmL7bVDSZY4Uj64EXNbHuA=', 'mac': 'j2UtyPo/UBSoiaQCWfzCiRZXp3IRt0ZZujuXgUMjnw4=' } } @@ -575,7 +578,8 @@ class FakeMatrixApi extends MockClient { 'encrypted': { '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { 'iv': 'cL/0MJZaiEd3fNU+I9oJrw==', - 'ciphertext': 'WL73Pzdk5wZdaaSpaeRH0uZYKcxkuV8IS6Qa2FEfA1+vMeRLuHcWlXbMX0w=', + 'ciphertext': + 'WL73Pzdk5wZdaaSpaeRH0uZYKcxkuV8IS6Qa2FEfA1+vMeRLuHcWlXbMX0w=', 'mac': '+xozp909S6oDX8KRV8D8ZFVRyh7eEYQpPP76f+DOsnw=' } } @@ -1527,12 +1531,14 @@ class FakeMatrixApi extends MockClient { 'event_fields': ['type', 'content', 'sender'] }, '/client/unstable/room_keys/version': (var req) => { - 'algorithm': 'm.megolm_backup.v1.curve25519-aes-sha2', - 'auth_data': {'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM'}, - 'count': 0, - 'etag': '0', - 'version': '5', - }, + 'algorithm': 'm.megolm_backup.v1.curve25519-aes-sha2', + 'auth_data': { + 'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM' + }, + 'count': 0, + 'etag': '0', + 'version': '5', + }, }, 'POST': { '/client/r0/delete_devices': (var req) => {}, @@ -1782,7 +1788,8 @@ class FakeMatrixApi extends MockClient { 'user_id': '@test:fakeServer.notExisting', 'usage': ['master'], 'keys': { - 'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8': '82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8', + 'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8': + '82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8', }, 'signatures': {}, }, @@ -1792,7 +1799,8 @@ class FakeMatrixApi extends MockClient { 'user_id': '@test:fakeServer.notExisting', 'usage': ['self_signing'], 'keys': { - 'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY': 'F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY', + 'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY': + 'F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY', }, 'signatures': {}, }, @@ -1802,7 +1810,8 @@ class FakeMatrixApi extends MockClient { 'user_id': '@test:fakeServer.notExisting', 'usage': ['user_signing'], 'keys': { - 'ed25519:0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g': '0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g', + 'ed25519:0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g': + '0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g', }, 'signatures': {}, }, From ef0a567401c8ad366a5c10e3c8675a51530d2c83 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 13 Jun 2020 18:10:24 +0200 Subject: [PATCH 52/64] finish ssss tests --- lib/encryption/cross_signing.dart | 2 +- lib/encryption/key_manager.dart | 3 +- lib/src/database/database.g.dart | 9 + lib/src/database/database.moor | 1 + test.sh | 2 +- test/encryption/cross_signing_test.dart | 113 ++++++ test/encryption/ssss_test.dart | 487 +++++++++++++++++------- test/fake_matrix_api.dart | 60 ++- 8 files changed, 513 insertions(+), 164 deletions(-) create mode 100644 test/encryption/cross_signing_test.dart diff --git a/lib/encryption/cross_signing.dart b/lib/encryption/cross_signing.dart index 6154088..92cbb86 100644 --- a/lib/encryption/cross_signing.dart +++ b/lib/encryption/cross_signing.dart @@ -93,7 +93,7 @@ class CrossSigning { } // master key is valid, set it to verified await masterKey.setVerified(true, false); - // and now sign bout our own key and our master key + // and now sign both our own key and our master key await sign([ masterKey, client.userDeviceKeys[client.userID].deviceKeys[client.deviceID] diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index a284d19..eeeb08d 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -405,7 +405,8 @@ class KeyManager { try { await loadSingleKey(room.id, sessionId); } catch (err, stacktrace) { - print('[KeyManager] Failed to access online key backup: ' + err.toString()); + print( + '[KeyManager] Failed to access online key backup: ' + err.toString()); print(stacktrace); } if (!hadPreviously && diff --git a/lib/src/database/database.g.dart b/lib/src/database/database.g.dart index 0c50118..52bf988 100644 --- a/lib/src/database/database.g.dart +++ b/lib/src/database/database.g.dart @@ -5850,6 +5850,15 @@ abstract class _$Database extends GeneratedDatabase { readsFrom: {ssssCache}).map(_rowToDbSSSSCache); } + Future clearSSSSCache(int client_id) { + return customUpdate( + 'DELETE FROM ssss_cache WHERE client_id = :client_id', + variables: [Variable.withInt(client_id)], + updates: {ssssCache}, + updateKind: UpdateKind.delete, + ); + } + Future insertClient( String name, String homeserver_url, diff --git a/lib/src/database/database.moor b/lib/src/database/database.moor index 68a0ee5..abe513d 100644 --- a/lib/src/database/database.moor +++ b/lib/src/database/database.moor @@ -198,6 +198,7 @@ storeUserCrossSigningKey: INSERT OR REPLACE INTO user_cross_signing_keys (client removeUserCrossSigningKey: DELETE FROM user_cross_signing_keys WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key; storeSSSSCache: INSERT OR REPLACE INTO ssss_cache (client_id, type, key_id, ciphertext, content) VALUES (:client_id, :type, :key_id, :ciphertext, :content); dbGetSSSSCache: SELECT * FROM ssss_cache WHERE client_id = :client_id AND type = :type; +clearSSSSCache: DELETE FROM ssss_cache WHERE client_id = :client_id; insertClient: INSERT INTO clients (name, homeserver_url, token, user_id, device_id, device_name, prev_batch, olm_account) VALUES (:name, :homeserver_url, :token, :user_id, :device_id, :device_name, :prev_batch, :olm_account); ensureRoomExists: INSERT OR IGNORE INTO rooms (client_id, room_id, membership) VALUES (:client_id, :room_id, :membership); setRoomPrevBatch: UPDATE rooms SET prev_batch = :prev_batch WHERE client_id = :client_id AND room_id = :room_id; diff --git a/test.sh b/test.sh index 5efc157..2496656 100644 --- a/test.sh +++ b/test.sh @@ -2,5 +2,5 @@ pub run test -p vm pub run test_coverage pub global activate remove_from_coverage -pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '.g.dart$' +pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' genhtml -o coverage coverage/lcov.info || true diff --git a/test/encryption/cross_signing_test.dart b/test/encryption/cross_signing_test.dart new file mode 100644 index 0000000..4ec212b --- /dev/null +++ b/test/encryption/cross_signing_test.dart @@ -0,0 +1,113 @@ +/* + * Ansible inventory script used at Famedly GmbH for managing many hosts + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:convert'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:test/test.dart'; +import 'package:olm/olm.dart' as olm; + +import '../fake_client.dart'; +import '../fake_matrix_api.dart'; + +void main() { + group('Cross Signing', () { + var olmEnabled = true; + try { + olm.init(); + olm.Account(); + } catch (_) { + olmEnabled = false; + print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + } + print('[LibOlm] Enabled: $olmEnabled'); + + if (!olmEnabled) return; + + Client client; + + test('setupClient', () async { + client = await getClient(); + }); + + test('basic things', () async { + expect(client.encryption.crossSigning.enabled, true); + }); + + test('selfSign', () async { + final key = client.userDeviceKeys[client.userID].masterKey; + key.setDirectVerified(false); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.crossSigning.selfSign(recoveryKey: SSSS_KEY); + expect(key.directVerified, true); + expect( + FakeMatrixApi.calledEndpoints + .containsKey('/client/r0/keys/signatures/upload'), + true); + expect(await client.encryption.crossSigning.isCached(), true); + }); + + test('signable', () async { + expect( + client.encryption.crossSigning + .signable([client.userDeviceKeys[client.userID].masterKey]), + true); + expect( + client.encryption.crossSigning.signable([ + client.userDeviceKeys[client.userID].deviceKeys[client.deviceID] + ]), + false); + expect( + client.encryption.crossSigning.signable( + [client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']]), + true); + expect( + client.encryption.crossSigning.signable([ + client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'] + ]), + false); + }); + + test('sign', () async { + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.crossSigning.sign([ + client.userDeviceKeys[client.userID].masterKey, + client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'], + client.userDeviceKeys['@othertest:fakeServer.notExisting'].masterKey + ]); + var body = json.decode(FakeMatrixApi + .calledEndpoints['/client/r0/keys/signatures/upload'].first); + expect(body['@test:fakeServer.notExisting'].containsKey('OTHERDEVICE'), + true); + expect( + body['@test:fakeServer.notExisting'].containsKey( + client.userDeviceKeys[client.userID].masterKey.publicKey), + true); + expect( + body['@othertest:fakeServer.notExisting'].containsKey(client + .userDeviceKeys['@othertest:fakeServer.notExisting'] + .masterKey + .publicKey), + true); + }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); + }); + }); +} diff --git a/test/encryption/ssss_test.dart b/test/encryption/ssss_test.dart index 55d0d8f..a0d5b94 100644 --- a/test/encryption/ssss_test.dart +++ b/test/encryption/ssss_test.dart @@ -32,176 +32,367 @@ import '../fake_matrix_api.dart'; void main() { group('SSSS', () { var olmEnabled = true; + try { + olm.init(); + olm.Account(); + } catch (_) { + olmEnabled = false; + print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + } + print('[LibOlm] Enabled: $olmEnabled'); + + if (!olmEnabled) return; + + Client client; + + test('setupClient', () async { + client = await getClient(); + }); + + test('basic things', () async { + expect(client.encryption.ssss.defaultKeyId, + '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'); + }); + + test('encrypt / decrypt', () { + final key = Uint8List.fromList(SecureRandom(32).bytes); + + final enc = SSSS.encryptAes('secret foxies', key, 'name'); + final dec = SSSS.decryptAes(enc, key, 'name'); + expect(dec, 'secret foxies'); + }); + + test('store', () async { + final handle = client.encryption.ssss.open(); + var failed = false; try { - olm.init(); - olm.Account(); + handle.unlock(passphrase: 'invalid'); } catch (_) { - olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + failed = true; } - print('[LibOlm] Enabled: $olmEnabled'); - - if (!olmEnabled) return; - - Client client; - - test('setupClient', () async { - client = await getClient(); + expect(failed, true); + expect(handle.isUnlocked, false); + failed = false; + try { + handle.unlock(recoveryKey: 'invalid'); + } catch (_) { + failed = true; + } + expect(failed, true); + expect(handle.isUnlocked, false); + handle.unlock(passphrase: SSSS_PASSPHRASE); + handle.unlock(recoveryKey: SSSS_KEY); + expect(handle.isUnlocked, true); + FakeMatrixApi.calledEndpoints.clear(); + await handle.store('best animal', 'foxies'); + // alright, since we don't properly sync we will manually have to update + // account_data for this test + final content = FakeMatrixApi + .calledEndpoints[ + '/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal'] + .first; + client.accountData['best animal'] = BasicEvent.fromJson({ + 'type': 'best animal', + 'content': json.decode(content), }); + expect(await handle.getStored('best animal'), 'foxies'); + }); - test('basic things', () async { - expect(client.encryption.ssss.defaultKeyId, '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'); - }); + test('cache', () async { + final handle = + client.encryption.ssss.open('m.cross_signing.self_signing'); + handle.unlock(recoveryKey: SSSS_KEY); + expect( + (await client.encryption.ssss + .getCached('m.cross_signing.self_signing')) != + null, + false); + expect( + (await client.encryption.ssss + .getCached('m.cross_signing.user_signing')) != + null, + false); + await handle.getStored('m.cross_signing.self_signing'); + expect( + (await client.encryption.ssss + .getCached('m.cross_signing.self_signing')) != + null, + true); + await handle.maybeCacheAll(); + expect( + (await client.encryption.ssss + .getCached('m.cross_signing.user_signing')) != + null, + true); + expect( + (await client.encryption.ssss.getCached('m.megolm_backup.v1')) != + null, + true); + }); - test('encrypt / decrypt', () { - final signing = olm.PkSigning(); - final key = Uint8List.fromList(SecureRandom(32).bytes); - - final enc = SSSS.encryptAes('secret foxies', key, 'name'); - final dec = SSSS.decryptAes(enc, key, 'name'); - expect(dec, 'secret foxies'); - }); + test('make share requests', () async { + final key = + client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(true); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.request('some.type', [key]); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + true); + }); - test('store', () async { - final handle = client.encryption.ssss.open(); - var failed = false; - try { - handle.unlock(passphrase: 'invalid'); - } catch (_) { - failed = true; - } - expect(failed, true); - expect(handle.isUnlocked, false); - failed = false; - try { - handle.unlock(recoveryKey: 'invalid'); - } catch (_) { - failed = true; - } - expect(failed, true); - expect(handle.isUnlocked, false); - handle.unlock(passphrase: SSSS_PASSPHRASE); - handle.unlock(recoveryKey: SSSS_KEY); - expect(handle.isUnlocked, true); - FakeMatrixApi.calledEndpoints.clear(); - await handle.store('best animal', 'foxies'); - // alright, since we don't properly sync we will manually have to update - // account_data for this test - final content = FakeMatrixApi.calledEndpoints['/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal'].first; - client.accountData['best animal'] = BasicEvent.fromJson({ - 'type': 'best animal', - 'content': json.decode(content), - }); - expect(await handle.getStored('best animal'), 'foxies'); - }); + test('answer to share requests', () async { + var event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + true); - test('cache', () async { - final handle = client.encryption.ssss.open('m.cross_signing.self_signing'); - handle.unlock(recoveryKey: SSSS_KEY); - expect((await client.encryption.ssss.getCached('m.cross_signing.self_signing')) != null, false); - expect((await client.encryption.ssss.getCached('m.cross_signing.user_signing')) != null, false); - await handle.getStored('m.cross_signing.self_signing'); - expect((await client.encryption.ssss.getCached('m.cross_signing.self_signing')) != null, true); - await handle.maybeCacheAll(); - expect((await client.encryption.ssss.getCached('m.cross_signing.user_signing')) != null, true); - expect((await client.encryption.ssss.getCached('m.megolm_backup.v1')) != null, true); - }); + // now test some fail scenarios - test('make share requests', () async { - final key = client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; - key.setDirectVerified(true); - FakeMatrixApi.calledEndpoints.clear(); - await client.encryption.ssss.request('some.type', [key]); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), true); - }); + // not by us + event = ToDeviceEvent( + sender: '@someotheruser:example.org', + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); - test('answer to share requests', () async { - var event = ToDeviceEvent( - sender: client.userID, - type: 'm.secret.request', - content: { - 'action': 'request', - 'requesting_device_id': 'OTHERDEVICE', - 'name': 'm.cross_signing.self_signing', - 'request_id': '1', - }, - ); - FakeMatrixApi.calledEndpoints.clear(); - await client.encryption.ssss.handleToDeviceEvent(event); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), true); + // secret not cached + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.unknown.secret', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); - // now test some fail scenarios + // is a cancelation + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request_cancellation', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); - // not by us - event = ToDeviceEvent( - sender: '@someotheruser:example.org', - type: 'm.secret.request', - content: { - 'action': 'request', - 'requesting_device_id': 'OTHERDEVICE', - 'name': 'm.cross_signing.self_signing', - 'request_id': '1', - }, - ); - FakeMatrixApi.calledEndpoints.clear(); - await client.encryption.ssss.handleToDeviceEvent(event); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + // device not verified + final key = + client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(false); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); + key.setDirectVerified(true); + }); - // secret not cached + test('receive share requests', () async { + final key = + client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(true); + final handle = + client.encryption.ssss.open('m.cross_signing.self_signing'); + handle.unlock(recoveryKey: SSSS_KEY); + + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('best animal', [key]); + var event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': 'foxies!', + }, + encryptedContent: { + 'sender_key': key.curve25519Key, + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(await client.encryption.ssss.getCached('best animal'), 'foxies!'); + + // test the different validators + for (final type in [ + 'm.cross_signing.self_signing', + 'm.cross_signing.user_signing', + 'm.megolm_backup.v1' + ]) { + final secret = await handle.getStored(type); + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request(type, [key]); event = ToDeviceEvent( sender: client.userID, - type: 'm.secret.request', + type: 'm.secret.send', content: { - 'action': 'request', - 'requesting_device_id': 'OTHERDEVICE', - 'name': 'm.unknown.secret', - 'request_id': '1', + 'request_id': + client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': secret, + }, + encryptedContent: { + 'sender_key': key.curve25519Key, }, ); - FakeMatrixApi.calledEndpoints.clear(); await client.encryption.ssss.handleToDeviceEvent(event); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + expect(await client.encryption.ssss.getCached(type), secret); + } - // is a cancelation - event = ToDeviceEvent( - sender: client.userID, - type: 'm.secret.request', - content: { - 'action': 'request_cancellation', - 'requesting_device_id': 'OTHERDEVICE', - 'name': 'm.cross_signing.self_signing', - 'request_id': '1', - }, - ); - FakeMatrixApi.calledEndpoints.clear(); - await client.encryption.ssss.handleToDeviceEvent(event); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + // test different fail scenarios - // device not verified - final key = client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; - key.setDirectVerified(false); - event = ToDeviceEvent( - sender: client.userID, - type: 'm.secret.request', - content: { - 'action': 'request', - 'requesting_device_id': 'OTHERDEVICE', - 'name': 'm.cross_signing.self_signing', - 'request_id': '1', - }, - ); - FakeMatrixApi.calledEndpoints.clear(); - await client.encryption.ssss.handleToDeviceEvent(event); - expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); - key.setDirectVerified(true); - }); + // not encrypted + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('best animal', [key]); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': 'foxies!', + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(await client.encryption.ssss.getCached('best animal'), null); -// test('fail', () { -// expect(true, false); -// }); + // unknown request id + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('best animal', [key]); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': 'invalid', + 'secret': 'foxies!', + }, + encryptedContent: { + 'sender_key': key.curve25519Key, + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(await client.encryption.ssss.getCached('best animal'), null); - test('dispose client', () async { - await client.dispose(closeDatabase: true); - }); + // not from a device we sent the request to + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('best animal', [key]); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': 'foxies!', + }, + encryptedContent: { + 'sender_key': 'invalid', + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(await client.encryption.ssss.getCached('best animal'), null); + + // secret not a string + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('best animal', [key]); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': 42, + }, + encryptedContent: { + 'sender_key': key.curve25519Key, + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(await client.encryption.ssss.getCached('best animal'), null); + + // validator doesn't check out + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.request('m.megolm_backup.v1', [key]); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.send', + content: { + 'request_id': client.encryption.ssss.pendingShareRequests.keys.first, + 'secret': 'foxies!', + }, + encryptedContent: { + 'sender_key': key.curve25519Key, + }, + ); + await client.encryption.ssss.handleToDeviceEvent(event); + expect( + await client.encryption.ssss.getCached('m.megolm_backup.v1'), null); + }); + + test('request all', () async { + final key = + client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(true); + await client.database.clearSSSSCache(client.id); + client.encryption.ssss.pendingShareRequests.clear(); + await client.encryption.ssss.maybeRequestAll([key]); + expect(client.encryption.ssss.pendingShareRequests.length, 3); + }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); + }); }); } diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index e53e7a8..e69538c 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -539,7 +539,8 @@ class FakeMatrixApi extends MockClient { 'encrypted': { '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { 'iv': 'eIb2IITxtmcq+1TrT8D5eQ==', - 'ciphertext': 'lWRTPo5qxf4LAVwVPzGHOyMcP181n7bb9/B0lvkLDC2Oy4DvAL0eLx2x3bY=', + 'ciphertext': + 'lWRTPo5qxf4LAVwVPzGHOyMcP181n7bb9/B0lvkLDC2Oy4DvAL0eLx2x3bY=', 'mac': 'Ynx89tIxPkx0o6ljMgxszww17JOgB4tg4etmNnMC9XI=' } } @@ -551,7 +552,8 @@ class FakeMatrixApi extends MockClient { 'encrypted': { '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { 'iv': 'YqU2XIjYulYZl+bkZtGgVw==', - 'ciphertext': 'kM2TSoy/jR/4d357ZoRPbpPypxQl6XRLo3FsEXz+f7vIOp82GeRp28RYb3k=', + 'ciphertext': + 'kM2TSoy/jR/4d357ZoRPbpPypxQl6XRLo3FsEXz+f7vIOp82GeRp28RYb3k=', 'mac': 'F+DZa5tAFmWsYSryw5EuEpzTmmABRab4GETkM85bGGo=' } } @@ -563,7 +565,8 @@ class FakeMatrixApi extends MockClient { 'encrypted': { '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { 'iv': 'D7AM3LXFu7ZlyGOkR+OeqQ==', - 'ciphertext': 'bYA2+OMgsO6QB1E31aY+ESAWrT0fUBTXqajy4qmL7bVDSZY4Uj64EXNbHuA=', + 'ciphertext': + 'bYA2+OMgsO6QB1E31aY+ESAWrT0fUBTXqajy4qmL7bVDSZY4Uj64EXNbHuA=', 'mac': 'j2UtyPo/UBSoiaQCWfzCiRZXp3IRt0ZZujuXgUMjnw4=' } } @@ -575,7 +578,8 @@ class FakeMatrixApi extends MockClient { 'encrypted': { '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { 'iv': 'cL/0MJZaiEd3fNU+I9oJrw==', - 'ciphertext': 'WL73Pzdk5wZdaaSpaeRH0uZYKcxkuV8IS6Qa2FEfA1+vMeRLuHcWlXbMX0w=', + 'ciphertext': + 'WL73Pzdk5wZdaaSpaeRH0uZYKcxkuV8IS6Qa2FEfA1+vMeRLuHcWlXbMX0w=', 'mac': '+xozp909S6oDX8KRV8D8ZFVRyh7eEYQpPP76f+DOsnw=' } } @@ -1527,12 +1531,14 @@ class FakeMatrixApi extends MockClient { 'event_fields': ['type', 'content', 'sender'] }, '/client/unstable/room_keys/version': (var req) => { - 'algorithm': 'm.megolm_backup.v1.curve25519-aes-sha2', - 'auth_data': {'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM'}, - 'count': 0, - 'etag': '0', - 'version': '5', - }, + 'algorithm': 'm.megolm_backup.v1.curve25519-aes-sha2', + 'auth_data': { + 'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM' + }, + 'count': 0, + 'etag': '0', + 'version': '5', + }, }, 'POST': { '/client/r0/delete_devices': (var req) => {}, @@ -1782,7 +1788,16 @@ class FakeMatrixApi extends MockClient { 'user_id': '@test:fakeServer.notExisting', 'usage': ['master'], 'keys': { - 'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8': '82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8', + 'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8': + '82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8', + }, + 'signatures': {}, + }, + '@othertest:fakeServer.notExisting': { + 'user_id': '@othertest:fakeServer.notExisting', + 'usage': ['master'], + 'keys': { + 'ed25519:master': 'master', }, 'signatures': {}, }, @@ -1792,7 +1807,16 @@ class FakeMatrixApi extends MockClient { 'user_id': '@test:fakeServer.notExisting', 'usage': ['self_signing'], 'keys': { - 'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY': 'F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY', + 'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY': + 'F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY', + }, + 'signatures': {}, + }, + '@othertest:fakeServer.notExisting': { + 'user_id': '@othertest:fakeServer.notExisting', + 'usage': ['self_signing'], + 'keys': { + 'ed25519:self_signing': 'self_signing', }, 'signatures': {}, }, @@ -1802,7 +1826,16 @@ class FakeMatrixApi extends MockClient { 'user_id': '@test:fakeServer.notExisting', 'usage': ['user_signing'], 'keys': { - 'ed25519:0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g': '0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g', + 'ed25519:0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g': + '0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g', + }, + 'signatures': {}, + }, + '@othertest:fakeServer.notExisting': { + 'user_id': '@othertest:fakeServer.notExisting', + 'usage': ['user_signing'], + 'keys': { + 'ed25519:user_signing': 'user_signing', }, 'signatures': {}, }, @@ -1854,6 +1887,7 @@ class FakeMatrixApi extends MockClient { '/client/r0/rooms/!localpart%3Aserver.abc/ban': (var reqI) => {}, '/client/r0/rooms/!localpart%3Aserver.abc/unban': (var reqI) => {}, '/client/r0/rooms/!localpart%3Aserver.abc/invite': (var reqI) => {}, + '/client/r0/keys/signatures/upload': (var reqI) => {'failures': {}}, }, 'PUT': { '/client/r0/presence/${Uri.encodeComponent('@alice:example.com')}/status': From c233d57f9fc81367904453378271155887996f4c Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 13 Jun 2020 19:48:38 +0200 Subject: [PATCH 53/64] add online key backup test --- lib/encryption/key_manager.dart | 31 +++++---- test/encryption/key_request_test.dart | 20 +++--- test/encryption/online_key_backup_test.dart | 73 +++++++++++++++++++++ test/fake_matrix_api.dart | 15 ++++- 4 files changed, 116 insertions(+), 23 deletions(-) create mode 100644 test/encryption/online_key_backup_test.dart diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index eeeb08d..9621112 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -398,20 +398,23 @@ class KeyManager { } /// Request a certain key from another device - Future request(Room room, String sessionId, String senderKey) async { - // let's first check our online key backup store thingy... - var hadPreviously = - getInboundGroupSession(room.id, sessionId, senderKey) != null; - try { - await loadSingleKey(room.id, sessionId); - } catch (err, stacktrace) { - print( - '[KeyManager] Failed to access online key backup: ' + err.toString()); - print(stacktrace); - } - if (!hadPreviously && - getInboundGroupSession(room.id, sessionId, senderKey) != null) { - return; // we managed to load the session from online backup, no need to care about it now + Future request(Room room, String sessionId, String senderKey, + {bool tryOnlineBackup = true}) async { + if (tryOnlineBackup) { + // let's first check our online key backup store thingy... + var hadPreviously = + getInboundGroupSession(room.id, sessionId, senderKey) != null; + try { + await loadSingleKey(room.id, sessionId); + } catch (err, stacktrace) { + print('[KeyManager] Failed to access online key backup: ' + + err.toString()); + print(stacktrace); + } + if (!hadPreviously && + getInboundGroupSession(room.id, sessionId, senderKey) != null) { + return; // we managed to load the session from online backup, no need to care about it now + } } // while we just send the to-device event to '*', we still need to save the // devices themself to know where to send the cancel to after receiving a reply diff --git a/test/encryption/key_request_test.dart b/test/encryption/key_request_test.dart index 168a167..c7dbb9f 100644 --- a/test/encryption/key_request_test.dart +++ b/test/encryption/key_request_test.dart @@ -56,8 +56,9 @@ void main() { test('Create Request', () async { var matrix = await getClient(); final requestRoom = matrix.getRoomById('!726s6s6q:example.com'); - await matrix.encryption.keyManager - .request(requestRoom, 'sessionId', validSenderKey); + await matrix.encryption.keyManager.request( + requestRoom, 'sessionId', validSenderKey, + tryOnlineBackup: false); var foundEvent = false; for (var entry in FakeMatrixApi.calledEndpoints.entries) { final payload = jsonDecode(entry.value.first); @@ -223,8 +224,9 @@ void main() { test('Receive shared keys', () async { var matrix = await getClient(); final requestRoom = matrix.getRoomById('!726s6s6q:example.com'); - await matrix.encryption.keyManager - .request(requestRoom, validSessionId, validSenderKey); + await matrix.encryption.keyManager.request( + requestRoom, validSessionId, validSenderKey, + tryOnlineBackup: false); final session = await matrix.encryption.keyManager .loadInboundGroupSession( @@ -279,8 +281,9 @@ void main() { false); // unknown device - await matrix.encryption.keyManager - .request(requestRoom, validSessionId, validSenderKey); + await matrix.encryption.keyManager.request( + requestRoom, validSessionId, validSenderKey, + tryOnlineBackup: false); matrix.encryption.keyManager.clearInboundGroupSessions(); event = ToDeviceEvent( sender: '@alice:example.com', @@ -304,8 +307,9 @@ void main() { false); // no encrypted content - await matrix.encryption.keyManager - .request(requestRoom, validSessionId, validSenderKey); + await matrix.encryption.keyManager.request( + requestRoom, validSessionId, validSenderKey, + tryOnlineBackup: false); matrix.encryption.keyManager.clearInboundGroupSessions(); event = ToDeviceEvent( sender: '@alice:example.com', diff --git a/test/encryption/online_key_backup_test.dart b/test/encryption/online_key_backup_test.dart new file mode 100644 index 0000000..0a3b842 --- /dev/null +++ b/test/encryption/online_key_backup_test.dart @@ -0,0 +1,73 @@ +/* + * Ansible inventory script used at Famedly GmbH for managing many hosts + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:test/test.dart'; +import 'package:olm/olm.dart' as olm; + +import '../fake_client.dart'; + +void main() { + group('Online Key Backup', () { + var olmEnabled = true; + try { + olm.init(); + olm.Account(); + } catch (_) { + olmEnabled = false; + print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + } + print('[LibOlm] Enabled: $olmEnabled'); + + if (!olmEnabled) return; + + Client client; + + final roomId = '!726s6s6q:example.com'; + final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; + final senderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg'; + + test('setupClient', () async { + client = await getClient(); + }); + + test('basic things', () async { + expect(client.encryption.keyManager.enabled, true); + expect(await client.encryption.keyManager.isCached(), false); + final handle = client.encryption.ssss.open(); + handle.unlock(recoveryKey: SSSS_KEY); + await handle.maybeCacheAll(); + expect(await client.encryption.keyManager.isCached(), true); + }); + + test('load key', () async { + client.encryption.keyManager.clearInboundGroupSessions(); + await client.encryption.keyManager + .request(client.getRoomById(roomId), sessionId, senderKey); + expect( + client.encryption.keyManager + .getInboundGroupSession(roomId, sessionId, senderKey) != + null, + true); + }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); + }); + }); +} diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index e69538c..4fb17cf 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -1533,12 +1533,25 @@ class FakeMatrixApi extends MockClient { '/client/unstable/room_keys/version': (var req) => { 'algorithm': 'm.megolm_backup.v1.curve25519-aes-sha2', 'auth_data': { - 'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM' + 'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM', + 'signatures': {}, }, 'count': 0, 'etag': '0', 'version': '5', }, + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5': + (var req) => { + 'first_message_index': 0, + 'forwarded_count': 0, + 'is_verified': true, + 'session_data': { + 'ephemeral': 'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc', + 'ciphertext': + '19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg', + 'mac': 'QzKV/fgAs4U', + }, + }, }, 'POST': { '/client/r0/delete_devices': (var req) => {}, From 9b2952435f835de7ad8367068906e6e08da4b8af Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 13 Jun 2020 20:44:25 +0200 Subject: [PATCH 54/64] add matrix api tests --- lib/matrix_api/matrix_api.dart | 2 +- test/fake_matrix_api.dart | 69 +++++++++ test/matrix_api_test.dart | 266 +++++++++++++++++++++++++++++++++ 3 files changed, 336 insertions(+), 1 deletion(-) diff --git a/lib/matrix_api/matrix_api.dart b/lib/matrix_api/matrix_api.dart index 3699f07..f09c59b 100644 --- a/lib/matrix_api/matrix_api.dart +++ b/lib/matrix_api/matrix_api.dart @@ -2077,7 +2077,7 @@ class MatrixApi { '/client/unstable/room_keys/version/${Uri.encodeComponent(version)}', data: { 'algorithm': algorithm.algorithmString, - 'auth_data': authData.toJson, + 'auth_data': authData.toJson(), 'version': version, }, ); diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 4fb17cf..322b2b7 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -1552,6 +1552,43 @@ class FakeMatrixApi extends MockClient { 'mac': 'QzKV/fgAs4U', }, }, + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5': + (var req) => { + 'sessions': { + 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU': { + 'first_message_index': 0, + 'forwarded_count': 0, + 'is_verified': true, + 'session_data': { + 'ephemeral': + 'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc', + 'ciphertext': + '19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg', + 'mac': 'QzKV/fgAs4U', + }, + }, + }, + }, + '/client/unstable/room_keys/keys?version=5': (var req) => { + 'rooms': { + '!726s6s6q:example.com': { + 'sessions': { + 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU': { + 'first_message_index': 0, + 'forwarded_count': 0, + 'is_verified': true, + 'session_data': { + 'ephemeral': + 'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc', + 'ciphertext': + '19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg', + 'mac': 'QzKV/fgAs4U', + }, + }, + }, + }, + }, + }, }, 'POST': { '/client/r0/delete_devices': (var req) => {}, @@ -1900,7 +1937,9 @@ class FakeMatrixApi extends MockClient { '/client/r0/rooms/!localpart%3Aserver.abc/ban': (var reqI) => {}, '/client/r0/rooms/!localpart%3Aserver.abc/unban': (var reqI) => {}, '/client/r0/rooms/!localpart%3Aserver.abc/invite': (var reqI) => {}, + '/client/r0/keys/device_signing/upload': (var reqI) => {}, '/client/r0/keys/signatures/upload': (var reqI) => {'failures': {}}, + '/client/unstable/room_keys/version': (var reqI) => {'version': '5'}, }, 'PUT': { '/client/r0/presence/${Uri.encodeComponent('@alice:example.com')}/status': @@ -1979,6 +2018,21 @@ class FakeMatrixApi extends MockClient { (var reqI) => {}, '/client/r0/directory/list/room/!localpart%3Aexample.com': (var req) => {}, + '/client/unstable/room_keys/version/5': (var req) => {}, + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5': + (var req) => { + 'etag': 'asdf', + 'count': 1, + }, + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5': + (var req) => { + 'etag': 'asdf', + 'count': 1, + }, + '/client/unstable/room_keys/keys?version=5': (var req) => { + 'etag': 'asdf', + 'count': 1, + }, }, 'DELETE': { '/unknown/token': (var req) => {'errcode': 'M_UNKNOWN_TOKEN'}, @@ -1989,6 +2043,21 @@ class FakeMatrixApi extends MockClient { (var req) => {}, '/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag': (var req) => {}, + '/client/unstable/room_keys/version/5': (var req) => {}, + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5': + (var req) => { + 'etag': 'asdf', + 'count': 1, + }, + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5': + (var req) => { + 'etag': 'asdf', + 'count': 1, + }, + '/client/unstable/room_keys/keys?version=5': (var req) => { + 'etag': 'asdf', + 'count': 1, + }, }, }; } diff --git a/test/matrix_api_test.dart b/test/matrix_api_test.dart index 317bae5..d585b36 100644 --- a/test/matrix_api_test.dart +++ b/test/matrix_api_test.dart @@ -1116,6 +1116,83 @@ void main() { matrixApi.homeserver = matrixApi.accessToken = null; }); + test('uploadDeviceSigningKeys', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final masterKey = MatrixCrossSigningKey.fromJson({ + 'user_id': '@test:fakeServer.notExisting', + 'usage': ['master'], + 'keys': { + 'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8': + '82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8', + }, + 'signatures': {}, + }); + final selfSigningKey = MatrixCrossSigningKey.fromJson({ + 'user_id': '@test:fakeServer.notExisting', + 'usage': ['self_signing'], + 'keys': { + 'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY': + 'F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY', + }, + 'signatures': {}, + }); + final userSigningKey = MatrixCrossSigningKey.fromJson({ + 'user_id': '@test:fakeServer.notExisting', + 'usage': ['user_signing'], + 'keys': { + 'ed25519:0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g': + '0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g', + }, + 'signatures': {}, + }); + await matrixApi.uploadDeviceSigningKeys( + masterKey: masterKey, + selfSigningKey: selfSigningKey, + userSigningKey: userSigningKey); + }); + test('uploadKeySignatures', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final key1 = MatrixDeviceKeys.fromJson({ + 'user_id': '@alice:example.com', + 'device_id': 'JLAFKJWSCS', + 'algorithms': ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'], + 'keys': { + 'curve25519:JLAFKJWSCS': + '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI', + 'ed25519:JLAFKJWSCS': 'lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI' + }, + 'signatures': { + '@alice:example.com': { + 'ed25519:JLAFKJWSCS': + 'dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA' + } + }, + 'unsigned': {'device_display_name': 'Alices mobile phone'}, + }); + final key2 = MatrixDeviceKeys.fromJson({ + 'user_id': '@alice:example.com', + 'device_id': 'JLAFKJWSCS', + 'algorithms': ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'], + 'keys': { + 'curve25519:JLAFKJWSCS': + '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI', + 'ed25519:JLAFKJWSCS': 'lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI' + }, + 'signatures': { + '@alice:example.com': {'ed25519:OTHERDEVICE': 'OTHERSIG'} + }, + 'unsigned': {'device_display_name': 'Alices mobile phone'}, + }); + final ret = await matrixApi.uploadKeySignatures([key1, key2]); + expect( + FakeMatrixApi.api['POST']['/client/r0/keys/signatures/upload']({}), + ret.toJson(), + ); + }); test('requestPushers', () async { matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); matrixApi.accessToken = '1234'; @@ -1514,5 +1591,194 @@ void main() { matrixApi.homeserver = matrixApi.accessToken = null; }); + test('createRoomKeysBackup', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final algorithm = RoomKeysAlgorithmType.v1Curve25519AesSha2; + final authData = RoomKeysAuthDataV1Curve25519AesSha2.fromJson({ + 'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM', + 'signatures': {}, + }); + final ret = await matrixApi.createRoomKeysBackup(algorithm, authData); + expect( + FakeMatrixApi.api['POST'] + ['/client/unstable/room_keys/version']({})['version'], + ret); + }); + test('getRoomKeysBackup', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final ret = await matrixApi.getRoomKeysBackup(); + expect(FakeMatrixApi.api['GET']['/client/unstable/room_keys/version']({}), + ret.toJson()); + }); + test('updateRoomKeysBackup', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final algorithm = RoomKeysAlgorithmType.v1Curve25519AesSha2; + final authData = RoomKeysAuthDataV1Curve25519AesSha2.fromJson({ + 'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM', + 'signatures': {}, + }); + await matrixApi.updateRoomKeysBackup('5', algorithm, authData); + }); + test('deleteRoomKeysBackup', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + await matrixApi.deleteRoomKeysBackup('5'); + }); + test('storeRoomKeysSingleKey', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final roomId = '!726s6s6q:example.com'; + final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; + final session = RoomKeysSingleKey.fromJson({ + 'first_message_index': 0, + 'forwarded_count': 0, + 'is_verified': true, + 'session_data': { + 'ephemeral': 'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc', + 'ciphertext': + '19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg', + 'mac': 'QzKV/fgAs4U', + }, + }); + final ret = await matrixApi.storeRoomKeysSingleKey( + roomId, sessionId, '5', session); + expect( + FakeMatrixApi.api['PUT'][ + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5']({}), + ret.toJson()); + }); + test('getRoomKeysSingleKey', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final roomId = '!726s6s6q:example.com'; + final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; + final ret = await matrixApi.getRoomKeysSingleKey(roomId, sessionId, '5'); + expect( + FakeMatrixApi.api['GET'][ + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5']({}), + ret.toJson()); + }); + test('deleteRoomKeysSingleKey', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final roomId = '!726s6s6q:example.com'; + final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; + final ret = + await matrixApi.deleteRoomKeysSingleKey(roomId, sessionId, '5'); + expect( + FakeMatrixApi.api['DELETE'][ + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5']({}), + ret.toJson()); + }); + test('storeRoomKeysRoom', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final roomId = '!726s6s6q:example.com'; + final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; + final session = RoomKeysRoom.fromJson({ + 'sessions': { + sessionId: { + 'first_message_index': 0, + 'forwarded_count': 0, + 'is_verified': true, + 'session_data': { + 'ephemeral': 'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc', + 'ciphertext': + '19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg', + 'mac': 'QzKV/fgAs4U', + }, + }, + }, + }); + final ret = await matrixApi.storeRoomKeysRoom(roomId, '5', session); + expect( + FakeMatrixApi.api['PUT'][ + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5']({}), + ret.toJson()); + }); + test('getRoomKeysRoom', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final roomId = '!726s6s6q:example.com'; + final ret = await matrixApi.getRoomKeysRoom(roomId, '5'); + expect( + FakeMatrixApi.api['GET'][ + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5']({}), + ret.toJson()); + }); + test('deleteRoomKeysRoom', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final roomId = '!726s6s6q:example.com'; + final ret = await matrixApi.deleteRoomKeysRoom(roomId, '5'); + expect( + FakeMatrixApi.api['DELETE'][ + '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5']({}), + ret.toJson()); + }); + test('storeRoomKeys', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final roomId = '!726s6s6q:example.com'; + final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; + final session = RoomKeys.fromJson({ + 'rooms': { + roomId: { + 'sessions': { + sessionId: { + 'first_message_index': 0, + 'forwarded_count': 0, + 'is_verified': true, + 'session_data': { + 'ephemeral': 'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc', + 'ciphertext': + '19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg', + 'mac': 'QzKV/fgAs4U', + }, + }, + }, + }, + }, + }); + final ret = await matrixApi.storeRoomKeys('5', session); + expect( + FakeMatrixApi.api['PUT'] + ['/client/unstable/room_keys/keys?version=5']({}), + ret.toJson()); + }); + test('getRoomKeys', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final ret = await matrixApi.getRoomKeys('5'); + expect( + FakeMatrixApi.api['GET'] + ['/client/unstable/room_keys/keys?version=5']({}), + ret.toJson()); + }); + test('deleteRoomKeys', () async { + matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); + matrixApi.accessToken = '1234'; + + final ret = await matrixApi.deleteRoomKeys('5'); + expect( + FakeMatrixApi.api['DELETE'] + ['/client/unstable/room_keys/keys?version=5']({}), + ret.toJson()); + }); }); } From e874a5e00b366dfb33ae606a0095aa68a530608a Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 15 Jun 2020 10:26:50 +0200 Subject: [PATCH 55/64] add signature verification tests --- lib/src/client.dart | 6 +- lib/src/utils/device_keys_list.dart | 7 +- test/device_keys_list_test.dart | 123 ++++++++++++++++++++++++++++ test/fake_matrix_api.dart | 28 ++++++- 4 files changed, 155 insertions(+), 9 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 6e324e8..f2b4faf 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1135,7 +1135,7 @@ class Client { var outdatedLists = {}; for (var userId in trackedUserIds) { if (!userDeviceKeys.containsKey(userId)) { - _userDeviceKeys[userId] = DeviceKeysList(userId); + _userDeviceKeys[userId] = DeviceKeysList(userId, this); } var deviceKeysList = userDeviceKeys[userId]; if (deviceKeysList.outdated) { @@ -1151,7 +1151,7 @@ class Client { for (final rawDeviceKeyListEntry in response.deviceKeys.entries) { final userId = rawDeviceKeyListEntry.key; if (!userDeviceKeys.containsKey(userId)) { - _userDeviceKeys[userId] = DeviceKeysList(userId); + _userDeviceKeys[userId] = DeviceKeysList(userId, this); } final oldKeys = Map.from(_userDeviceKeys[userId].deviceKeys); @@ -1230,7 +1230,7 @@ class Client { for (final crossSigningKeyListEntry in keys.entries) { final userId = crossSigningKeyListEntry.key; if (!userDeviceKeys.containsKey(userId)) { - _userDeviceKeys[userId] = DeviceKeysList(userId); + _userDeviceKeys[userId] = DeviceKeysList(userId, this); } final oldKeys = Map.from( _userDeviceKeys[userId].crossSigningKeys); diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index c32f713..7e4b08f 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -54,6 +54,8 @@ class DeviceKeysList { } Future startVerification() async { + print('++++++++++++'); + print(client.toString()); final roomId = await User(userId, room: Room(client: client)).startDirectChat(); if (roomId == null) { @@ -95,7 +97,7 @@ class DeviceKeysList { } } - DeviceKeysList(this.userId); + DeviceKeysList(this.userId, this.client); } abstract class SignableKey extends MatrixSignableKey { @@ -239,7 +241,8 @@ abstract class SignableKey extends MatrixSignableKey { Future setVerified(bool newVerified, [bool sign = true]) { _verified = newVerified; - if (sign && + if (newVerified && + sign && client.encryptionEnabled && client.encryption.crossSigning.signable([this])) { // sign the key! diff --git a/test/device_keys_list_test.dart b/test/device_keys_list_test.dart index c1f3204..a98ff1b 100644 --- a/test/device_keys_list_test.dart +++ b/test/device_keys_list_test.dart @@ -20,6 +20,10 @@ import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:test/test.dart'; +import 'package:olm/olm.dart' as olm; + +import './fake_client.dart'; +import './fake_matrix_api.dart'; void main() { /// All Tests related to device keys @@ -49,6 +53,125 @@ void main() { expect(json.encode(key.toJson()), json.encode(rawJson)); expect(key.directVerified, false); expect(key.blocked, true); + + rawJson = { + 'user_id': '@test:fakeServer.notExisting', + 'usage': ['master'], + 'keys': { + 'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8': + '82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8', + }, + 'signatures': {}, + }; + final crossKey = CrossSigningKey.fromJson(rawJson, null); + expect(json.encode(crossKey.toJson()), json.encode(rawJson)); + expect(crossKey.usage.first, 'master'); + }); + + var olmEnabled = true; + try { + olm.init(); + olm.Account(); + } catch (_) { + olmEnabled = false; + print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + } + print('[LibOlm] Enabled: $olmEnabled'); + + if (!olmEnabled) return; + + Client client; + + test('setupClient', () async { + client = await getClient(); + }); + + test('set blocked / verified', () async { + final key = + client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + final masterKey = client.userDeviceKeys[client.userID].masterKey; + masterKey.setDirectVerified(true); + // we need to populate the ssss cache to be able to test signing easily + final handle = client.encryption.ssss.open(); + handle.unlock(recoveryKey: SSSS_KEY); + await handle.maybeCacheAll(); + + expect(key.verified, true); + await key.setBlocked(true); + expect(key.verified, false); + await key.setBlocked(false); + expect(key.directVerified, false); + expect(key.verified, true); // still verified via cross-sgining + + expect(masterKey.verified, true); + await masterKey.setBlocked(true); + expect(masterKey.verified, false); + await masterKey.setBlocked(false); + expect(masterKey.verified, true); + + FakeMatrixApi.calledEndpoints.clear(); + await key.setVerified(true); + await Future.delayed(Duration(milliseconds: 10)); + expect( + FakeMatrixApi.calledEndpoints.keys + .any((k) => k == '/client/r0/keys/signatures/upload'), + true); + expect(key.directVerified, true); + + FakeMatrixApi.calledEndpoints.clear(); + await key.setVerified(false); + await Future.delayed(Duration(milliseconds: 10)); + expect( + FakeMatrixApi.calledEndpoints.keys + .any((k) => k == '/client/r0/keys/signatures/upload'), + false); + expect(key.directVerified, false); + }); + + test('verification based on signatures', () async { + final user = client.userDeviceKeys[client.userID]; + user.masterKey.setDirectVerified(true); + expect(user.deviceKeys['GHTYAJCE'].crossVerified, true); + expect(user.deviceKeys['GHTYAJCE'].signed, true); + expect(user.getKey('GHTYAJCE').crossVerified, true); + expect(user.deviceKeys['OTHERDEVICE'].crossVerified, true); + expect(user.selfSigningKey.crossVerified, true); + expect( + user + .getKey('F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY') + .crossVerified, + true); + expect(user.userSigningKey.crossVerified, true); + expect(user.verified, UserVerifiedStatus.verified); + user.masterKey.setDirectVerified(false); + expect(user.deviceKeys['GHTYAJCE'].crossVerified, false); + expect(user.deviceKeys['OTHERDEVICE'].crossVerified, false); + expect(user.verified, UserVerifiedStatus.unknown); + user.masterKey.setDirectVerified(true); + user.deviceKeys['GHTYAJCE'].signatures.clear(); + expect(user.deviceKeys['GHTYAJCE'].verified, + true); // it's our own device, should be direct verified + expect( + user.deviceKeys['GHTYAJCE'].signed, false); // not verified for others + user.deviceKeys['OTHERDEVICE'].signatures.clear(); + expect(user.verified, UserVerifiedStatus.unknownDevice); + }); + + test('start verification', () async { + var req = client + .userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'] + .startVerification(); + expect(req != null, true); + expect(req.room != null, false); + + req = + await client.userDeviceKeys['@alice:example.com'].startVerification(); + expect(req != null, true); + expect(req.room != null, true); + }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); }); }); } diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 322b2b7..4c02258 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -1799,7 +1799,12 @@ class FakeMatrixApi extends MockClient { 'ed25519:GHTYAJCE': 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo' }, - 'signatures': {}, + 'signatures': { + '@test:fakeServer.notExisting': { + 'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY': + 'Q4/55vZjEJD7M2EC40bgZqd9Zuy/4C75UPVopJdXeioQVaKtFf6EF0nUUuql0yD+r3hinsZcock0wO6Q2xcoAQ', + }, + }, }, 'OTHERDEVICE': { 'user_id': '@test:fakeServer.notExisting', @@ -1812,7 +1817,12 @@ class FakeMatrixApi extends MockClient { 'curve25519:OTHERDEVICE': 'blah', 'ed25519:OTHERDEVICE': 'blah' }, - 'signatures': {}, + 'signatures': { + '@test:fakeServer.notExisting': { + 'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY': + 'o7ucKPWrF2VKx7wYqP1f+aw4QohLMz7kX+SIw6aWCYsLC3XyIlg8rX/7QQ9B8figCVnRK7IjtjWvQodBCfWCAA', + }, + }, }, }, '@othertest:fakeServer.notExisting': { @@ -1860,7 +1870,12 @@ class FakeMatrixApi extends MockClient { 'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY': 'F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY', }, - 'signatures': {}, + 'signatures': { + '@test:fakeServer.notExisting': { + 'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8': + 'afkrbGvPn5Zb5zc7Lk9cz2skI3QrzI/L0st1GS+/GATxNjMzc6vKmGu7r9cMb1GJxy4RdeUpfH3L7Fs/fNL1Dw', + }, + }, }, '@othertest:fakeServer.notExisting': { 'user_id': '@othertest:fakeServer.notExisting', @@ -1879,7 +1894,12 @@ class FakeMatrixApi extends MockClient { 'ed25519:0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g': '0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g', }, - 'signatures': {}, + 'signatures': { + '@test:fakeServer.notExisting': { + 'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8': + 'pvgbZxEbllaElhpiRnb7/uOIUhrglvHCFnpoxr3/5ZrWa0EK/uaefhex9eEV4uBLrHjHg2ymwdNaM7ap9+sBBg', + }, + }, }, '@othertest:fakeServer.notExisting': { 'user_id': '@othertest:fakeServer.notExisting', From 0e0fd61c657567e2bcbdd429ee8aecfe0ec69636 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 15 Jun 2020 10:27:28 +0200 Subject: [PATCH 56/64] remove unneded print --- lib/src/utils/device_keys_list.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 7e4b08f..1f279f4 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -54,8 +54,6 @@ class DeviceKeysList { } Future startVerification() async { - print('++++++++++++'); - print(client.toString()); final roomId = await User(userId, room: Room(client: client)).startDirectChat(); if (roomId == null) { From a88460b9d5dd06168906c7d5c4c4b2bfd2f2acef Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 15 Jun 2020 10:39:26 +0200 Subject: [PATCH 57/64] also test storing a decrytped room udpate --- test/encryption/encrypt_decrypt_room_message_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/encryption/encrypt_decrypt_room_message_test.dart b/test/encryption/encrypt_decrypt_room_message_test.dart index 7227134..1a941f8 100644 --- a/test/encryption/encrypt_decrypt_room_message_test.dart +++ b/test/encryption/encrypt_decrypt_room_message_test.dart @@ -90,6 +90,7 @@ void main() { expect(decryptedEvent.type, 'm.room.message'); expect(decryptedEvent.content['msgtype'], 'm.text'); expect(decryptedEvent.content['text'], 'Hello foxies!'); + await client.encryption.decryptRoomEvent(roomId, encryptedEvent, store: true); }); test('dispose client', () async { From c4491fe97bdec8c0c29bc5e2a67cc7ebb7efedff Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 15 Jun 2020 10:42:10 +0200 Subject: [PATCH 58/64] fix --- test/encryption/encrypt_decrypt_room_message_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/encryption/encrypt_decrypt_room_message_test.dart b/test/encryption/encrypt_decrypt_room_message_test.dart index 1a941f8..d1abab3 100644 --- a/test/encryption/encrypt_decrypt_room_message_test.dart +++ b/test/encryption/encrypt_decrypt_room_message_test.dart @@ -84,6 +84,7 @@ void main() { room: room, originServerTs: now, eventId: '\$event', + senderId: '@alice:example.com', ); final decryptedEvent = await client.encryption.decryptRoomEvent(roomId, encryptedEvent); From e2c358f319363cbb7386da261b7a2d43ab93748f Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 15 Jun 2020 10:48:35 +0200 Subject: [PATCH 59/64] format --- test/encryption/encrypt_decrypt_room_message_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/encryption/encrypt_decrypt_room_message_test.dart b/test/encryption/encrypt_decrypt_room_message_test.dart index d1abab3..70d75fa 100644 --- a/test/encryption/encrypt_decrypt_room_message_test.dart +++ b/test/encryption/encrypt_decrypt_room_message_test.dart @@ -91,7 +91,8 @@ void main() { expect(decryptedEvent.type, 'm.room.message'); expect(decryptedEvent.content['msgtype'], 'm.text'); expect(decryptedEvent.content['text'], 'Hello foxies!'); - await client.encryption.decryptRoomEvent(roomId, encryptedEvent, store: true); + await client.encryption + .decryptRoomEvent(roomId, encryptedEvent, store: true); }); test('dispose client', () async { From c4d09268a0f37f5888e274b5f59ea586d3563218 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 15 Jun 2020 13:12:59 +0200 Subject: [PATCH 60/64] add key verification test --- lib/encryption/key_verification_manager.dart | 16 +- test/encryption/key_verification_test.dart | 459 ++++++++++++++++--- test/fake_matrix_api.dart | 9 + 3 files changed, 425 insertions(+), 59 deletions(-) diff --git a/lib/encryption/key_verification_manager.dart b/lib/encryption/key_verification_manager.dart index ba80770..de82074 100644 --- a/lib/encryption/key_verification_manager.dart +++ b/lib/encryption/key_verification_manager.dart @@ -99,16 +99,16 @@ class KeyVerificationManager { if (_requests.containsKey(transactionId)) { final req = _requests[transactionId]; + final otherDeviceId = event['content']['from_device']; if (event['sender'] != client.userID) { await req.handlePayload(type, event['content'], event['event_id']); - } else if (req.userId == client.userID && req.deviceId == null) { - // okay, maybe another of our devices answered - await req.handlePayload(type, event['content'], event['event_id']); - if (req.deviceId != client.deviceID) { - req.otherDeviceAccepted(); - req.dispose(); - _requests.remove(transactionId); - } + } else if (event['sender'] == client.userID && + otherDeviceId != null && + otherDeviceId != client.deviceID) { + // okay, another of our devices answered + req.otherDeviceAccepted(); + req.dispose(); + _requests.remove(transactionId); } } else if (event['sender'] != client.userID) { final room = client.getRoomById(update.roomID) ?? diff --git a/test/encryption/key_verification_test.dart b/test/encryption/key_verification_test.dart index 1a9ddc9..66ccdee 100644 --- a/test/encryption/key_verification_test.dart +++ b/test/encryption/key_verification_test.dart @@ -16,12 +16,47 @@ * along with this program. If not, see . */ +import 'dart:convert'; + import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/encryption.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; import '../fake_client.dart'; +import '../fake_matrix_api.dart'; + +class MockSSSS extends SSSS { + MockSSSS(Encryption encryption) : super(encryption); + + bool requestedSecrets = false; + @override + Future maybeRequestAll(List devices) async { + requestedSecrets = true; + final handle = open(); + handle.unlock(recoveryKey: SSSS_KEY); + await handle.maybeCacheAll(); + } +} + +EventUpdate getLastSentEvent(KeyVerification req) { + final entry = FakeMatrixApi.calledEndpoints.entries + .firstWhere((p) => p.key.contains('/send/')); + final type = entry.key.split('/')[6]; + final content = json.decode(entry.value.first); + return EventUpdate( + content: { + 'event_id': req.transactionId, + 'type': type, + 'content': content, + 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, + 'sender': req.client.userID, + }, + eventType: type, + type: 'timeline', + roomID: req.room.id, + ); +} void main() { /// All Tests related to the ChatTime @@ -38,70 +73,392 @@ void main() { if (!olmEnabled) return; - Client client; - Room room; - var updateCounter = 0; - KeyVerification keyVerification; + // key @othertest:fakeServer.notExisting + const otherPickledOlmAccount = + 'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA'; + + Client client1; + Client client2; test('setupClient', () async { - client = await getClient(); - room = Room(id: '!localpart:server.abc', client: client); - keyVerification = KeyVerification( - encryption: client.encryption, - room: room, - userId: '@alice:example.com', - deviceId: 'ABCD', - onUpdate: () => updateCounter++, + client1 = await getClient(); + client2 = + Client('othertestclient', debug: true, httpClient: FakeMatrixApi()); + client2.database = client1.database; + await client2.checkServer('https://fakeServer.notExisting'); + client2.connect( + newToken: 'abc', + newUserID: '@othertest:fakeServer.notExisting', + newHomeserver: client2.api.homeserver, + newDeviceName: 'Text Matrix Client', + newDeviceID: 'FOXDEVICE', + newOlmAccount: otherPickledOlmAccount, ); + await Future.delayed(Duration(milliseconds: 10)); + client1.verificationMethods = { + KeyVerificationMethod.emoji, + KeyVerificationMethod.numbers + }; + client2.verificationMethods = { + KeyVerificationMethod.emoji, + KeyVerificationMethod.numbers + }; }); - test('acceptSas', () async { - await keyVerification.acceptSas(); - }); - test('acceptVerification', () async { - await keyVerification.acceptVerification(); - }); - test('cancel', () async { - await keyVerification.cancel('m.cancelcode'); - expect(keyVerification.canceled, true); - expect(keyVerification.canceledCode, 'm.cancelcode'); - expect(keyVerification.canceledReason, null); - }); - test('handlePayload', () async { - await keyVerification.handlePayload('m.key.verification.request', { - 'from_device': 'AliceDevice2', - 'methods': ['m.sas.v1'], - 'timestamp': 1559598944869, - 'transaction_id': 'S0meUniqueAndOpaqueString' + test('Run emoji / number verification', () async { + // for a full run we test in-room verification in a cleartext room + // because then we can easily intercept the payloads and inject in the other client + FakeMatrixApi.calledEndpoints.clear(); + // make sure our master key is *not* verified to not triger SSSS for now + client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false); + final req1 = + await client1.userDeviceKeys[client2.userID].startVerification(); + var evt = getLastSentEvent(req1); + expect(req1.state, KeyVerificationState.waitingAccept); + + KeyVerification req2; + final sub = client2.onKeyVerificationRequest.stream.listen((req) { + req2 = req; }); - await keyVerification.handlePayload('m.key.verification.start', { - 'from_device': 'BobDevice1', - 'method': 'm.sas.v1', - 'transaction_id': 'S0meUniqueAndOpaqueString' + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + await Future.delayed(Duration(milliseconds: 10)); + await sub.cancel(); + expect(req2 != null, true); + + // send ready + FakeMatrixApi.calledEndpoints.clear(); + await req2.acceptVerification(); + evt = getLastSentEvent(req2); + expect(req2.state, KeyVerificationState.waitingAccept); + + // send start + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send accept + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // receive last key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + + // compare emoji + expect(req1.state, KeyVerificationState.askSas); + expect(req2.state, KeyVerificationState.askSas); + expect(req1.sasTypes[0], 'emoji'); + expect(req1.sasTypes[1], 'decimal'); + expect(req2.sasTypes[0], 'emoji'); + expect(req2.sasTypes[1], 'decimal'); + // compare emoji + final emoji1 = req1.sasEmojis; + final emoji2 = req2.sasEmojis; + for (var i = 0; i < 7; i++) { + expect(emoji1[i].emoji, emoji2[i].emoji); + expect(emoji1[i].name, emoji2[i].name); + } + // compare numbers + final numbers1 = req1.sasNumbers; + final numbers2 = req2.sasNumbers; + for (var i = 0; i < 3; i++) { + expect(numbers1[i], numbers2[i]); + } + + // alright, they match + + // send mac + FakeMatrixApi.calledEndpoints.clear(); + await req1.acceptSas(); + evt = getLastSentEvent(req1); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + expect(req1.state, KeyVerificationState.waitingSas); + + // send mac + FakeMatrixApi.calledEndpoints.clear(); + await req2.acceptSas(); + evt = getLastSentEvent(req2); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + + expect(req1.state, KeyVerificationState.done); + expect(req2.state, KeyVerificationState.done); + expect( + client1.userDeviceKeys[client2.userID].deviceKeys[client2.deviceID] + .directVerified, + true); + expect( + client2.userDeviceKeys[client1.userID].deviceKeys[client1.deviceID] + .directVerified, + true); + await client1.encryption.keyVerificationManager.cleanup(); + await client2.encryption.keyVerificationManager.cleanup(); + }); + + test('ask SSSS start', () async { + client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true); + await client1.database.clearSSSSCache(client1.id); + final req1 = + await client1.userDeviceKeys[client2.userID].startVerification(); + expect(req1.state, KeyVerificationState.askSSSS); + await req1.openSSSS(recoveryKey: SSSS_KEY); + await Future.delayed(Duration(milliseconds: 10)); + expect(req1.state, KeyVerificationState.waitingAccept); + + await req1.cancel(); + await client1.encryption.keyVerificationManager.cleanup(); + }); + + test('ask SSSS end', () async { + FakeMatrixApi.calledEndpoints.clear(); + // make sure our master key is *not* verified to not triger SSSS for now + client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false); + // the other one has to have their master key verified to trigger asking for ssss + client2.userDeviceKeys[client2.userID].masterKey.setDirectVerified(true); + final req1 = + await client1.userDeviceKeys[client2.userID].startVerification(); + var evt = getLastSentEvent(req1); + expect(req1.state, KeyVerificationState.waitingAccept); + + KeyVerification req2; + final sub = client2.onKeyVerificationRequest.stream.listen((req) { + req2 = req; }); - await keyVerification.handlePayload('m.key.verification.cancel', { - 'code': 'm.user', - 'reason': 'User rejected the key verification request', - 'transaction_id': 'S0meUniqueAndOpaqueString' + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + await Future.delayed(Duration(milliseconds: 10)); + await sub.cancel(); + expect(req2 != null, true); + + // send ready + FakeMatrixApi.calledEndpoints.clear(); + await req2.acceptVerification(); + evt = getLastSentEvent(req2); + expect(req2.state, KeyVerificationState.waitingAccept); + + // send start + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send accept + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // receive last key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + + // compare emoji + expect(req1.state, KeyVerificationState.askSas); + expect(req2.state, KeyVerificationState.askSas); + // compare emoji + final emoji1 = req1.sasEmojis; + final emoji2 = req2.sasEmojis; + for (var i = 0; i < 7; i++) { + expect(emoji1[i].emoji, emoji2[i].emoji); + expect(emoji1[i].name, emoji2[i].name); + } + // compare numbers + final numbers1 = req1.sasNumbers; + final numbers2 = req2.sasNumbers; + for (var i = 0; i < 3; i++) { + expect(numbers1[i], numbers2[i]); + } + + // alright, they match + client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true); + await client1.database.clearSSSSCache(client1.id); + + // send mac + FakeMatrixApi.calledEndpoints.clear(); + await req1.acceptSas(); + evt = getLastSentEvent(req1); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + expect(req1.state, KeyVerificationState.waitingSas); + + // send mac + FakeMatrixApi.calledEndpoints.clear(); + await req2.acceptSas(); + evt = getLastSentEvent(req2); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + + expect(req1.state, KeyVerificationState.askSSSS); + expect(req2.state, KeyVerificationState.done); + + await req1.openSSSS(recoveryKey: SSSS_KEY); + await Future.delayed(Duration(milliseconds: 10)); + expect(req1.state, KeyVerificationState.done); + + client1.encryption.ssss = MockSSSS(client1.encryption); + (client1.encryption.ssss as MockSSSS).requestedSecrets = false; + await client1.database.clearSSSSCache(client1.id); + await req1.maybeRequestSSSSSecrets(); + await Future.delayed(Duration(milliseconds: 10)); + expect((client1.encryption.ssss as MockSSSS).requestedSecrets, true); + // delay for 12 seconds to be sure no other tests clear the ssss cache + await Future.delayed(Duration(seconds: 12)); + + await client1.encryption.keyVerificationManager.cleanup(); + await client2.encryption.keyVerificationManager.cleanup(); + }); + + test('reject verification', () async { + FakeMatrixApi.calledEndpoints.clear(); + // make sure our master key is *not* verified to not triger SSSS for now + client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false); + final req1 = + await client1.userDeviceKeys[client2.userID].startVerification(); + var evt = getLastSentEvent(req1); + expect(req1.state, KeyVerificationState.waitingAccept); + + KeyVerification req2; + final sub = client2.onKeyVerificationRequest.stream.listen((req) { + req2 = req; }); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + await Future.delayed(Duration(milliseconds: 10)); + await sub.cancel(); + + FakeMatrixApi.calledEndpoints.clear(); + await req2.rejectVerification(); + evt = getLastSentEvent(req2); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + expect(req1.state, KeyVerificationState.error); + expect(req2.state, KeyVerificationState.error); + + await client1.encryption.keyVerificationManager.cleanup(); + await client2.encryption.keyVerificationManager.cleanup(); }); - test('rejectSas', () async { - await keyVerification.rejectSas(); + + test('reject sas', () async { + FakeMatrixApi.calledEndpoints.clear(); + // make sure our master key is *not* verified to not triger SSSS for now + client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false); + final req1 = + await client1.userDeviceKeys[client2.userID].startVerification(); + var evt = getLastSentEvent(req1); + expect(req1.state, KeyVerificationState.waitingAccept); + + KeyVerification req2; + final sub = client2.onKeyVerificationRequest.stream.listen((req) { + req2 = req; + }); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + await Future.delayed(Duration(milliseconds: 10)); + await sub.cancel(); + expect(req2 != null, true); + + // send ready + FakeMatrixApi.calledEndpoints.clear(); + await req2.acceptVerification(); + evt = getLastSentEvent(req2); + expect(req2.state, KeyVerificationState.waitingAccept); + + // send start + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send accept + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // receive last key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + + await req1.acceptSas(); + FakeMatrixApi.calledEndpoints.clear(); + await req2.rejectSas(); + evt = getLastSentEvent(req2); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + expect(req1.state, KeyVerificationState.error); + expect(req2.state, KeyVerificationState.error); + + await client1.encryption.keyVerificationManager.cleanup(); + await client2.encryption.keyVerificationManager.cleanup(); }); - test('rejectVerification', () async { - await keyVerification.rejectVerification(); - }); - test('start', () async { - await keyVerification.start(); - }); - test('verifyActivity', () async { - final verified = await keyVerification.verifyActivity(); - expect(verified, true); - keyVerification?.dispose(); + + test('other device accepted', () async { + FakeMatrixApi.calledEndpoints.clear(); + // make sure our master key is *not* verified to not triger SSSS for now + client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false); + final req1 = + await client1.userDeviceKeys[client2.userID].startVerification(); + var evt = getLastSentEvent(req1); + expect(req1.state, KeyVerificationState.waitingAccept); + + KeyVerification req2; + final sub = client2.onKeyVerificationRequest.stream.listen((req) { + req2 = req; + }); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + await Future.delayed(Duration(milliseconds: 10)); + await sub.cancel(); + expect(req2 != null, true); + + await client2.encryption.keyVerificationManager + .handleEventUpdate(EventUpdate( + content: { + 'event_id': req2.transactionId, + 'type': 'm.key.verification.ready', + 'content': { + 'methods': ['m.sas.v1'], + 'from_device': 'SOMEOTHERDEVICE', + 'm.relates_to': { + 'rel_type': 'm.reference', + 'event_id': req2.transactionId, + }, + }, + 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, + 'sender': client2.userID, + }, + eventType: 'm.key.verification.ready', + type: 'timeline', + roomID: req2.room.id, + )); + expect(req2.state, KeyVerificationState.error); + + await req2.cancel(); + await client1.encryption.keyVerificationManager.cleanup(); + await client2.encryption.keyVerificationManager.cleanup(); }); test('dispose client', () async { - await client.dispose(closeDatabase: true); + await client1.dispose(closeDatabase: true); + await client2.dispose(closeDatabase: true); }); }); } diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 4c02258..e51fca5 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -78,6 +78,11 @@ class FakeMatrixApi extends MockClient { action.contains('/state/m.room.member/')) { res = {'displayname': ''}; return Response(json.encode(res), 200); + } else if (method == 'PUT' && + action.contains( + '/client/r0/rooms/%211234%3AfakeServer.notExisting/send/')) { + res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'}; + return Response(json.encode(res), 200); } else { res = { 'errcode': 'M_UNRECOGNIZED', @@ -2008,6 +2013,10 @@ class FakeMatrixApi extends MockClient { (var req) => {}, '/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data': (var req) => {}, + '/client/r0/user/%40test%3AfakeServer.notExisting/account_data/m.direct': + (var req) => {}, + '/client/r0/user/%40othertest%3AfakeServer.notExisting/account_data/m.direct': + (var req) => {}, '/client/r0/profile/%40alice%3Aexample.com/displayname': (var reqI) => {}, '/client/r0/profile/%40alice%3Aexample.com/avatar_url': (var reqI) => {}, '/client/r0/profile/%40test%3AfakeServer.notExisting/avatar_url': From 18f8d0db63fe3197f7b621d907d144f54011fd37 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sun, 21 Jun 2020 21:38:26 +0200 Subject: [PATCH 61/64] add deviceDisplayName --- lib/src/utils/device_keys_list.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 1f279f4..dfa0f78 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -316,6 +316,8 @@ class DeviceKeys extends SignableKey { List algorithms; String get curve25519Key => keys['curve25519:$deviceId']; + String get deviceDisplayName => + unsigned != null ? unsigned['device_display_name'] : null; bool get isValid => userId != null && From dbcdb6883d8d1afcc67dc0521bda8b24774d160d Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sun, 21 Jun 2020 21:48:06 +0200 Subject: [PATCH 62/64] better set verified --- lib/src/utils/device_keys_list.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index dfa0f78..c8430b7 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -237,7 +237,7 @@ abstract class SignableKey extends MatrixSignableKey { return false; } - Future setVerified(bool newVerified, [bool sign = true]) { + void setVerified(bool newVerified, [bool sign = true]) { _verified = newVerified; if (newVerified && sign && @@ -246,7 +246,6 @@ abstract class SignableKey extends MatrixSignableKey { // sign the key! client.encryption.crossSigning.sign([this]); } - return Future.value(); } Future setBlocked(bool newBlocked); From 48c03865a25589ad17ab2d55a365186b4cf9a64f Mon Sep 17 00:00:00 2001 From: Sorunome Date: Tue, 23 Jun 2020 08:30:50 +0200 Subject: [PATCH 63/64] make auth_data just a json object --- lib/encryption/key_manager.dart | 9 ++--- lib/matrix_api/matrix_api.dart | 8 ++-- lib/matrix_api/model/room_keys_info.dart | 50 ++---------------------- test/matrix_api_test.dart | 8 ++-- 4 files changed, 15 insertions(+), 60 deletions(-) diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 9621112..d9b2a88 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -44,11 +44,11 @@ class KeyManager { final keyObj = olm.PkDecryption(); try { final info = await client.api.getRoomKeysBackup(); - if (!(info.authData is RoomKeysAuthDataV1Curve25519AesSha2)) { + if (info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2) { return false; } if (keyObj.init_with_private_key(base64.decode(secret)) == - (info.authData as RoomKeysAuthDataV1Curve25519AesSha2).publicKey) { + info.authData['public_key']) { _requestedSessionIds.clear(); return true; } @@ -340,9 +340,8 @@ class KeyManager { backupPubKey = decryption.init_with_private_key(privateKey); if (backupPubKey == null || - !(info.authData is RoomKeysAuthDataV1Curve25519AesSha2) || - (info.authData as RoomKeysAuthDataV1Curve25519AesSha2).publicKey != - backupPubKey) { + info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2 || + info.authData['public_key'] != backupPubKey) { return; } for (final roomEntry in keys.rooms.entries) { diff --git a/lib/matrix_api/matrix_api.dart b/lib/matrix_api/matrix_api.dart index f09c59b..d939fa7 100644 --- a/lib/matrix_api/matrix_api.dart +++ b/lib/matrix_api/matrix_api.dart @@ -2042,13 +2042,13 @@ class MatrixApi { /// Create room keys backup /// https://matrix.org/docs/spec/client_server/unstable#post-matrix-client-r0-room-keys-version Future createRoomKeysBackup( - RoomKeysAlgorithmType algorithm, RoomKeysAuthData authData) async { + RoomKeysAlgorithmType algorithm, Map authData) async { final ret = await request( RequestType.POST, '/client/unstable/room_keys/version', data: { 'algorithm': algorithm.algorithmString, - 'auth_data': authData.toJson(), + 'auth_data': authData, }, ); return ret['version']; @@ -2071,13 +2071,13 @@ class MatrixApi { /// Updates a room key backup /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-version-version Future updateRoomKeysBackup(String version, - RoomKeysAlgorithmType algorithm, RoomKeysAuthData authData) async { + RoomKeysAlgorithmType algorithm, Map authData) async { await request( RequestType.PUT, '/client/unstable/room_keys/version/${Uri.encodeComponent(version)}', data: { 'algorithm': algorithm.algorithmString, - 'auth_data': authData.toJson(), + 'auth_data': authData, 'version': version, }, ); diff --git a/lib/matrix_api/model/room_keys_info.dart b/lib/matrix_api/model/room_keys_info.dart index 42e2398..f7a2cfe 100644 --- a/lib/matrix_api/model/room_keys_info.dart +++ b/lib/matrix_api/model/room_keys_info.dart @@ -38,46 +38,9 @@ extension RoomKeysAlgorithmTypeExtension on RoomKeysAlgorithmType { } } -abstract class RoomKeysAuthData { - // This object is used for signing so we need the raw json too - Map _json; - - RoomKeysAuthData.fromJson(Map json) { - _json = json; - } - - Map toJson() { - return _json; - } -} - -class RoomKeysAuthDataV1Curve25519AesSha2 extends RoomKeysAuthData { - String publicKey; - Map> signatures; - - RoomKeysAuthDataV1Curve25519AesSha2.fromJson(Map json) - : super.fromJson(json) { - publicKey = json['public_key']; - signatures = json['signatures'] is Map - ? Map>.from((json['signatures'] as Map) - .map((k, v) => MapEntry(k, Map.from(v)))) - : null; - } - - @override - Map toJson() { - final data = super.toJson(); - data['public_key'] = publicKey; - if (signatures != null) { - data['signatures'] = signatures; - } - return data; - } -} - class RoomKeysVersionResponse { RoomKeysAlgorithmType algorithm; - RoomKeysAuthData authData; + Map authData; int count; String etag; String version; @@ -85,14 +48,7 @@ class RoomKeysVersionResponse { RoomKeysVersionResponse.fromJson(Map json) { algorithm = RoomKeysAlgorithmTypeExtension.fromAlgorithmString(json['algorithm']); - switch (algorithm) { - case RoomKeysAlgorithmType.v1Curve25519AesSha2: - authData = - RoomKeysAuthDataV1Curve25519AesSha2.fromJson(json['auth_data']); - break; - default: - authData = null; - } + authData = json['auth_data']; count = json['count']; etag = json['etag'].toString(); // synapse replies an int but docs say string? @@ -102,7 +58,7 @@ class RoomKeysVersionResponse { Map toJson() { final data = {}; data['algorithm'] = algorithm?.algorithmString; - data['auth_data'] = authData?.toJson(); + data['auth_data'] = authData; data['count'] = count; data['etag'] = etag; data['version'] = version; diff --git a/test/matrix_api_test.dart b/test/matrix_api_test.dart index d585b36..a165628 100644 --- a/test/matrix_api_test.dart +++ b/test/matrix_api_test.dart @@ -1596,10 +1596,10 @@ void main() { matrixApi.accessToken = '1234'; final algorithm = RoomKeysAlgorithmType.v1Curve25519AesSha2; - final authData = RoomKeysAuthDataV1Curve25519AesSha2.fromJson({ + final authData = { 'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM', 'signatures': {}, - }); + }; final ret = await matrixApi.createRoomKeysBackup(algorithm, authData); expect( FakeMatrixApi.api['POST'] @@ -1619,10 +1619,10 @@ void main() { matrixApi.accessToken = '1234'; final algorithm = RoomKeysAlgorithmType.v1Curve25519AesSha2; - final authData = RoomKeysAuthDataV1Curve25519AesSha2.fromJson({ + final authData = { 'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM', 'signatures': {}, - }); + }; await matrixApi.updateRoomKeysBackup('5', algorithm, authData); }); test('deleteRoomKeysBackup', () async { From 8de4b5acc01e103f037c9d824bc12a850bc0ab6c Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 25 Jun 2020 09:46:01 +0200 Subject: [PATCH 64/64] update link in comments --- lib/matrix_api/matrix_api.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matrix_api/matrix_api.dart b/lib/matrix_api/matrix_api.dart index d939fa7..d0d716d 100644 --- a/lib/matrix_api/matrix_api.dart +++ b/lib/matrix_api/matrix_api.dart @@ -1507,7 +1507,7 @@ class MatrixApi { } /// Uploads your own cross-signing keys. - /// https://12682-24998719-gh.circle-artifacts.com/0/scripts/gen/client_server/unstable.html#post-matrix-client-r0-keys-device-signing-upload + /// https://github.com/matrix-org/matrix-doc/pull/2536 Future uploadDeviceSigningKeys({ MatrixCrossSigningKey masterKey, MatrixCrossSigningKey selfSigningKey, @@ -1525,7 +1525,7 @@ class MatrixApi { } /// Uploads new signatures of keys - /// https://12682-24998719-gh.circle-artifacts.com/0/scripts/gen/client_server/unstable.html#post-matrix-client-r0-keys-signatures-upload + /// https://github.com/matrix-org/matrix-doc/pull/2536 Future uploadKeySignatures( List keys) async { final payload = {};