feature: Upload to online key backup

This commit is contained in:
Sorunome 2020-08-17 14:25:48 +02:00
parent 8899f4c677
commit 99d536b14f
No known key found for this signature in database
GPG key ID: B19471D07FC9BE9C
18 changed files with 375 additions and 38 deletions

View file

@ -63,6 +63,8 @@ class Encryption {
Future<void> init(String olmAccount) async { Future<void> init(String olmAccount) async {
await olmManager.init(olmAccount); await olmManager.init(olmAccount);
_backgroundTasksRunning = true;
_backgroundTasks(); // start the background tasks
} }
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) { void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
@ -307,10 +309,25 @@ class Encryption {
return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload); return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload);
} }
// this method is responsible for all background tasks, such as uploading online key backups
bool _backgroundTasksRunning = true;
void _backgroundTasks() {
if (!_backgroundTasksRunning) {
return;
}
keyManager.backgroundTasks();
if (_backgroundTasksRunning) {
Timer(Duration(seconds: 10), _backgroundTasks);
}
}
void dispose() { void dispose() {
keyManager.dispose(); keyManager.dispose();
olmManager.dispose(); olmManager.dispose();
keyVerificationManager.dispose(); keyVerificationManager.dispose();
_backgroundTasksRunning = false;
} }
} }

View file

@ -26,7 +26,9 @@ import './utils/outbound_group_session.dart';
import './utils/session_key.dart'; import './utils/session_key.dart';
import '../famedlysdk.dart'; import '../famedlysdk.dart';
import '../matrix_api.dart'; import '../matrix_api.dart';
import '../src/database/database.dart';
import '../src/utils/logs.dart'; import '../src/utils/logs.dart';
import '../src/utils/run_in_background.dart';
const MEGOLM_KEY = 'm.megolm_backup.v1'; const MEGOLM_KEY = 'm.megolm_backup.v1';
@ -71,18 +73,14 @@ class KeyManager {
void setInboundGroupSession(String roomId, String sessionId, String senderKey, void setInboundGroupSession(String roomId, String sessionId, String senderKey,
Map<String, dynamic> content, Map<String, dynamic> content,
{bool forwarded = false, Map<String, String> senderClaimedKeys}) { {bool forwarded = false,
Map<String, String> senderClaimedKeys,
bool uploaded = false}) {
senderClaimedKeys ??= <String, String>{}; senderClaimedKeys ??= <String, String>{};
if (!senderClaimedKeys.containsKey('ed25519')) { if (!senderClaimedKeys.containsKey('ed25519')) {
DeviceKeys device; final device = client.getUserDeviceKeysByCurve25519Key(senderKey);
for (final user in client.userDeviceKeys.values) { if (device != null) {
device = user.deviceKeys.values.firstWhere( senderClaimedKeys['ed25519'] = device.ed25519Key;
(e) => e.curve25519Key == senderKey,
orElse: () => null);
if (device != null) {
senderClaimedKeys['ed25519'] = device.ed25519Key;
break;
}
} }
} }
final oldSession = final oldSession =
@ -109,6 +107,8 @@ class KeyManager {
content: content, content: content,
inboundGroupSession: inboundGroupSession, inboundGroupSession: inboundGroupSession,
indexes: {}, indexes: {},
roomId: roomId,
sessionId: sessionId,
key: client.userID, key: client.userID,
senderKey: senderKey, senderKey: senderKey,
senderClaimedKeys: senderClaimedKeys, senderClaimedKeys: senderClaimedKeys,
@ -132,7 +132,8 @@ class KeyManager {
_inboundGroupSessions[roomId] = <String, SessionKey>{}; _inboundGroupSessions[roomId] = <String, SessionKey>{};
} }
_inboundGroupSessions[roomId][sessionId] = newSession; _inboundGroupSessions[roomId][sessionId] = newSession;
client.database?.storeInboundGroupSession( client.database
?.storeInboundGroupSession(
client.id, client.id,
roomId, roomId,
sessionId, sessionId,
@ -141,9 +142,13 @@ class KeyManager {
json.encode({}), json.encode({}),
senderKey, senderKey,
json.encode(senderClaimedKeys), json.encode(senderClaimedKeys),
); )
// Note to self: When adding key-backup that needs to be unawaited(), else ?.then((_) {
// we might accidentally end up with http requests inside of the sync loop if (uploaded) {
client.database
.markInboundGroupSessionAsUploaded(client.id, roomId, sessionId);
}
});
// TODO: somehow try to decrypt last message again // TODO: somehow try to decrypt last message again
final room = client.getRoomById(roomId); final room = client.getRoomById(roomId);
if (room != null) { if (room != null) {
@ -410,7 +415,8 @@ class KeyManager {
forwarded: true, forwarded: true,
senderClaimedKeys: decrypted['sender_claimed_keys'] != null senderClaimedKeys: decrypted['sender_claimed_keys'] != null
? Map<String, String>.from(decrypted['sender_claimed_keys']) ? Map<String, String>.from(decrypted['sender_claimed_keys'])
: null); : null,
uploaded: true);
} }
} }
} }
@ -492,6 +498,79 @@ class KeyManager {
} }
} }
bool _isUploadingKeys = false;
Future<void> backgroundTasks() async {
if (_isUploadingKeys || client.database == null) {
return;
}
_isUploadingKeys = true;
try {
if (!(await isCached())) {
return; // we can't backup anyways
}
final dbSessions =
await client.database.getInboundGroupSessionsToUpload().get();
if (dbSessions.isEmpty) {
return; // nothing to do
}
final privateKey =
base64.decode(await encryption.ssss.getCached(MEGOLM_KEY));
// decryption is needed to calculate the public key and thus see if the claimed information is in fact valid
final decryption = olm.PkDecryption();
final info = await client.getRoomKeysBackup();
String backupPubKey;
try {
backupPubKey = decryption.init_with_private_key(privateKey);
if (backupPubKey == null ||
info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2 ||
info.authData['public_key'] != backupPubKey) {
return;
}
final args = _GenerateUploadKeysArgs(
pubkey: backupPubKey,
dbSessions: <_DbInboundGroupSessionBundle>[],
userId: client.userID,
);
// we need to calculate verified beforehand, as else we pass a closure to an isolate
// with 500 keys they do, however, noticably block the UI, which is why we give brief async suspentions in here
// so that the event loop can progress
var i = 0;
for (final dbSession in dbSessions) {
final device =
client.getUserDeviceKeysByCurve25519Key(dbSession.senderKey);
args.dbSessions.add(_DbInboundGroupSessionBundle(
dbSession: dbSession,
verified: device?.verified ?? false,
));
i++;
if (i > 10) {
await Future.delayed(Duration(milliseconds: 1));
i = 0;
}
}
final roomKeys =
await runInBackground<RoomKeys, _GenerateUploadKeysArgs>(
_generateUploadKeys, args);
Logs.info('[Key Manager] Uploading ${dbSessions.length} room keys...');
// upload the payload...
await client.storeRoomKeys(info.version, roomKeys);
// and now finally mark all the keys as uploaded
// no need to optimze this, as we only run it so seldomly and almost never with many keys at once
for (final dbSession in dbSessions) {
await client.database.markInboundGroupSessionAsUploaded(
client.id, dbSession.roomId, dbSession.sessionId);
}
} finally {
decryption.free();
}
} catch (e, s) {
Logs.error('[Key Manager] Error uploading room keys: ' + e.toString(), s);
} finally {
_isUploadingKeys = false;
}
}
/// Handle an incoming to_device event that is related to key sharing /// Handle an incoming to_device event that is related to key sharing
Future<void> handleToDeviceEvent(ToDeviceEvent event) async { Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
if (event.type == 'm.room_key_request') { if (event.type == 'm.room_key_request') {
@ -725,3 +804,67 @@ class RoomKeyRequest extends ToDeviceEvent {
keyManager.incomingShareRequests.remove(request.requestId); keyManager.incomingShareRequests.remove(request.requestId);
} }
} }
RoomKeys _generateUploadKeys(_GenerateUploadKeysArgs args) {
final enc = olm.PkEncryption();
try {
enc.set_recipient_key(args.pubkey);
// first we generate the payload to upload all the session keys in this chunk
final roomKeys = RoomKeys();
for (final dbSession in args.dbSessions) {
final sess = SessionKey.fromDb(dbSession.dbSession, args.userId);
if (!sess.isValid) {
continue;
}
// create the room if it doesn't exist
if (!roomKeys.rooms.containsKey(sess.roomId)) {
roomKeys.rooms[sess.roomId] = RoomKeysRoom();
}
// generate the encrypted content
final payload = <String, dynamic>{
'algorithm': 'm.megolm.v1.aes-sha2',
'forwarding_curve25519_key_chain': sess.forwardingCurve25519KeyChain,
'sender_key': sess.senderKey,
'sender_clencaimed_keys': sess.senderClaimedKeys,
'session_key': sess.inboundGroupSession
.export_session(sess.inboundGroupSession.first_known_index()),
};
// encrypt the content
final encrypted = enc.encrypt(json.encode(payload));
// fetch the device, if available...
//final device = args.client.getUserDeviceKeysByCurve25519Key(sess.senderKey);
// aaaand finally add the session key to our payload
roomKeys.rooms[sess.roomId].sessions[sess.sessionId] = RoomKeysSingleKey(
firstMessageIndex: sess.inboundGroupSession.first_known_index(),
forwardedCount: sess.forwardingCurve25519KeyChain.length,
isVerified: dbSession.verified, //device?.verified ?? false,
sessionData: {
'ephemeral': encrypted.ephemeral,
'ciphertext': encrypted.ciphertext,
'mac': encrypted.mac,
},
);
}
return roomKeys;
} catch (e, s) {
Logs.error('[Key Manager] Error generating payload ' + e.toString(), s);
rethrow;
} finally {
enc.free();
}
}
class _DbInboundGroupSessionBundle {
_DbInboundGroupSessionBundle({this.dbSession, this.verified});
DbInboundGroupSession dbSession;
bool verified;
}
class _GenerateUploadKeysArgs {
_GenerateUploadKeysArgs({this.pubkey, this.dbSessions, this.userId});
String pubkey;
List<_DbInboundGroupSessionBundle> dbSessions;
String userId;
}

View file

@ -27,6 +27,7 @@ import 'package:password_hash/password_hash.dart';
import '../famedlysdk.dart'; import '../famedlysdk.dart';
import '../matrix_api.dart'; import '../matrix_api.dart';
import '../src/database/database.dart';
import '../src/utils/logs.dart'; import '../src/utils/logs.dart';
import 'encryption.dart'; import 'encryption.dart';
@ -48,8 +49,15 @@ class SSSS {
Client get client => encryption.client; Client get client => encryption.client;
final pendingShareRequests = <String, _ShareRequest>{}; final pendingShareRequests = <String, _ShareRequest>{};
final _validators = <String, Future<bool> Function(String)>{}; final _validators = <String, Future<bool> Function(String)>{};
final Map<String, DbSSSSCache> _cache = <String, DbSSSSCache>{};
SSSS(this.encryption); SSSS(this.encryption);
// for testing
Future<void> clearCache() async {
await client.database?.clearSSSSCache(client.id);
_cache.clear();
}
static _DerivedKeys deriveKeys(Uint8List key, String name) { static _DerivedKeys deriveKeys(Uint8List key, String name) {
final zerosalt = Uint8List(8); final zerosalt = Uint8List(8);
final prk = Hmac(sha256, zerosalt).convert(key); final prk = Hmac(sha256, zerosalt).convert(key);
@ -173,16 +181,22 @@ class SSSS {
if (client.database == null) { if (client.database == null) {
return null; return null;
} }
// check if it is still valid
final keys = keyIdsFromType(type);
final isValid = (dbEntry) =>
keys.contains(dbEntry.keyId) &&
client.accountData[type].content['encrypted'][dbEntry.keyId]
['ciphertext'] ==
dbEntry.ciphertext;
if (_cache.containsKey(type) && isValid(_cache[type])) {
return _cache[type].content;
}
final ret = await client.database.getSSSSCache(client.id, type); final ret = await client.database.getSSSSCache(client.id, type);
if (ret == null) { if (ret == null) {
return null; return null;
} }
// check if it is still valid if (isValid(ret)) {
final keys = keyIdsFromType(type); _cache[type] = ret;
if (keys.contains(ret.keyId) &&
client.accountData[type].content['encrypted'][ret.keyId]
['ciphertext'] ==
ret.ciphertext) {
return ret.content; return ret.content;
} }
return null; return null;

View file

@ -37,12 +37,16 @@ class SessionKey {
Map<String, String> senderClaimedKeys; Map<String, String> senderClaimedKeys;
String senderKey; String senderKey;
bool get isValid => inboundGroupSession != null; bool get isValid => inboundGroupSession != null;
String roomId;
String sessionId;
SessionKey( SessionKey(
{this.content, {this.content,
this.inboundGroupSession, this.inboundGroupSession,
this.key, this.key,
this.indexes, this.indexes,
this.roomId,
this.sessionId,
String senderKey, String senderKey,
Map<String, String> senderClaimedKeys}) { Map<String, String> senderClaimedKeys}) {
_setSenderKey(senderKey); _setSenderKey(senderKey);
@ -59,6 +63,8 @@ class SessionKey {
indexes = parsedIndexes != null indexes = parsedIndexes != null
? Map<String, int>.from(parsedIndexes) ? Map<String, int>.from(parsedIndexes)
: <String, int>{}; : <String, int>{};
roomId = dbEntry.roomId;
sessionId = dbEntry.sessionId;
_setSenderKey(dbEntry.senderKey); _setSenderKey(dbEntry.senderKey);
_setSenderClaimedKeys(Map<String, String>.from(parsedSenderClaimedKeys)); _setSenderClaimedKeys(Map<String, String>.from(parsedSenderClaimedKeys));

View file

@ -2062,7 +2062,7 @@ class MatrixApi {
return RoomKeysRoom.fromJson(ret); return RoomKeysRoom.fromJson(ret);
} }
/// Deletes room ekys for a room /// Deletes room keys for a room
/// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid /// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid
Future<RoomKeysUpdateResponse> deleteRoomKeysRoom( Future<RoomKeysUpdateResponse> deleteRoomKeysRoom(
String roomId, String version) async { String roomId, String version) async {

View file

@ -22,6 +22,12 @@ class RoomKeysSingleKey {
bool isVerified; bool isVerified;
Map<String, dynamic> sessionData; Map<String, dynamic> sessionData;
RoomKeysSingleKey(
{this.firstMessageIndex,
this.forwardedCount,
this.isVerified,
this.sessionData});
RoomKeysSingleKey.fromJson(Map<String, dynamic> json) { RoomKeysSingleKey.fromJson(Map<String, dynamic> json) {
firstMessageIndex = json['first_message_index']; firstMessageIndex = json['first_message_index'];
forwardedCount = json['forwarded_count']; forwardedCount = json['forwarded_count'];
@ -42,6 +48,10 @@ class RoomKeysSingleKey {
class RoomKeysRoom { class RoomKeysRoom {
Map<String, RoomKeysSingleKey> sessions; Map<String, RoomKeysSingleKey> sessions;
RoomKeysRoom({this.sessions}) {
sessions ??= <String, RoomKeysSingleKey>{};
}
RoomKeysRoom.fromJson(Map<String, dynamic> json) { RoomKeysRoom.fromJson(Map<String, dynamic> json) {
sessions = (json['sessions'] as Map) sessions = (json['sessions'] as Map)
.map((k, v) => MapEntry(k, RoomKeysSingleKey.fromJson(v))); .map((k, v) => MapEntry(k, RoomKeysSingleKey.fromJson(v)));
@ -57,6 +67,10 @@ class RoomKeysRoom {
class RoomKeys { class RoomKeys {
Map<String, RoomKeysRoom> rooms; Map<String, RoomKeysRoom> rooms;
RoomKeys({this.rooms}) {
rooms ??= <String, RoomKeysRoom>{};
}
RoomKeys.fromJson(Map<String, dynamic> json) { RoomKeys.fromJson(Map<String, dynamic> json) {
rooms = (json['rooms'] as Map) rooms = (json['rooms'] as Map)
.map((k, v) => MapEntry(k, RoomKeysRoom.fromJson(v))); .map((k, v) => MapEntry(k, RoomKeysRoom.fromJson(v)));

View file

@ -615,6 +615,7 @@ class Client extends MatrixApi {
return; return;
} }
encryption?.dispose();
encryption = encryption =
Encryption(client: this, enableE2eeRecovery: enableE2eeRecovery); Encryption(client: this, enableE2eeRecovery: enableE2eeRecovery);
await encryption.init(olmAccount); await encryption.init(olmAccount);
@ -1165,6 +1166,18 @@ class Client extends MatrixApi {
Map<String, DeviceKeysList> get userDeviceKeys => _userDeviceKeys; Map<String, DeviceKeysList> get userDeviceKeys => _userDeviceKeys;
Map<String, DeviceKeysList> _userDeviceKeys = {}; Map<String, DeviceKeysList> _userDeviceKeys = {};
/// Gets user device keys by its curve25519 key. Returns null if it isn't found
DeviceKeys getUserDeviceKeysByCurve25519Key(String senderKey) {
for (final user in userDeviceKeys.values) {
final device = user.deviceKeys.values
.firstWhere((e) => e.curve25519Key == senderKey, orElse: () => null);
if (device != null) {
return device;
}
}
return null;
}
Future<Set<String>> _getUserIdsInEncryptedRooms() async { Future<Set<String>> _getUserIdsInEncryptedRooms() async {
var userIds = <String>{}; var userIds = <String>{};
for (var i = 0; i < rooms.length; i++) { for (var i = 0; i < rooms.length; i++) {
@ -1493,6 +1506,8 @@ class Client extends MatrixApi {
} }
if (closeDatabase) await database?.close(); if (closeDatabase) await database?.close();
database = null; database = null;
encryption?.dispose();
encryption = null;
return; return;
} }
} }

View file

@ -5873,6 +5873,27 @@ abstract class _$Database extends GeneratedDatabase {
); );
} }
Selectable<DbInboundGroupSession> getInboundGroupSessionsToUpload() {
return customSelect(
'SELECT * FROM inbound_group_sessions WHERE uploaded = false LIMIT 500',
variables: [],
readsFrom: {inboundGroupSessions}).map(_rowToDbInboundGroupSession);
}
Future<int> markInboundGroupSessionAsUploaded(
int client_id, String room_id, String session_id) {
return customUpdate(
'UPDATE inbound_group_sessions SET uploaded = true WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id',
variables: [
Variable.withInt(client_id),
Variable.withString(room_id),
Variable.withString(session_id)
],
updates: {inboundGroupSessions},
updateKind: UpdateKind.update,
);
}
Future<int> storeUserDeviceKeysInfo( Future<int> storeUserDeviceKeysInfo(
int client_id, String user_id, bool outdated) { int client_id, String user_id, bool outdated) {
return customInsert( return customInsert(

View file

@ -191,6 +191,8 @@ dbGetInboundGroupSessionKeys: SELECT * FROM inbound_group_sessions WHERE client_
getAllInboundGroupSessions: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id; getAllInboundGroupSessions: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id;
storeInboundGroupSession: INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes, sender_key, sender_claimed_keys) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes, :sender_key, :sender_claimed_keys); storeInboundGroupSession: INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes, sender_key, sender_claimed_keys) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes, :sender_key, :sender_claimed_keys);
updateInboundGroupSessionIndexes: UPDATE inbound_group_sessions SET indexes = :indexes WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id; updateInboundGroupSessionIndexes: UPDATE inbound_group_sessions SET indexes = :indexes WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
getInboundGroupSessionsToUpload: SELECT * FROM inbound_group_sessions WHERE uploaded = false LIMIT 500;
markInboundGroupSessionAsUploaded: UPDATE inbound_group_sessions SET uploaded = true WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
storeUserDeviceKeysInfo: INSERT OR REPLACE INTO user_device_keys (client_id, user_id, outdated) VALUES (:client_id, :user_id, :outdated); 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; 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; setBlockedUserDeviceKey: UPDATE user_device_keys_key SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
import 'package:ansicolor/ansicolor.dart'; import 'package:ansicolor/ansicolor.dart';
abstract class Logs { abstract class Logs {

View file

@ -0,0 +1,30 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import 'package:isolate/isolate.dart';
import 'dart:async';
Future<T> runInBackground<T, U>(
FutureOr<T> Function(U arg) function, U arg) async {
final isolate = await IsolateRunner.spawn();
try {
return await isolate.run(function, arg);
} finally {
await isolate.close();
}
}

View file

@ -281,6 +281,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.4" version: "0.3.4"
isolate:
dependency: "direct main"
description:
name: isolate
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -609,7 +616,7 @@ packages:
name: test_coverage name: test_coverage
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.1" version: "0.4.3"
timing: timing:
dependency: transitive dependency: transitive
description: description:

View file

@ -22,10 +22,11 @@ dependencies:
olm: ^1.2.1 olm: ^1.2.1
matrix_file_e2ee: ^1.0.4 matrix_file_e2ee: ^1.0.4
ansicolor: ^1.0.2 ansicolor: ^1.0.2
isolate: ^2.0.3
dev_dependencies: dev_dependencies:
test: ^1.0.0 test: ^1.0.0
test_coverage: ^0.4.1 test_coverage: ^0.4.3
moor_generator: ^3.0.0 moor_generator: ^3.0.0
build_runner: ^1.5.2 build_runner: ^1.5.2
pedantic: ^1.9.0 pedantic: ^1.9.0

View file

@ -1,6 +1,6 @@
#!/bin/sh -e #!/bin/sh -e
pub run test -p vm # pub run test -p vm
pub run test_coverage pub run test_coverage --print-test-output
pub global activate remove_from_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 genhtml -o coverage coverage/lcov.info || true

View file

@ -207,7 +207,7 @@ void main() {
test('ask SSSS start', () async { test('ask SSSS start', () async {
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true); client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
await client1.database.clearSSSSCache(client1.id); await client1.encryption.ssss.clearCache();
final req1 = final req1 =
await client1.userDeviceKeys[client2.userID].startVerification(); await client1.userDeviceKeys[client2.userID].startVerification();
expect(req1.state, KeyVerificationState.askSSSS); expect(req1.state, KeyVerificationState.askSSSS);
@ -288,7 +288,7 @@ void main() {
// alright, they match // alright, they match
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true); client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
await client1.database.clearSSSSCache(client1.id); await client1.encryption.ssss.clearCache();
// send mac // send mac
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
@ -312,7 +312,7 @@ void main() {
client1.encryption.ssss = MockSSSS(client1.encryption); client1.encryption.ssss = MockSSSS(client1.encryption);
(client1.encryption.ssss as MockSSSS).requestedSecrets = false; (client1.encryption.ssss as MockSSSS).requestedSecrets = false;
await client1.database.clearSSSSCache(client1.id); await client1.encryption.ssss.clearCache();
await req1.maybeRequestSSSSSecrets(); await req1.maybeRequestSSSSSecrets();
await Future.delayed(Duration(milliseconds: 10)); await Future.delayed(Duration(milliseconds: 10));
expect((client1.encryption.ssss as MockSSSS).requestedSecrets, true); expect((client1.encryption.ssss as MockSSSS).requestedSecrets, true);

View file

@ -16,12 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/utils/logs.dart'; import 'package:famedlysdk/src/utils/logs.dart';
import 'package:famedlysdk/matrix_api.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
import '../fake_client.dart'; import '../fake_client.dart';
import '../fake_matrix_api.dart';
void main() { void main() {
group('Online Key Backup', () { group('Online Key Backup', () {
@ -67,6 +71,49 @@ void main() {
true); true);
}); });
test('upload key', () async {
final session = olm.OutboundGroupSession();
session.create();
final inbound = olm.InboundGroupSession();
inbound.create(session.session_key());
final senderKey = client.identityKey;
final roomId = '!someroom:example.org';
final sessionId = inbound.session_id();
// set a payload...
var sessionPayload = <String, dynamic>{
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': roomId,
'forwarding_curve25519_key_chain': [client.identityKey],
'session_id': sessionId,
'session_key': inbound.export_session(1),
'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey,
};
FakeMatrixApi.calledEndpoints.clear();
client.encryption.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload,
forwarded: true);
var dbSessions =
await client.database.getInboundGroupSessionsToUpload().get();
expect(dbSessions.isNotEmpty, true);
await client.encryption.keyManager.backgroundTasks();
final payload = FakeMatrixApi
.calledEndpoints['/client/unstable/room_keys/keys?version=5'].first;
dbSessions =
await client.database.getInboundGroupSessionsToUpload().get();
expect(dbSessions.isEmpty, true);
final onlineKeys = RoomKeys.fromJson(json.decode(payload));
client.encryption.keyManager.clearInboundGroupSessions();
var ret = client.encryption.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey);
expect(ret, null);
await client.encryption.keyManager.loadFromResponse(onlineKeys);
ret = client.encryption.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey);
expect(ret != null, true);
});
test('dispose client', () async { test('dispose client', () async {
await client.dispose(closeDatabase: true); await client.dispose(closeDatabase: true);
}); });

View file

@ -248,7 +248,7 @@ void main() {
client.encryption.ssss.open('m.cross_signing.self_signing'); client.encryption.ssss.open('m.cross_signing.self_signing');
handle.unlock(recoveryKey: SSSS_KEY); handle.unlock(recoveryKey: SSSS_KEY);
await client.database.clearSSSSCache(client.id); await client.encryption.ssss.clearCache();
client.encryption.ssss.pendingShareRequests.clear(); client.encryption.ssss.pendingShareRequests.clear();
await client.encryption.ssss.request('best animal', [key]); await client.encryption.ssss.request('best animal', [key]);
var event = ToDeviceEvent( var event = ToDeviceEvent(
@ -272,7 +272,7 @@ void main() {
'm.megolm_backup.v1' 'm.megolm_backup.v1'
]) { ]) {
final secret = await handle.getStored(type); final secret = await handle.getStored(type);
await client.database.clearSSSSCache(client.id); await client.encryption.ssss.clearCache();
client.encryption.ssss.pendingShareRequests.clear(); client.encryption.ssss.pendingShareRequests.clear();
await client.encryption.ssss.request(type, [key]); await client.encryption.ssss.request(type, [key]);
event = ToDeviceEvent( event = ToDeviceEvent(
@ -294,7 +294,7 @@ void main() {
// test different fail scenarios // test different fail scenarios
// not encrypted // not encrypted
await client.database.clearSSSSCache(client.id); await client.encryption.ssss.clearCache();
client.encryption.ssss.pendingShareRequests.clear(); client.encryption.ssss.pendingShareRequests.clear();
await client.encryption.ssss.request('best animal', [key]); await client.encryption.ssss.request('best animal', [key]);
event = ToDeviceEvent( event = ToDeviceEvent(
@ -309,7 +309,7 @@ void main() {
expect(await client.encryption.ssss.getCached('best animal'), null); expect(await client.encryption.ssss.getCached('best animal'), null);
// unknown request id // unknown request id
await client.database.clearSSSSCache(client.id); await client.encryption.ssss.clearCache();
client.encryption.ssss.pendingShareRequests.clear(); client.encryption.ssss.pendingShareRequests.clear();
await client.encryption.ssss.request('best animal', [key]); await client.encryption.ssss.request('best animal', [key]);
event = ToDeviceEvent( event = ToDeviceEvent(
@ -327,7 +327,7 @@ void main() {
expect(await client.encryption.ssss.getCached('best animal'), null); expect(await client.encryption.ssss.getCached('best animal'), null);
// not from a device we sent the request to // not from a device we sent the request to
await client.database.clearSSSSCache(client.id); await client.encryption.ssss.clearCache();
client.encryption.ssss.pendingShareRequests.clear(); client.encryption.ssss.pendingShareRequests.clear();
await client.encryption.ssss.request('best animal', [key]); await client.encryption.ssss.request('best animal', [key]);
event = ToDeviceEvent( event = ToDeviceEvent(
@ -345,7 +345,7 @@ void main() {
expect(await client.encryption.ssss.getCached('best animal'), null); expect(await client.encryption.ssss.getCached('best animal'), null);
// secret not a string // secret not a string
await client.database.clearSSSSCache(client.id); await client.encryption.ssss.clearCache();
client.encryption.ssss.pendingShareRequests.clear(); client.encryption.ssss.pendingShareRequests.clear();
await client.encryption.ssss.request('best animal', [key]); await client.encryption.ssss.request('best animal', [key]);
event = ToDeviceEvent( event = ToDeviceEvent(
@ -363,7 +363,7 @@ void main() {
expect(await client.encryption.ssss.getCached('best animal'), null); expect(await client.encryption.ssss.getCached('best animal'), null);
// validator doesn't check out // validator doesn't check out
await client.database.clearSSSSCache(client.id); await client.encryption.ssss.clearCache();
client.encryption.ssss.pendingShareRequests.clear(); client.encryption.ssss.pendingShareRequests.clear();
await client.encryption.ssss.request('m.megolm_backup.v1', [key]); await client.encryption.ssss.request('m.megolm_backup.v1', [key]);
event = ToDeviceEvent( event = ToDeviceEvent(
@ -386,7 +386,7 @@ void main() {
final key = final key =
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
key.setDirectVerified(true); key.setDirectVerified(true);
await client.database.clearSSSSCache(client.id); await client.encryption.ssss.clearCache();
client.encryption.ssss.pendingShareRequests.clear(); client.encryption.ssss.pendingShareRequests.clear();
await client.encryption.ssss.maybeRequestAll([key]); await client.encryption.ssss.maybeRequestAll([key]);
expect(client.encryption.ssss.pendingShareRequests.length, 3); expect(client.encryption.ssss.pendingShareRequests.length, 3);

View file

@ -132,6 +132,8 @@ void main() {
await client.checkServer('https://fakeserver.notexisting'); await client.checkServer('https://fakeserver.notexisting');
expect(user1.canChangePowerLevel, false); expect(user1.canChangePowerLevel, false);
}); });
client.dispose(); test('dispose client', () async {
await client.dispose();
});
}); });
} }