Merge branch 'soru/cross-signing' into 'master'
Cross-Signing See merge request famedly/famedlysdk!319
This commit is contained in:
commit
5dda0c3623
|
@ -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';
|
||||
|
|
183
lib/encryption/cross_signing.dart
Normal file
183
lib/encryption/cross_signing.dart
Normal file
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* 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 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:famedlysdk/famedlysdk.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 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)) ==
|
||||
client.userDeviceKeys[client.userID].selfSigningKey.ed25519Key;
|
||||
} catch (_) {
|
||||
return false;
|
||||
} finally {
|
||||
keyObj.free();
|
||||
}
|
||||
});
|
||||
encryption.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 &&
|
||||
client.accountData[USER_SIGNING_KEY] != null &&
|
||||
client.accountData[MASTER_KEY] != null;
|
||||
|
||||
Future<bool> isCached() async {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
return (await encryption.ssss.getCached(SELF_SIGNING_KEY)) != null &&
|
||||
(await encryption.ssss.getCached(USER_SIGNING_KEY)) != null;
|
||||
}
|
||||
|
||||
Future<void> selfSign({String passphrase, String recoveryKey}) async {
|
||||
final handle = encryption.ssss.open(MASTER_KEY);
|
||||
await handle.unlock(passphrase: passphrase, 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
|
||||
await masterKey.setVerified(true, false);
|
||||
// and now sign both our own key and our master key
|
||||
await sign([
|
||||
masterKey,
|
||||
client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]
|
||||
]);
|
||||
}
|
||||
|
||||
bool signable(List<SignableKey> 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<void> sign(List<SignableKey> keys) async {
|
||||
Uint8List selfSigningKey;
|
||||
Uint8List userSigningKey;
|
||||
final signedKeys = <MatrixSignableKey>[];
|
||||
final addSignature =
|
||||
(SignableKey key, SignableKey signedWith, String signature) {
|
||||
if (key == null || signedWith == null || signature == null) {
|
||||
return;
|
||||
}
|
||||
final signedKey = key.cloneForSigning();
|
||||
signedKey.signatures[signedWith.userId] = <String, String>{};
|
||||
signedKey.signatures[signedWith.userId]
|
||||
['ed25519:${signedWith.identifier}'] = signature;
|
||||
signedKeys.add(signedKey);
|
||||
};
|
||||
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 =
|
||||
encryption.olmManager.signString(key.signingContent);
|
||||
addSignature(
|
||||
key,
|
||||
client
|
||||
.userDeviceKeys[client.userID].deviceKeys[client.deviceID],
|
||||
signature);
|
||||
}
|
||||
// we don't care about signing other cross-signing keys
|
||||
} else {
|
||||
// okay, we'll sign a device key with our self signing key
|
||||
selfSigningKey ??= base64
|
||||
.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);
|
||||
}
|
||||
}
|
||||
} 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) ?? '');
|
||||
if (userSigningKey.isNotEmpty) {
|
||||
final signature = _sign(key.signingContent, userSigningKey);
|
||||
addSignature(key, client.userDeviceKeys[client.userID].userSigningKey,
|
||||
signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (signedKeys.isNotEmpty) {
|
||||
// post our new keys!
|
||||
await client.api.uploadKeySignatures(signedKeys);
|
||||
}
|
||||
}
|
||||
|
||||
String _sign(String canonicalJson, Uint8List key) {
|
||||
final keyObj = olm.PkSigning();
|
||||
try {
|
||||
keyObj.init_with_seed(key);
|
||||
return keyObj.sign(canonicalJson);
|
||||
} finally {
|
||||
keyObj.free();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<void> init(String olmAccount) async {
|
||||
|
@ -77,6 +83,24 @@ 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<void> 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<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
|
||||
|
|
|
@ -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,29 @@ class KeyManager {
|
|||
final Set<String> _loadedOutboundGroupSessions = <String>{};
|
||||
final Set<String> _requestedSessionIds = <String>{};
|
||||
|
||||
KeyManager(this.encryption);
|
||||
KeyManager(this.encryption) {
|
||||
encryption.ssss.setValidator(MEGOLM_KEY, (String secret) async {
|
||||
final keyObj = olm.PkDecryption();
|
||||
try {
|
||||
final info = await client.api.getRoomKeysBackup();
|
||||
if (info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2) {
|
||||
return false;
|
||||
}
|
||||
if (keyObj.init_with_private_key(base64.decode(secret)) ==
|
||||
info.authData['public_key']) {
|
||||
_requestedSessionIds.clear();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} 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() {
|
||||
|
@ -296,8 +320,101 @@ class KeyManager {
|
|||
_outboundGroupSessions[roomId] = sess;
|
||||
}
|
||||
|
||||
Future<bool> isCached() async {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
return (await encryption.ssss.getCached(MEGOLM_KEY)) != null;
|
||||
}
|
||||
|
||||
Future<void> loadFromResponse(RoomKeys keys) async {
|
||||
if (!(await isCached())) {
|
||||
return;
|
||||
}
|
||||
final privateKey =
|
||||
base64.decode(await encryption.ssss.getCached(MEGOLM_KEY));
|
||||
final decryption = olm.PkDecryption();
|
||||
final info = await client.api.getRoomKeysBackup();
|
||||
String backupPubKey;
|
||||
try {
|
||||
backupPubKey = decryption.init_with_private_key(privateKey);
|
||||
|
||||
if (backupPubKey == null ||
|
||||
info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2 ||
|
||||
info.authData['public_key'] != backupPubKey) {
|
||||
return;
|
||||
}
|
||||
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 ||
|
||||
!(sessionData is Map)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, dynamic> 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<void> loadSingleKey(String roomId, String sessionId) async {
|
||||
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.toJson(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await loadFromResponse(keys);
|
||||
}
|
||||
|
||||
/// Request a certain key from another device
|
||||
Future<void> request(Room room, String sessionId, String senderKey) async {
|
||||
Future<void> 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
|
||||
final devices = await room.getUserDeviceKeys();
|
||||
|
@ -336,22 +453,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'];
|
||||
|
@ -359,6 +481,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(
|
||||
|
@ -369,6 +492,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;
|
||||
|
@ -377,9 +501,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
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ class KeyVerificationManager {
|
|||
final Map<String, KeyVerification> _requests = {};
|
||||
|
||||
Future<void> cleanup() async {
|
||||
final Set entriesToDispose = <String>{};
|
||||
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) {
|
||||
|
@ -51,7 +55,8 @@ class KeyVerificationManager {
|
|||
}
|
||||
|
||||
Future<void> 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 +80,54 @@ class KeyVerificationManager {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> 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];
|
||||
final otherDeviceId = event['content']['from_device'];
|
||||
if (event['sender'] != client.userID) {
|
||||
await req.handlePayload(type, event['content'], event['event_id']);
|
||||
} 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) ??
|
||||
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();
|
||||
|
|
|
@ -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<String, dynamic> signedJson,
|
||||
String userId, String deviceId) {
|
||||
|
|
483
lib/encryption/ssss.dart
Normal file
483
lib/encryption/ssss.dart
Normal file
|
@ -0,0 +1,483 @@
|
|||
/*
|
||||
* 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 '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:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
|
||||
import 'encryption.dart';
|
||||
|
||||
const CACHE_TYPES = <String>[
|
||||
'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 = Base58Codec(BASE58_ALPHABET);
|
||||
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
||||
const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm
|
||||
|
||||
class SSSS {
|
||||
final Encryption encryption;
|
||||
Client get client => encryption.client;
|
||||
final pendingShareRequests = <String, _ShareRequest>{};
|
||||
final _validators = <String, Future<bool> Function(String)>{};
|
||||
SSSS(this.encryption);
|
||||
|
||||
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 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);
|
||||
|
||||
final plain = Uint8List.fromList(utf8.encode(data));
|
||||
final ciphertext = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null)
|
||||
.encrypt(plain, iv: IV(iv))
|
||||
.bytes;
|
||||
|
||||
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 cipher = base64.decode(data.ciphertext);
|
||||
final hmac = base64
|
||||
.encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes)
|
||||
.replaceAll(RegExp(r'=+$'), '');
|
||||
if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) {
|
||||
throw 'Bad MAC';
|
||||
}
|
||||
final decipher = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null)
|
||||
.decrypt(Encrypted(cipher), iv: IV(base64.decode(data.iv)));
|
||||
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 keyFromPassphrase(String passphrase, _PassphraseInfo info) {
|
||||
if (info.algorithm != 'm.pbkdf2') {
|
||||
throw 'Unknown algorithm';
|
||||
}
|
||||
final generator = PBKDF2(hashAlgorithm: sha512);
|
||||
return Uint8List.fromList(generator.generateKey(passphrase, info.salt,
|
||||
info.iterations, info.bits != null ? info.bits / 8 : 32));
|
||||
}
|
||||
|
||||
void setValidator(String type, Future<bool> 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)) {
|
||||
return null;
|
||||
}
|
||||
return keyData.content['key'];
|
||||
}
|
||||
|
||||
BasicEvent getKey(String keyId) {
|
||||
return client.accountData['m.secret_storage.key.${keyId}'];
|
||||
}
|
||||
|
||||
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)) {
|
||||
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<String> 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) &&
|
||||
client.accountData[type].content['encrypted'][ret.keyId]
|
||||
['ciphertext'] ==
|
||||
ret.ciphertext) {
|
||||
return ret.content;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> 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, enc['ciphertext'], decrypted);
|
||||
}
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
Future<void> store(
|
||||
String type, String secret, String keyId, Uint8List key) async {
|
||||
final encrypted = encryptAes(secret, key, type);
|
||||
final content = <String, dynamic>{
|
||||
'encrypted': <String, dynamic>{},
|
||||
};
|
||||
content['encrypted'][keyId] = <String, dynamic>{
|
||||
'iv': encrypted.iv,
|
||||
'ciphertext': encrypted.ciphertext,
|
||||
'mac': encrypted.mac,
|
||||
};
|
||||
// store the thing in your account data
|
||||
await client.api.setAccountData(client.userID, type, content);
|
||||
if (CACHE_TYPES.contains(type) && client.database != null) {
|
||||
// cache the thing
|
||||
await client.database
|
||||
.storeSSSSCache(client.id, type, keyId, encrypted.ciphertext, secret);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> maybeCacheAll(String keyId, Uint8List key) async {
|
||||
for (final type in CACHE_TYPES) {
|
||||
final secret = await getCached(type);
|
||||
if (secret == null) {
|
||||
try {
|
||||
await getStored(type, keyId, key);
|
||||
} catch (_) {
|
||||
// the entry wasn't stored, just ignore it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> maybeRequestAll(List<DeviceKeys> devices) async {
|
||||
for (final type in CACHE_TYPES) {
|
||||
final secret = await getCached(type);
|
||||
if (secret == null) {
|
||||
await request(type, devices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> request(String type, List<DeviceKeys> 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 = client.generateUniqueTransactionId();
|
||||
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<void> 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']) ||
|
||||
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, 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
|
||||
}
|
||||
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
|
||||
}
|
||||
print('[SSSS] Secret for type ${request.type} is ok, storing it');
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> keyIdsFromType(String type) {
|
||||
final data = client.accountData[type];
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
if (data.content['encrypted'] is Map) {
|
||||
final Set keys = <String>{};
|
||||
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]) {
|
||||
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<DeviceKeys> 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 _PassphraseInfo {
|
||||
final String algorithm;
|
||||
final String salt;
|
||||
final int iterations;
|
||||
final int bits;
|
||||
|
||||
_PassphraseInfo({this.algorithm, this.salt, this.iterations, this.bits});
|
||||
}
|
||||
|
||||
class OpenSSSS {
|
||||
final SSSS ssss;
|
||||
final String keyId;
|
||||
final BasicEvent keyData;
|
||||
OpenSSSS({this.ssss, this.keyId, this.keyData});
|
||||
Uint8List privateKey;
|
||||
|
||||
bool get isUnlocked => privateKey != null;
|
||||
|
||||
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'],
|
||||
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<String> getStored(String type) async {
|
||||
return await ssss.getStored(type, keyId, privateKey);
|
||||
}
|
||||
|
||||
Future<void> store(String type, String secret) async {
|
||||
await ssss.store(type, secret, keyId, privateKey);
|
||||
}
|
||||
|
||||
Future<void> maybeCacheAll() async {
|
||||
await ssss.maybeCacheAll(keyId, privateKey);
|
||||
}
|
||||
}
|
|
@ -16,9 +16,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
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 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
|
@ -63,6 +64,7 @@ import '../encryption.dart';
|
|||
|
||||
enum KeyVerificationState {
|
||||
askAccept,
|
||||
askSSSS,
|
||||
waitingAccept,
|
||||
askSas,
|
||||
waitingSas,
|
||||
|
@ -70,6 +72,8 @@ enum KeyVerificationState {
|
|||
error
|
||||
}
|
||||
|
||||
enum KeyVerificationMethod { emoji, numbers }
|
||||
|
||||
List<String> _intersect(List<String> a, List<dynamic> b) {
|
||||
if (b == null || a == null) {
|
||||
return [];
|
||||
|
@ -103,11 +107,9 @@ List<int> _bytesToInt(Uint8List bytes, int totalBits) {
|
|||
return ret;
|
||||
}
|
||||
|
||||
final VERIFICATION_METHODS = [_KeyVerificationMethodSas.type];
|
||||
|
||||
_KeyVerificationMethod _makeVerificationMethod(
|
||||
String type, KeyVerification request) {
|
||||
if (type == _KeyVerificationMethodSas.type) {
|
||||
if (type == 'm.sas.v1') {
|
||||
return _KeyVerificationMethodSas(request: request);
|
||||
}
|
||||
throw 'Unkown method type';
|
||||
|
@ -126,6 +128,8 @@ class KeyVerification {
|
|||
_KeyVerificationMethod method;
|
||||
List<String> possibleMethods;
|
||||
Map<String, dynamic> startPaylaod;
|
||||
String _nextAction;
|
||||
List<SignableKey> _verifiedDevices;
|
||||
|
||||
DateTime lastActivity;
|
||||
String lastStep;
|
||||
|
@ -157,22 +161,44 @@ class KeyVerification {
|
|||
: null);
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
if (room == null) {
|
||||
transactionId = randomString(512);
|
||||
List<String> get knownVerificationMethods {
|
||||
final methods = <String>[];
|
||||
if (client.verificationMethods.contains(KeyVerificationMethod.numbers) ||
|
||||
client.verificationMethods.contains(KeyVerificationMethod.emoji)) {
|
||||
methods.add('m.sas.v1');
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
Future<void> sendStart() async {
|
||||
await send('m.key.verification.request', {
|
||||
'methods': VERIFICATION_METHODS,
|
||||
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
'methods': knownVerificationMethods,
|
||||
if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
});
|
||||
startedVerification = true;
|
||||
setState(KeyVerificationState.waitingAccept);
|
||||
lastActivity = DateTime.now();
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
if (room == null) {
|
||||
transactionId = client.generateUniqueTransactionId();
|
||||
}
|
||||
if (encryption.crossSigning.enabled &&
|
||||
!(await encryption.crossSigning.isCached()) &&
|
||||
!client.isUnknownSession) {
|
||||
setState(KeyVerificationState.askSSSS);
|
||||
_nextAction = 'request';
|
||||
} else {
|
||||
await sendStart();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handlePayload(String type, Map<String, dynamic> payload,
|
||||
[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'];
|
||||
|
@ -188,7 +214,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');
|
||||
|
@ -197,13 +223,17 @@ class KeyVerification {
|
|||
setState(KeyVerificationState.askAccept);
|
||||
break;
|
||||
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');
|
||||
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();
|
||||
|
@ -212,10 +242,33 @@ class KeyVerification {
|
|||
case 'm.key.verification.start':
|
||||
_deviceId ??= payload['from_device'];
|
||||
transactionId ??= eventId ?? payload['transaction_id'];
|
||||
if (method != null) {
|
||||
// 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
|
||||
return;
|
||||
} else {
|
||||
// the other start won, let's hand off
|
||||
startedVerification = false; // it is now as if they started
|
||||
thisLastStep = 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
|
||||
}
|
||||
if (!VERIFICATION_METHODS.contains(payload['method'])) {
|
||||
if (!knownVerificationMethods.contains(payload['method'])) {
|
||||
await cancel('m.unknown_method');
|
||||
return;
|
||||
}
|
||||
|
@ -228,6 +281,7 @@ class KeyVerification {
|
|||
startPaylaod = payload;
|
||||
setState(KeyVerificationState.askAccept);
|
||||
} else {
|
||||
print('handling start in method.....');
|
||||
await method.handlePayload(type, payload);
|
||||
}
|
||||
break;
|
||||
|
@ -244,7 +298,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);
|
||||
|
@ -254,6 +310,36 @@ class KeyVerification {
|
|||
}
|
||||
}
|
||||
|
||||
void otherDeviceAccepted() {
|
||||
canceled = true;
|
||||
canceledCode = 'm.accepted';
|
||||
canceledReason = 'm.accepted';
|
||||
setState(KeyVerificationState.error);
|
||||
}
|
||||
|
||||
Future<void> openSSSS(
|
||||
{String passphrase, 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
|
||||
encryption.crossSigning.sign(_verifiedDevices);
|
||||
}
|
||||
setState(KeyVerificationState.done);
|
||||
}
|
||||
};
|
||||
if (skip) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
final handle = encryption.ssss.open('m.cross_signing.user_signing');
|
||||
await handle.unlock(passphrase: passphrase, recoveryKey: recoveryKey);
|
||||
await handle.maybeCacheAll();
|
||||
next();
|
||||
}
|
||||
|
||||
/// called when the user accepts an incoming verification
|
||||
Future<void> acceptVerification() async {
|
||||
if (!(await verifyLastStep(
|
||||
|
@ -318,9 +404,29 @@ class KeyVerification {
|
|||
return [];
|
||||
}
|
||||
|
||||
Future<void> maybeRequestSSSSSecrets([int i = 0]) async {
|
||||
final requestInterval = <int>[10, 60];
|
||||
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
|
||||
return;
|
||||
}
|
||||
unawaited(encryption.ssss
|
||||
.maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList()));
|
||||
if (requestInterval.length <= i) {
|
||||
return;
|
||||
}
|
||||
Timer(Duration(seconds: requestInterval[i]),
|
||||
() => maybeRequestSSSSSecrets(i + 1));
|
||||
}
|
||||
|
||||
Future<void> verifyKeys(Map<String, String> keys,
|
||||
Future<bool> Function(String, DeviceKeys) verifier) async {
|
||||
final verifiedDevices = <String>[];
|
||||
Future<bool> Function(String, SignableKey) verifier) async {
|
||||
_verifiedDevices = <SignableKey>[];
|
||||
|
||||
if (!client.userDeviceKeys.containsKey(userId)) {
|
||||
await cancel('m.key_mismatch');
|
||||
|
@ -330,23 +436,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 {
|
||||
// TODO: we would check here if what we are verifying is actually a
|
||||
// cross-signing key and not a "normal" device key
|
||||
_verifiedDevices.add(key);
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
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
|
||||
if (key is CrossSigningKey && key.usage.contains('master')) {
|
||||
verifiedMasterKey = true;
|
||||
}
|
||||
}
|
||||
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(maybeRequestSSSSSecrets());
|
||||
}
|
||||
await send('m.key.verification.done', {});
|
||||
|
||||
var askingSSSS = false;
|
||||
if (encryption.crossSigning.enabled &&
|
||||
encryption.crossSigning.signable(_verifiedDevices)) {
|
||||
// these keys can be signed! Let's do so
|
||||
if (await encryption.crossSigning.isCached()) {
|
||||
// and now let's sign them all in the background
|
||||
unawaited(encryption.crossSigning.sign(_verifiedDevices));
|
||||
} else if (!wasUnknownSession) {
|
||||
askingSSSS = true;
|
||||
}
|
||||
}
|
||||
if (askingSSSS) {
|
||||
setState(KeyVerificationState.askSSSS);
|
||||
_nextAction = 'done';
|
||||
} else {
|
||||
setState(KeyVerificationState.done);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -439,6 +570,9 @@ abstract class _KeyVerificationMethod {
|
|||
return false;
|
||||
}
|
||||
|
||||
String _type;
|
||||
String get type => _type;
|
||||
|
||||
Future<void> sendStart();
|
||||
void dispose() {}
|
||||
}
|
||||
|
@ -446,13 +580,13 @@ 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})
|
||||
: super(request: request);
|
||||
|
||||
static String type = 'm.sas.v1';
|
||||
@override
|
||||
final _type = 'm.sas.v1';
|
||||
|
||||
String keyAgreementProtocol;
|
||||
String hash;
|
||||
|
@ -469,6 +603,19 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
sas?.free();
|
||||
}
|
||||
|
||||
List<String> get knownAuthentificationTypes {
|
||||
final types = <String>[];
|
||||
if (request.client.verificationMethods
|
||||
.contains(KeyVerificationMethod.emoji)) {
|
||||
types.add('emoji');
|
||||
}
|
||||
if (request.client.verificationMethods
|
||||
.contains(KeyVerificationMethod.numbers)) {
|
||||
types.add('decimal');
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
|
||||
try {
|
||||
|
@ -550,7 +697,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
|
||||
|
@ -582,7 +729,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;
|
||||
}
|
||||
|
@ -620,7 +767,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;
|
||||
}
|
||||
|
@ -690,6 +837,17 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
_calculateMac(encryption.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', {
|
||||
|
@ -725,15 +883,10 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
mac[entry.key] = entry.value;
|
||||
}
|
||||
}
|
||||
await request.verifyKeys(mac, (String mac, DeviceKeys device) async {
|
||||
await request.verifyKeys(mac, (String mac, SignableKey key) async {
|
||||
return mac ==
|
||||
_calculateMac(
|
||||
device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId);
|
||||
_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) {
|
||||
|
|
|
@ -31,8 +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_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';
|
||||
|
@ -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';
|
||||
|
@ -58,6 +60,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';
|
||||
|
|
|
@ -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';
|
||||
|
@ -49,11 +49,14 @@ 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';
|
||||
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 +1506,55 @@ class MatrixApi {
|
|||
return DeviceListsUpdate.fromJson(response);
|
||||
}
|
||||
|
||||
/// Uploads your own cross-signing keys.
|
||||
/// https://github.com/matrix-org/matrix-doc/pull/2536
|
||||
Future<void> 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://github.com/matrix-org/matrix-doc/pull/2536
|
||||
Future<UploadKeySignaturesResponse> uploadKeySignatures(
|
||||
List<MatrixSignableKey> keys) async {
|
||||
final payload = <String, dynamic>{};
|
||||
for (final key in keys) {
|
||||
if (key.identifier == null ||
|
||||
key.signatures == null ||
|
||||
key.signatures.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (!payload.containsKey(key.userId)) {
|
||||
payload[key.userId] = <String, dynamic>{};
|
||||
}
|
||||
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<List<Pusher>> requestPushers() async {
|
||||
|
@ -1986,4 +2038,156 @@ class MatrixApi {
|
|||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Create room keys backup
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#post-matrix-client-r0-room-keys-version
|
||||
Future<String> createRoomKeysBackup(
|
||||
RoomKeysAlgorithmType algorithm, Map<String, dynamic> authData) async {
|
||||
final ret = await request(
|
||||
RequestType.POST,
|
||||
'/client/unstable/room_keys/version',
|
||||
data: {
|
||||
'algorithm': algorithm.algorithmString,
|
||||
'auth_data': authData,
|
||||
},
|
||||
);
|
||||
return ret['version'];
|
||||
}
|
||||
|
||||
/// Gets a room key backup
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-version
|
||||
Future<RoomKeysVersionResponse> 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<void> updateRoomKeysBackup(String version,
|
||||
RoomKeysAlgorithmType algorithm, Map<String, dynamic> authData) async {
|
||||
await request(
|
||||
RequestType.PUT,
|
||||
'/client/unstable/room_keys/version/${Uri.encodeComponent(version)}',
|
||||
data: {
|
||||
'algorithm': algorithm.algorithmString,
|
||||
'auth_data': authData,
|
||||
'version': version,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a room key backup
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-version-version
|
||||
Future<void> 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<RoomKeysUpdateResponse> 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<RoomKeysSingleKey> 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<RoomKeysUpdateResponse> 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<RoomKeysUpdateResponse> 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<RoomKeysRoom> 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<RoomKeysUpdateResponse> 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<RoomKeysUpdateResponse> 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<RoomKeys> 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<RoomKeysUpdateResponse> deleteRoomKeys(String version) async {
|
||||
final ret = await request(
|
||||
RequestType.DELETE,
|
||||
'/client/unstable/room_keys/keys?version=${Uri.encodeComponent(version)}',
|
||||
);
|
||||
return RoomKeysUpdateResponse.fromJson(ret);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'matrix_device_keys.dart';
|
||||
import 'matrix_keys.dart';
|
||||
|
||||
class KeysQueryResponse {
|
||||
Map<String, dynamic> failures;
|
||||
Map<String, Map<String, MatrixDeviceKeys>> deviceKeys;
|
||||
Map<String, MatrixCrossSigningKey> masterKeys;
|
||||
Map<String, MatrixCrossSigningKey> selfSigningKeys;
|
||||
Map<String, MatrixCrossSigningKey> userSigningKeys;
|
||||
|
||||
KeysQueryResponse.fromJson(Map<String, dynamic> json) {
|
||||
failures = Map<String, dynamic>.from(json['failures']);
|
||||
|
@ -37,6 +40,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<String, dynamic> toJson() {
|
||||
|
@ -57,6 +86,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,38 +16,28 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
class MatrixDeviceKeys {
|
||||
class MatrixSignableKey {
|
||||
String userId;
|
||||
String deviceId;
|
||||
List<String> algorithms;
|
||||
String identifier;
|
||||
Map<String, String> keys;
|
||||
Map<String, Map<String, String>> signatures;
|
||||
Map<String, dynamic> unsigned;
|
||||
String get deviceDisplayName =>
|
||||
unsigned != null ? unsigned['device_display_name'] : null;
|
||||
|
||||
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<String, dynamic> _json;
|
||||
|
||||
MatrixDeviceKeys(
|
||||
this.userId,
|
||||
this.deviceId,
|
||||
this.algorithms,
|
||||
this.keys,
|
||||
this.signatures, {
|
||||
this.unsigned,
|
||||
});
|
||||
|
||||
MatrixDeviceKeys.fromJson(Map<String, dynamic> json) {
|
||||
MatrixSignableKey.fromJson(Map<String, dynamic> json) {
|
||||
_json = json;
|
||||
userId = json['user_id'];
|
||||
deviceId = json['device_id'];
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
keys = Map<String, String>.from(json['keys']);
|
||||
signatures = Map<String, Map<String, String>>.from(
|
||||
(json['signatures'] as Map)
|
||||
.map((k, v) => MapEntry(k, Map<String, String>.from(v))));
|
||||
unsigned = json['unsigned'] != null
|
||||
signatures = json['signatures'] is Map
|
||||
? Map<String, Map<String, String>>.from((json['signatures'] as Map)
|
||||
.map((k, v) => MapEntry(k, Map<String, String>.from(v))))
|
||||
: null;
|
||||
unsigned = json['unsigned'] is Map
|
||||
? Map<String, dynamic>.from(json['unsigned'])
|
||||
: null;
|
||||
}
|
||||
|
@ -55,8 +45,6 @@ class MatrixDeviceKeys {
|
|||
Map<String, dynamic> toJson() {
|
||||
final data = _json ?? <String, dynamic>{};
|
||||
data['user_id'] = userId;
|
||||
data['device_id'] = deviceId;
|
||||
data['algorithms'] = algorithms;
|
||||
data['keys'] = keys;
|
||||
|
||||
if (signatures != null) {
|
||||
|
@ -68,3 +56,60 @@ class MatrixDeviceKeys {
|
|||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixCrossSigningKey extends MatrixSignableKey {
|
||||
List<String> usage;
|
||||
String get publicKey => identifier;
|
||||
|
||||
MatrixCrossSigningKey(
|
||||
String userId,
|
||||
this.usage,
|
||||
Map<String, String> keys,
|
||||
Map<String, Map<String, String>> signatures, {
|
||||
Map<String, dynamic> unsigned,
|
||||
}) : super(userId, keys?.values?.first, keys, signatures, unsigned: unsigned);
|
||||
|
||||
@override
|
||||
MatrixCrossSigningKey.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json) {
|
||||
usage = List<String>.from(json['usage']);
|
||||
identifier = keys?.values?.first;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = super.toJson();
|
||||
data['usage'] = usage;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixDeviceKeys extends MatrixSignableKey {
|
||||
String get deviceId => identifier;
|
||||
List<String> algorithms;
|
||||
String get deviceDisplayName =>
|
||||
unsigned != null ? unsigned['device_display_name'] : null;
|
||||
|
||||
MatrixDeviceKeys(
|
||||
String userId,
|
||||
String deviceId,
|
||||
this.algorithms,
|
||||
Map<String, String> keys,
|
||||
Map<String, Map<String, String>> signatures, {
|
||||
Map<String, dynamic> unsigned,
|
||||
}) : super(userId, deviceId, keys, signatures, unsigned: unsigned);
|
||||
|
||||
@override
|
||||
MatrixDeviceKeys.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
|
||||
identifier = json['device_id'];
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = super.toJson();
|
||||
data['device_id'] = deviceId;
|
||||
data['algorithms'] = algorithms;
|
||||
return data;
|
||||
}
|
||||
}
|
67
lib/matrix_api/model/room_keys_info.dart
Normal file
67
lib/matrix_api/model/room_keys_info.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RoomKeysVersionResponse {
|
||||
RoomKeysAlgorithmType algorithm;
|
||||
Map<String, dynamic> authData;
|
||||
int count;
|
||||
String etag;
|
||||
String version;
|
||||
|
||||
RoomKeysVersionResponse.fromJson(Map<String, dynamic> json) {
|
||||
algorithm =
|
||||
RoomKeysAlgorithmTypeExtension.fromAlgorithmString(json['algorithm']);
|
||||
authData = json['auth_data'];
|
||||
count = json['count'];
|
||||
etag =
|
||||
json['etag'].toString(); // synapse replies an int but docs say string?
|
||||
version = json['version'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['algorithm'] = algorithm?.algorithmString;
|
||||
data['auth_data'] = authData;
|
||||
data['count'] = count;
|
||||
data['etag'] = etag;
|
||||
data['version'] = version;
|
||||
return data;
|
||||
}
|
||||
}
|
87
lib/matrix_api/model/room_keys_keys.dart
Normal file
87
lib/matrix_api/model/room_keys_keys.dart
Normal file
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
class RoomKeysSingleKey {
|
||||
int firstMessageIndex;
|
||||
int forwardedCount;
|
||||
bool isVerified;
|
||||
Map<String, dynamic> sessionData;
|
||||
|
||||
RoomKeysSingleKey.fromJson(Map<String, dynamic> json) {
|
||||
firstMessageIndex = json['first_message_index'];
|
||||
forwardedCount = json['forwarded_count'];
|
||||
isVerified = json['is_verified'];
|
||||
sessionData = json['session_data'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['first_message_index'] = firstMessageIndex;
|
||||
data['forwarded_count'] = forwardedCount;
|
||||
data['is_verified'] = isVerified;
|
||||
data['session_data'] = sessionData;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomKeysRoom {
|
||||
Map<String, RoomKeysSingleKey> sessions;
|
||||
|
||||
RoomKeysRoom.fromJson(Map<String, dynamic> json) {
|
||||
sessions = (json['sessions'] as Map)
|
||||
.map((k, v) => MapEntry(k, RoomKeysSingleKey.fromJson(v)));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['sessions'] = sessions.map((k, v) => MapEntry(k, v.toJson()));
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomKeys {
|
||||
Map<String, RoomKeysRoom> rooms;
|
||||
|
||||
RoomKeys.fromJson(Map<String, dynamic> json) {
|
||||
rooms = (json['rooms'] as Map)
|
||||
.map((k, v) => MapEntry(k, RoomKeysRoom.fromJson(v)));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['rooms'] = rooms.map((k, v) => MapEntry(k, v.toJson()));
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomKeysUpdateResponse {
|
||||
String etag;
|
||||
int count;
|
||||
|
||||
RoomKeysUpdateResponse.fromJson(Map<String, dynamic> json) {
|
||||
etag = json['etag']; // synapse replies an int but docs say string?
|
||||
count = json['count'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['etag'] = etag;
|
||||
data['count'] = count;
|
||||
return data;
|
||||
}
|
||||
}
|
55
lib/matrix_api/model/upload_key_signatures_response.dart
Normal file
55
lib/matrix_api/model/upload_key_signatures_response.dart
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'matrix_exception.dart';
|
||||
|
||||
class UploadKeySignaturesResponse {
|
||||
Map<String, Map<String, MatrixException>> failures;
|
||||
|
||||
UploadKeySignaturesResponse.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
if (failures != null) {
|
||||
data['failures'] = failures.map(
|
||||
(k, v) => MapEntry(
|
||||
k,
|
||||
v.map(
|
||||
(k, v) => MapEntry(
|
||||
k,
|
||||
v.raw,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -56,16 +56,23 @@ class Client {
|
|||
|
||||
Encryption encryption;
|
||||
|
||||
Set<KeyVerificationMethod> 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.verificationMethods,
|
||||
http.Client httpClient}) {
|
||||
verificationMethods ??= <KeyVerificationMethod>{};
|
||||
api = MatrixApi(debug: debug, httpClient: httpClient);
|
||||
onLoginStateChanged.stream.listen((loginState) {
|
||||
if (debug) {
|
||||
|
@ -111,6 +118,12 @@ class Client {
|
|||
String get identityKey => encryption?.identityKey ?? '';
|
||||
String get fingerprintKey => encryption?.fingerprintKey ?? '';
|
||||
|
||||
/// Wheather this session is unknown to others
|
||||
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<Room> newList) {
|
||||
print('Warning! This endpoint is for testing only!');
|
||||
|
@ -627,7 +640,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);
|
||||
|
@ -953,6 +966,9 @@ class Client {
|
|||
await database.storeEventUpdate(id, update);
|
||||
}
|
||||
_updateRoomsByEventUpdate(update);
|
||||
if (encryptionEnabled) {
|
||||
await encryption.handleEventUpdate(update);
|
||||
}
|
||||
onEvent.add(update);
|
||||
|
||||
if (event['type'] == 'm.call.invite') {
|
||||
|
@ -1124,7 +1140,7 @@ class Client {
|
|||
var outdatedLists = <String, dynamic>{};
|
||||
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) {
|
||||
|
@ -1140,7 +1156,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<String, DeviceKeys>.from(_userDeviceKeys[userId].deviceKeys);
|
||||
|
@ -1149,34 +1165,45 @@ 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.verified = true;
|
||||
entry.setDirectVerified(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,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
_userDeviceKeys[userId].deviceKeys[deviceId] = oldKeys[deviceId];
|
||||
}
|
||||
if (database != null) {
|
||||
dbActions.add(() => database.storeUserDeviceKey(
|
||||
id,
|
||||
userId,
|
||||
deviceId,
|
||||
json.encode(entry.toJson()),
|
||||
entry.directVerified,
|
||||
entry.blocked,
|
||||
));
|
||||
}
|
||||
}
|
||||
// delete old/unused entries
|
||||
if (database != null) {
|
||||
for (final oldDeviceKeyEntry in oldKeys.entries) {
|
||||
final deviceId = oldDeviceKeyEntry.key;
|
||||
|
@ -1193,6 +1220,71 @@ class Client {
|
|||
.add(() => database.storeUserDeviceKeysInfo(id, userId, false));
|
||||
}
|
||||
}
|
||||
// next we parse and persist the cross signing keys
|
||||
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 crossSigningKeyListEntry in keys.entries) {
|
||||
final userId = crossSigningKeyListEntry.key;
|
||||
if (!userDeviceKeys.containsKey(userId)) {
|
||||
_userDeviceKeys[userId] = DeviceKeysList(userId, this);
|
||||
}
|
||||
final oldKeys = Map<String, CrossSigningKey>.from(
|
||||
_userDeviceKeys[userId].crossSigningKeys);
|
||||
_userDeviceKeys[userId].crossSigningKeys = {};
|
||||
// add the types we aren't handling atm back
|
||||
for (final oldEntry in oldKeys.entries) {
|
||||
if (!oldEntry.value.usage.contains(keyType)) {
|
||||
_userDeviceKeys[userId].crossSigningKeys[oldEntry.key] =
|
||||
oldEntry.value;
|
||||
}
|
||||
}
|
||||
final entry = CrossSigningKey.fromMatrixCrossSigningKey(
|
||||
crossSigningKeyListEntry.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.setDirectVerified(oldKeys[publicKey].directVerified);
|
||||
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()),
|
||||
entry.directVerified,
|
||||
entry.blocked,
|
||||
));
|
||||
}
|
||||
}
|
||||
_userDeviceKeys[userId].outdated = false;
|
||||
if (database != null) {
|
||||
dbActions.add(
|
||||
() => database.storeUserDeviceKeysInfo(id, userId, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await database?.transaction(() async {
|
||||
for (final f in dbActions) {
|
||||
|
@ -1212,12 +1304,16 @@ class Client {
|
|||
Map<String, dynamic> message, {
|
||||
bool encrypted = true,
|
||||
List<User> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,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;
|
||||
|
||||
|
@ -44,6 +44,16 @@ 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.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');
|
||||
from++;
|
||||
}
|
||||
},
|
||||
beforeOpen: (_) async {
|
||||
|
@ -64,16 +74,20 @@ class Database extends _$Database {
|
|||
}
|
||||
|
||||
Future<Map<String, sdk.DeviceKeysList>> getUserDeviceKeys(
|
||||
int clientId) async {
|
||||
final deviceKeys = await getAllUserDeviceKeys(clientId).get();
|
||||
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 = <String, sdk.DeviceKeysList>{};
|
||||
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;
|
||||
}
|
||||
|
@ -140,6 +154,14 @@ class Database extends _$Database {
|
|||
return res.first;
|
||||
}
|
||||
|
||||
Future<DbSSSSCache> getSSSSCache(int clientId, String type) async {
|
||||
final res = await dbGetSSSSCache(clientId, type).get();
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return res.first;
|
||||
}
|
||||
|
||||
Future<List<sdk.Room>> getRoomList(sdk.Client client,
|
||||
{bool onlyLeft = false}) async {
|
||||
final res = await (select(rooms)
|
||||
|
@ -428,11 +450,16 @@ 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)))
|
||||
.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();
|
||||
}
|
||||
|
||||
|
|
|
@ -1039,6 +1039,353 @@ class UserDeviceKeysKey extends Table
|
|||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class DbUserCrossSigningKey extends DataClass
|
||||
implements Insertable<DbUserCrossSigningKey> {
|
||||
final int clientId;
|
||||
final String userId;
|
||||
final String publicKey;
|
||||
final String content;
|
||||
final bool verified;
|
||||
final bool blocked;
|
||||
DbUserCrossSigningKey(
|
||||
{@required this.clientId,
|
||||
@required this.userId,
|
||||
@required this.publicKey,
|
||||
@required this.content,
|
||||
this.verified,
|
||||
this.blocked});
|
||||
factory DbUserCrossSigningKey.fromData(
|
||||
Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
final intType = db.typeSystem.forDartType<int>();
|
||||
final stringType = db.typeSystem.forDartType<String>();
|
||||
final boolType = db.typeSystem.forDartType<bool>();
|
||||
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']),
|
||||
verified:
|
||||
boolType.mapFromDatabaseResponse(data['${effectivePrefix}verified']),
|
||||
blocked:
|
||||
boolType.mapFromDatabaseResponse(data['${effectivePrefix}blocked']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (!nullToAbsent || clientId != null) {
|
||||
map['client_id'] = Variable<int>(clientId);
|
||||
}
|
||||
if (!nullToAbsent || userId != null) {
|
||||
map['user_id'] = Variable<String>(userId);
|
||||
}
|
||||
if (!nullToAbsent || publicKey != null) {
|
||||
map['public_key'] = Variable<String>(publicKey);
|
||||
}
|
||||
if (!nullToAbsent || content != null) {
|
||||
map['content'] = Variable<String>(content);
|
||||
}
|
||||
if (!nullToAbsent || verified != null) {
|
||||
map['verified'] = Variable<bool>(verified);
|
||||
}
|
||||
if (!nullToAbsent || blocked != null) {
|
||||
map['blocked'] = Variable<bool>(blocked);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
factory DbUserCrossSigningKey.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return DbUserCrossSigningKey(
|
||||
clientId: serializer.fromJson<int>(json['client_id']),
|
||||
userId: serializer.fromJson<String>(json['user_id']),
|
||||
publicKey: serializer.fromJson<String>(json['public_key']),
|
||||
content: serializer.fromJson<String>(json['content']),
|
||||
verified: serializer.fromJson<bool>(json['verified']),
|
||||
blocked: serializer.fromJson<bool>(json['blocked']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'client_id': serializer.toJson<int>(clientId),
|
||||
'user_id': serializer.toJson<String>(userId),
|
||||
'public_key': serializer.toJson<String>(publicKey),
|
||||
'content': serializer.toJson<String>(content),
|
||||
'verified': serializer.toJson<bool>(verified),
|
||||
'blocked': serializer.toJson<bool>(blocked),
|
||||
};
|
||||
}
|
||||
|
||||
DbUserCrossSigningKey copyWith(
|
||||
{int clientId,
|
||||
String userId,
|
||||
String publicKey,
|
||||
String content,
|
||||
bool verified,
|
||||
bool blocked}) =>
|
||||
DbUserCrossSigningKey(
|
||||
clientId: clientId ?? this.clientId,
|
||||
userId: userId ?? this.userId,
|
||||
publicKey: publicKey ?? this.publicKey,
|
||||
content: content ?? this.content,
|
||||
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('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(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.verified == this.verified &&
|
||||
other.blocked == this.blocked);
|
||||
}
|
||||
|
||||
class UserCrossSigningKeysCompanion
|
||||
extends UpdateCompanion<DbUserCrossSigningKey> {
|
||||
final Value<int> clientId;
|
||||
final Value<String> userId;
|
||||
final Value<String> publicKey;
|
||||
final Value<String> content;
|
||||
final Value<bool> verified;
|
||||
final Value<bool> blocked;
|
||||
const UserCrossSigningKeysCompanion({
|
||||
this.clientId = const Value.absent(),
|
||||
this.userId = const Value.absent(),
|
||||
this.publicKey = const Value.absent(),
|
||||
this.content = 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.verified = const Value.absent(),
|
||||
this.blocked = const Value.absent(),
|
||||
}) : clientId = Value(clientId),
|
||||
userId = Value(userId),
|
||||
publicKey = Value(publicKey),
|
||||
content = Value(content);
|
||||
static Insertable<DbUserCrossSigningKey> custom({
|
||||
Expression<int> clientId,
|
||||
Expression<String> userId,
|
||||
Expression<String> publicKey,
|
||||
Expression<String> content,
|
||||
Expression<bool> verified,
|
||||
Expression<bool> 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 (verified != null) 'verified': verified,
|
||||
if (blocked != null) 'blocked': blocked,
|
||||
});
|
||||
}
|
||||
|
||||
UserCrossSigningKeysCompanion copyWith(
|
||||
{Value<int> clientId,
|
||||
Value<String> userId,
|
||||
Value<String> publicKey,
|
||||
Value<String> content,
|
||||
Value<bool> verified,
|
||||
Value<bool> blocked}) {
|
||||
return UserCrossSigningKeysCompanion(
|
||||
clientId: clientId ?? this.clientId,
|
||||
userId: userId ?? this.userId,
|
||||
publicKey: publicKey ?? this.publicKey,
|
||||
content: content ?? this.content,
|
||||
verified: verified ?? this.verified,
|
||||
blocked: blocked ?? this.blocked,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (clientId.present) {
|
||||
map['client_id'] = Variable<int>(clientId.value);
|
||||
}
|
||||
if (userId.present) {
|
||||
map['user_id'] = Variable<String>(userId.value);
|
||||
}
|
||||
if (publicKey.present) {
|
||||
map['public_key'] = Variable<String>(publicKey.value);
|
||||
}
|
||||
if (content.present) {
|
||||
map['content'] = Variable<String>(content.value);
|
||||
}
|
||||
if (verified.present) {
|
||||
map['verified'] = Variable<bool>(verified.value);
|
||||
}
|
||||
if (blocked.present) {
|
||||
map['blocked'] = Variable<bool>(blocked.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class UserCrossSigningKeys extends Table
|
||||
with TableInfo<UserCrossSigningKeys, DbUserCrossSigningKey> {
|
||||
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 _verifiedMeta = const VerificationMeta('verified');
|
||||
GeneratedBoolColumn _verified;
|
||||
GeneratedBoolColumn get verified => _verified ??= _constructVerified();
|
||||
GeneratedBoolColumn _constructVerified() {
|
||||
return GeneratedBoolColumn('verified', $tableName, true,
|
||||
$customConstraints: 'DEFAULT false',
|
||||
defaultValue: const CustomExpression<bool>('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<bool>('false'));
|
||||
}
|
||||
|
||||
@override
|
||||
List<GeneratedColumn> get $columns =>
|
||||
[clientId, userId, publicKey, content, 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<DbUserCrossSigningKey> 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('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<GeneratedColumn> get $primaryKey => <GeneratedColumn>{};
|
||||
@override
|
||||
DbUserCrossSigningKey map(Map<String, dynamic> 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<String> get customConstraints =>
|
||||
const ['UNIQUE(client_id, user_id, public_key)'];
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class DbOlmSessions extends DataClass implements Insertable<DbOlmSessions> {
|
||||
final int clientId;
|
||||
final String identityKey;
|
||||
|
@ -4454,6 +4801,311 @@ class Presences extends Table with TableInfo<Presences, DbPresence> {
|
|||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class DbSSSSCache extends DataClass implements Insertable<DbSSSSCache> {
|
||||
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<String, dynamic> data, GeneratedDatabase db,
|
||||
{String prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
final intType = db.typeSystem.forDartType<int>();
|
||||
final stringType = db.typeSystem.forDartType<String>();
|
||||
return DbSSSSCache(
|
||||
clientId:
|
||||
intType.mapFromDatabaseResponse(data['${effectivePrefix}client_id']),
|
||||
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']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (!nullToAbsent || clientId != null) {
|
||||
map['client_id'] = Variable<int>(clientId);
|
||||
}
|
||||
if (!nullToAbsent || type != null) {
|
||||
map['type'] = Variable<String>(type);
|
||||
}
|
||||
if (!nullToAbsent || keyId != null) {
|
||||
map['key_id'] = Variable<String>(keyId);
|
||||
}
|
||||
if (!nullToAbsent || ciphertext != null) {
|
||||
map['ciphertext'] = Variable<String>(ciphertext);
|
||||
}
|
||||
if (!nullToAbsent || content != null) {
|
||||
map['content'] = Variable<String>(content);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
factory DbSSSSCache.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return DbSSSSCache(
|
||||
clientId: serializer.fromJson<int>(json['client_id']),
|
||||
type: serializer.fromJson<String>(json['type']),
|
||||
keyId: serializer.fromJson<String>(json['key_id']),
|
||||
ciphertext: serializer.fromJson<String>(json['ciphertext']),
|
||||
content: serializer.fromJson<String>(json['content']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'client_id': serializer.toJson<int>(clientId),
|
||||
'type': serializer.toJson<String>(type),
|
||||
'key_id': serializer.toJson<String>(keyId),
|
||||
'ciphertext': serializer.toJson<String>(ciphertext),
|
||||
'content': serializer.toJson<String>(content),
|
||||
};
|
||||
}
|
||||
|
||||
DbSSSSCache copyWith(
|
||||
{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
|
||||
String toString() {
|
||||
return (StringBuffer('DbSSSSCache(')
|
||||
..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, $mrjc(ciphertext.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.ciphertext == this.ciphertext &&
|
||||
other.content == this.content);
|
||||
}
|
||||
|
||||
class SsssCacheCompanion extends UpdateCompanion<DbSSSSCache> {
|
||||
final Value<int> clientId;
|
||||
final Value<String> type;
|
||||
final Value<String> keyId;
|
||||
final Value<String> ciphertext;
|
||||
final Value<String> 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<DbSSSSCache> custom({
|
||||
Expression<int> clientId,
|
||||
Expression<String> type,
|
||||
Expression<String> keyId,
|
||||
Expression<String> ciphertext,
|
||||
Expression<String> 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,
|
||||
});
|
||||
}
|
||||
|
||||
SsssCacheCompanion copyWith(
|
||||
{Value<int> clientId,
|
||||
Value<String> type,
|
||||
Value<String> keyId,
|
||||
Value<String> ciphertext,
|
||||
Value<String> content}) {
|
||||
return SsssCacheCompanion(
|
||||
clientId: clientId ?? this.clientId,
|
||||
type: type ?? this.type,
|
||||
keyId: keyId ?? this.keyId,
|
||||
ciphertext: ciphertext ?? this.ciphertext,
|
||||
content: content ?? this.content,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (clientId.present) {
|
||||
map['client_id'] = Variable<int>(clientId.value);
|
||||
}
|
||||
if (type.present) {
|
||||
map['type'] = Variable<String>(type.value);
|
||||
}
|
||||
if (keyId.present) {
|
||||
map['key_id'] = Variable<String>(keyId.value);
|
||||
}
|
||||
if (ciphertext.present) {
|
||||
map['ciphertext'] = Variable<String>(ciphertext.value);
|
||||
}
|
||||
if (content.present) {
|
||||
map['content'] = Variable<String>(content.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class SsssCache extends Table with TableInfo<SsssCache, DbSSSSCache> {
|
||||
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 _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();
|
||||
GeneratedTextColumn _constructContent() {
|
||||
return GeneratedTextColumn('content', $tableName, false,
|
||||
$customConstraints: 'NOT NULL');
|
||||
}
|
||||
|
||||
@override
|
||||
List<GeneratedColumn> get $columns =>
|
||||
[clientId, type, keyId, ciphertext, content];
|
||||
@override
|
||||
SsssCache get asDslTable => this;
|
||||
@override
|
||||
String get $tableName => _alias ?? 'ssss_cache';
|
||||
@override
|
||||
final String actualTableName = 'ssss_cache';
|
||||
@override
|
||||
VerificationContext validateIntegrity(Insertable<DbSSSSCache> 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('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));
|
||||
} else if (isInserting) {
|
||||
context.missing(_contentMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => <GeneratedColumn>{};
|
||||
@override
|
||||
DbSSSSCache map(Map<String, dynamic> 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<String> get customConstraints => const ['UNIQUE(client_id, type)'];
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class DbFile extends DataClass implements Insertable<DbFile> {
|
||||
final String mxcUri;
|
||||
final Uint8List bytes;
|
||||
|
@ -4680,6 +5332,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;
|
||||
|
@ -4733,6 +5392,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) {
|
||||
|
@ -4835,6 +5496,24 @@ 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'),
|
||||
verified: row.readBool('verified'),
|
||||
blocked: row.readBool('blocked'),
|
||||
);
|
||||
}
|
||||
|
||||
Selectable<DbUserCrossSigningKey> 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'),
|
||||
|
@ -5079,6 +5758,107 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
);
|
||||
}
|
||||
|
||||
Future<int> 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<int> 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<int> 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, 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.withBool(verified),
|
||||
Variable.withBool(blocked)
|
||||
],
|
||||
updates: {userCrossSigningKeys},
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> 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<int> 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, 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},
|
||||
);
|
||||
}
|
||||
|
||||
DbSSSSCache _rowToDbSSSSCache(QueryRow row) {
|
||||
return DbSSSSCache(
|
||||
clientId: row.readInt('client_id'),
|
||||
type: row.readString('type'),
|
||||
keyId: row.readString('key_id'),
|
||||
ciphertext: row.readString('ciphertext'),
|
||||
content: row.readString('content'),
|
||||
);
|
||||
}
|
||||
|
||||
Selectable<DbSSSSCache> 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<int> 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<int> insertClient(
|
||||
String name,
|
||||
String homeserver_url,
|
||||
|
@ -5508,6 +6288,8 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
userDeviceKeysIndex,
|
||||
userDeviceKeysKey,
|
||||
userDeviceKeysKeyIndex,
|
||||
userCrossSigningKeys,
|
||||
userCrossSigningKeysIndex,
|
||||
olmSessions,
|
||||
olmSessionsIndex,
|
||||
outboundGroupSessions,
|
||||
|
@ -5526,6 +6308,7 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
roomAccountDataIndex,
|
||||
presences,
|
||||
presencesIndex,
|
||||
ssssCache,
|
||||
files
|
||||
];
|
||||
}
|
||||
|
|
|
@ -32,6 +32,17 @@ CREATE TABLE user_device_keys_key (
|
|||
) 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,
|
||||
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,
|
||||
|
@ -63,6 +74,15 @@ 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,
|
||||
ciphertext 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,
|
||||
|
@ -154,6 +174,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;
|
||||
dbGetOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id AND identity_key = :identity_key;
|
||||
storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle);
|
||||
|
@ -171,6 +192,13 @@ setVerifiedUserDeviceKey: UPDATE user_device_keys_key SET verified = :verified W
|
|||
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);
|
||||
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, 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, 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;
|
||||
|
|
|
@ -465,7 +465,9 @@ 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']);
|
||||
}
|
||||
|
|
|
@ -1,158 +1,371 @@
|
|||
import 'dart:convert';
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
|
||||
import '../client.dart';
|
||||
import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey;
|
||||
import '../user.dart';
|
||||
import '../room.dart';
|
||||
import '../database/database.dart'
|
||||
show DbUserDeviceKey, DbUserDeviceKeysKey, DbUserCrossSigningKey;
|
||||
import '../event.dart';
|
||||
|
||||
enum UserVerifiedStatus { verified, unknown, unknownDevice }
|
||||
|
||||
class DeviceKeysList {
|
||||
Client client;
|
||||
String userId;
|
||||
bool outdated = true;
|
||||
Map<String, DeviceKeys> deviceKeys = {};
|
||||
Map<String, CrossSigningKey> crossSigningKeys = {};
|
||||
|
||||
SignableKey getKey(String id) {
|
||||
if (deviceKeys.containsKey(id)) {
|
||||
return deviceKeys[id];
|
||||
}
|
||||
if (crossSigningKeys.containsKey(id)) {
|
||||
return crossSigningKeys[id];
|
||||
}
|
||||
return 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');
|
||||
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;
|
||||
}
|
||||
|
||||
Future<KeyVerification> 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(
|
||||
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
|
||||
return request;
|
||||
}
|
||||
|
||||
DeviceKeysList.fromDb(
|
||||
DbUserDeviceKey dbEntry, List<DbUserDeviceKeysKey> childEntries) {
|
||||
DbUserDeviceKey dbEntry,
|
||||
List<DbUserDeviceKeysKey> childEntries,
|
||||
List<DbUserCrossSigningKey> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeviceKeysList.fromJson(Map<String, dynamic> json) {
|
||||
userId = json['user_id'];
|
||||
outdated = json['outdated'];
|
||||
deviceKeys = {};
|
||||
for (final rawDeviceKeyEntry in json['device_keys'].entries) {
|
||||
deviceKeys[rawDeviceKeyEntry.key] =
|
||||
DeviceKeys.fromJson(rawDeviceKeyEntry.value);
|
||||
for (final crossSigningEntry in crossSigningEntries) {
|
||||
final entry = CrossSigningKey.fromDb(crossSigningEntry, client);
|
||||
if (entry.isValid) {
|
||||
crossSigningKeys[crossSigningEntry.publicKey] = entry;
|
||||
} else {
|
||||
outdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeviceKeysList(this.userId, this.client);
|
||||
}
|
||||
|
||||
abstract class SignableKey extends MatrixSignableKey {
|
||||
Client client;
|
||||
Map<String, dynamic> validSignatures;
|
||||
bool _verified;
|
||||
bool blocked;
|
||||
|
||||
String get ed25519Key => keys['ed25519:$identifier'];
|
||||
bool get verified => (directVerified || crossVerified) && !blocked;
|
||||
|
||||
void setDirectVerified(bool v) {
|
||||
_verified = v;
|
||||
}
|
||||
|
||||
bool get directVerified => _verified;
|
||||
bool get crossVerified => hasValidSignatureChain();
|
||||
bool get signed => hasValidSignatureChain(verifiedOnly: false);
|
||||
|
||||
SignableKey.fromJson(Map<String, dynamic> json, Client cl)
|
||||
: client = cl,
|
||||
super.fromJson(json) {
|
||||
_verified = false;
|
||||
blocked = false;
|
||||
}
|
||||
|
||||
MatrixSignableKey cloneForSigning() {
|
||||
final newKey =
|
||||
MatrixSignableKey.fromJson(Map<String, dynamic>.from(toJson()));
|
||||
newKey.identifier = identifier;
|
||||
newKey.signatures ??= <String, Map<String, String>>{};
|
||||
newKey.signatures.clear();
|
||||
return newKey;
|
||||
}
|
||||
|
||||
String get signingContent {
|
||||
final data = Map<String, dynamic>.from(super.toJson());
|
||||
// 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));
|
||||
}
|
||||
|
||||
bool _verifySignature(String pubKey, String signature) {
|
||||
final olmutil = olm.Utility();
|
||||
var valid = false;
|
||||
try {
|
||||
olmutil.ed25519_verify(pubKey, signingContent, signature);
|
||||
valid = true;
|
||||
} catch (_) {
|
||||
// bad signature
|
||||
valid = false;
|
||||
} finally {
|
||||
olmutil.free();
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
bool hasValidSignatureChain({bool verifiedOnly = true, Set<String> visited}) {
|
||||
if (!client.encryptionEnabled) {
|
||||
return false;
|
||||
}
|
||||
visited ??= <String>{};
|
||||
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);
|
||||
SignableKey key;
|
||||
if (client.userDeviceKeys[otherUserId].deviceKeys.containsKey(keyId)) {
|
||||
key = client.userDeviceKeys[otherUserId].deviceKeys[keyId];
|
||||
} else if (client.userDeviceKeys[otherUserId].crossSigningKeys
|
||||
.containsKey(keyId)) {
|
||||
key = client.userDeviceKeys[otherUserId].crossSigningKeys[keyId];
|
||||
} 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) {
|
||||
haveValidSignature = false;
|
||||
gotSignatureFromCache = true;
|
||||
}
|
||||
}
|
||||
if (!gotSignatureFromCache) {
|
||||
// validate the signature manually
|
||||
haveValidSignature = _verifySignature(key.ed25519Key, signature);
|
||||
validSignatures ??= <String, dynamic>{};
|
||||
if (!validSignatures.containsKey(otherUserId)) {
|
||||
validSignatures[otherUserId] = <String, dynamic>{};
|
||||
}
|
||||
validSignatures[otherUserId][fullKeyId] = haveValidSignature;
|
||||
}
|
||||
if (!haveValidSignature) {
|
||||
// no valid signature, this key is useless
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((verifiedOnly && key.directVerified) ||
|
||||
(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(
|
||||
verifiedOnly: verifiedOnly, visited: visited);
|
||||
if (haveChain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void setVerified(bool newVerified, [bool sign = true]) {
|
||||
_verified = newVerified;
|
||||
if (newVerified &&
|
||||
sign &&
|
||||
client.encryptionEnabled &&
|
||||
client.encryption.crossSigning.signable([this])) {
|
||||
// sign the key!
|
||||
client.encryption.crossSigning.sign([this]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setBlocked(bool newBlocked);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
var map = <String, dynamic>{};
|
||||
final data = map;
|
||||
data['user_id'] = userId;
|
||||
data['outdated'] = outdated ?? true;
|
||||
|
||||
var rawDeviceKeys = <String, dynamic>{};
|
||||
for (final deviceKeyEntry in deviceKeys.entries) {
|
||||
rawDeviceKeys[deviceKeyEntry.key] = deviceKeyEntry.value.toJson();
|
||||
}
|
||||
data['device_keys'] = rawDeviceKeys;
|
||||
final data = Map<String, dynamic>.from(super.toJson());
|
||||
// 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());
|
||||
|
||||
DeviceKeysList(this.userId);
|
||||
}
|
||||
|
||||
class DeviceKeys extends MatrixDeviceKeys {
|
||||
bool verified;
|
||||
bool blocked;
|
||||
class CrossSigningKey extends SignableKey {
|
||||
String get publicKey => identifier;
|
||||
List<String> usage;
|
||||
|
||||
bool get isValid =>
|
||||
userId != null && publicKey != null && keys != null && ed25519Key != null;
|
||||
|
||||
@override
|
||||
Future<void> setVerified(bool newVerified, [bool sign = true]) {
|
||||
super.setVerified(newVerified, sign);
|
||||
return client.database?.setVerifiedUserCrossSigningKey(
|
||||
newVerified, client.id, userId, publicKey);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setBlocked(bool newBlocked) {
|
||||
blocked = newBlocked;
|
||||
return client.database?.setBlockedUserCrossSigningKey(
|
||||
newBlocked, client.id, userId, publicKey);
|
||||
}
|
||||
|
||||
CrossSigningKey.fromMatrixCrossSigningKey(MatrixCrossSigningKey k, Client cl)
|
||||
: super.fromJson(Map<String, dynamic>.from(k.toJson()), cl) {
|
||||
final json = toJson();
|
||||
identifier = k.publicKey;
|
||||
usage = json['usage'].cast<String>();
|
||||
}
|
||||
|
||||
CrossSigningKey.fromDb(DbUserCrossSigningKey dbEntry, Client cl)
|
||||
: super.fromJson(Event.getMapFromPayload(dbEntry.content), cl) {
|
||||
final json = toJson();
|
||||
identifier = dbEntry.publicKey;
|
||||
usage = json['usage'].cast<String>();
|
||||
_verified = dbEntry.verified;
|
||||
blocked = dbEntry.blocked;
|
||||
}
|
||||
|
||||
CrossSigningKey.fromJson(Map<String, dynamic> json, Client cl)
|
||||
: super.fromJson(Map<String, dynamic>.from(json), cl) {
|
||||
final json = toJson();
|
||||
usage = json['usage'].cast<String>();
|
||||
if (keys != null && keys.isNotEmpty) {
|
||||
identifier = keys.values.first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceKeys extends SignableKey {
|
||||
String get deviceId => identifier;
|
||||
List<String> algorithms;
|
||||
|
||||
String get curve25519Key => keys['curve25519:$deviceId'];
|
||||
String get ed25519Key => keys['ed25519:$deviceId'];
|
||||
String get deviceDisplayName =>
|
||||
unsigned != null ? unsigned['device_display_name'] : null;
|
||||
|
||||
bool get isValid =>
|
||||
userId != null &&
|
||||
deviceId != null &&
|
||||
keys != null &&
|
||||
curve25519Key != null &&
|
||||
ed25519Key != null;
|
||||
|
||||
Future<void> setVerified(bool newVerified, Client client) {
|
||||
verified = newVerified;
|
||||
return client.database
|
||||
@override
|
||||
Future<void> setVerified(bool newVerified, [bool sign = true]) {
|
||||
super.setVerified(newVerified, sign);
|
||||
return client?.database
|
||||
?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
|
||||
}
|
||||
|
||||
Future<void> setBlocked(bool newBlocked, Client client) {
|
||||
@override
|
||||
Future<void> setBlocked(bool newBlocked) {
|
||||
blocked = newBlocked;
|
||||
return client.database
|
||||
return client?.database
|
||||
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
|
||||
}
|
||||
|
||||
DeviceKeys({
|
||||
String userId,
|
||||
String deviceId,
|
||||
List<String> algorithms,
|
||||
Map<String, String> keys,
|
||||
Map<String, Map<String, String>> signatures,
|
||||
Map<String, dynamic> 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<String>();
|
||||
deviceKeys.keys = content['keys'] != null
|
||||
? Map<String, String>.from(content['keys'])
|
||||
: null;
|
||||
deviceKeys.signatures = content['signatures'] != null
|
||||
? Map<String, Map<String, String>>.from((content['signatures'] as Map)
|
||||
.map((k, v) => MapEntry(k, Map<String, String>.from(v))))
|
||||
: null;
|
||||
deviceKeys.unsigned = content['unsigned'] != null
|
||||
? Map<String, dynamic>.from(content['unsigned'])
|
||||
: null;
|
||||
deviceKeys.verified = dbEntry.verified;
|
||||
deviceKeys.blocked = dbEntry.blocked;
|
||||
return deviceKeys;
|
||||
DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys k, Client cl)
|
||||
: super.fromJson(Map<String, dynamic>.from(k.toJson()), cl) {
|
||||
final json = toJson();
|
||||
identifier = k.deviceId;
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
}
|
||||
|
||||
static DeviceKeys fromJson(Map<String, dynamic> json) {
|
||||
var matrixDeviceKeys = MatrixDeviceKeys.fromJson(json);
|
||||
var deviceKeys = DeviceKeys(
|
||||
userId: matrixDeviceKeys.userId,
|
||||
deviceId: matrixDeviceKeys.deviceId,
|
||||
algorithms: matrixDeviceKeys.algorithms,
|
||||
keys: matrixDeviceKeys.keys,
|
||||
signatures: matrixDeviceKeys.signatures,
|
||||
unsigned: matrixDeviceKeys.unsigned,
|
||||
);
|
||||
deviceKeys.verified = json['verified'] ?? false;
|
||||
deviceKeys.blocked = json['blocked'] ?? false;
|
||||
return deviceKeys;
|
||||
DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry, Client cl)
|
||||
: super.fromJson(Event.getMapFromPayload(dbEntry.content), cl) {
|
||||
final json = toJson();
|
||||
identifier = dbEntry.deviceId;
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
_verified = dbEntry.verified;
|
||||
blocked = dbEntry.blocked;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = super.toJson();
|
||||
data['verified'] = verified;
|
||||
data['blocked'] = blocked;
|
||||
return data;
|
||||
DeviceKeys.fromJson(Map<String, dynamic> json, Client cl)
|
||||
: super.fromJson(Map<String, dynamic>.from(json), cl) {
|
||||
final json = toJson();
|
||||
identifier = json['device_id'];
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
}
|
||||
|
||||
KeyVerification startVerification(Client client) {
|
||||
KeyVerification startVerification() {
|
||||
final request = KeyVerification(
|
||||
encryption: client.encryption, userId: userId, deviceId: deviceId);
|
||||
|
||||
request.start();
|
||||
client.encryption.keyVerificationManager.addRequest(request);
|
||||
return request;
|
||||
|
|
41
pubspec.lock
41
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.2"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -386,10 +414,10 @@ packages:
|
|||
description:
|
||||
path: "."
|
||||
ref: "1.x.y"
|
||||
resolved-ref: f66975bd1b5cb1865eba5efe6e3a392aa5e396a5
|
||||
resolved-ref: "8e4fcccff7a2d4d0bd5142964db092bf45061905"
|
||||
url: "https://gitlab.com/famedly/libraries/dart-olm.git"
|
||||
source: git
|
||||
version: "1.1.1"
|
||||
version: "1.2.0"
|
||||
package_config:
|
||||
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:
|
||||
|
|
|
@ -16,6 +16,10 @@ dependencies:
|
|||
html_unescape: ^1.0.1+3
|
||||
moor: ^3.0.2
|
||||
random_string: ^2.0.1
|
||||
encrypt: ^4.0.2
|
||||
crypto: ^2.1.4
|
||||
base58check: ^1.0.1
|
||||
password_hash: ^2.0.0
|
||||
|
||||
olm:
|
||||
git:
|
||||
|
|
2
test.sh
2
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
|
||||
|
|
|
@ -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);
|
||||
|
@ -392,7 +392,7 @@ void main() {
|
|||
'dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA'
|
||||
}
|
||||
}
|
||||
});
|
||||
}, matrix);
|
||||
test('sendToDevice', () async {
|
||||
await matrix.sendToDevice(
|
||||
[deviceKeys],
|
||||
|
|
|
@ -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
|
||||
|
@ -41,33 +45,133 @@ void main() {
|
|||
}
|
||||
},
|
||||
'unsigned': {'device_display_name': "Alice's mobile phone"},
|
||||
'verified': false,
|
||||
'blocked': true,
|
||||
};
|
||||
var rawListJson = <String, dynamic>{
|
||||
'user_id': '@alice:example.com',
|
||||
'outdated': true,
|
||||
'device_keys': {'JLAFKJWSCS': rawJson},
|
||||
};
|
||||
|
||||
var userDeviceKeys = <String, DeviceKeysList>{
|
||||
'@alice:example.com': DeviceKeysList.fromJson(rawListJson),
|
||||
};
|
||||
var userDeviceKeyRaw = <String, dynamic>{
|
||||
'@alice:example.com': rawListJson,
|
||||
};
|
||||
final key = DeviceKeys.fromJson(rawJson, null);
|
||||
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);
|
||||
|
||||
expect(json.encode(DeviceKeys.fromJson(rawJson).toJson()),
|
||||
json.encode(rawJson));
|
||||
expect(json.encode(DeviceKeysList.fromJson(rawListJson).toJson()),
|
||||
json.encode(rawListJson));
|
||||
rawJson = <String, dynamic>{
|
||||
'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 mapFromRaw = <String, DeviceKeysList>{};
|
||||
for (final rawListEntry in userDeviceKeyRaw.entries) {
|
||||
mapFromRaw[rawListEntry.key] =
|
||||
DeviceKeysList.fromJson(rawListEntry.value);
|
||||
}
|
||||
expect(mapFromRaw.toString(), userDeviceKeys.toString());
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
113
test/encryption/cross_signing_test.dart
Normal file
113
test/encryption/cross_signing_test.dart
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -84,12 +84,15 @@ void main() {
|
|||
room: room,
|
||||
originServerTs: now,
|
||||
eventId: '\$event',
|
||||
senderId: '@alice:example.com',
|
||||
);
|
||||
final decryptedEvent =
|
||||
await client.encryption.decryptRoomEvent(roomId, encryptedEvent);
|
||||
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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
@ -85,10 +86,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',
|
||||
|
@ -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',
|
||||
|
|
|
@ -16,12 +16,47 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<void> maybeRequestAll(List<DeviceKeys> 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
73
test/encryption/online_key_backup_test.dart
Normal file
73
test/encryption/online_key_backup_test.dart
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
398
test/encryption/ssss_test.dart
Normal file
398
test/encryption/ssss_test.dart
Normal file
|
@ -0,0 +1,398 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 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('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.send',
|
||||
content: {
|
||||
'request_id':
|
||||
client.encryption.ssss.pendingShareRequests.keys.first,
|
||||
'secret': secret,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption.ssss.getCached(type), secret);
|
||||
}
|
||||
|
||||
// test different fail scenarios
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
|
@ -528,6 +533,75 @@ 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': {
|
||||
|
@ -1473,6 +1547,65 @@ 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',
|
||||
'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',
|
||||
},
|
||||
},
|
||||
'/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) => {},
|
||||
|
@ -1683,7 +1816,30 @@ 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',
|
||||
'device_id': 'OTHERDEVICE',
|
||||
'algorithms': [
|
||||
'm.olm.v1.curve25519-aes-sha2',
|
||||
'm.megolm.v1.aes-sha2'
|
||||
],
|
||||
'keys': {
|
||||
'curve25519:OTHERDEVICE': 'blah',
|
||||
'ed25519:OTHERDEVICE': 'blah'
|
||||
},
|
||||
'signatures': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY':
|
||||
'o7ucKPWrF2VKx7wYqP1f+aw4QohLMz7kX+SIw6aWCYsLC3XyIlg8rX/7QQ9B8figCVnRK7IjtjWvQodBCfWCAA',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'@othertest:fakeServer.notExisting': {
|
||||
|
@ -1704,6 +1860,73 @@ class FakeMatrixApi extends MockClient {
|
|||
},
|
||||
},
|
||||
},
|
||||
'master_keys': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'user_id': '@test:fakeServer.notExisting',
|
||||
'usage': ['master'],
|
||||
'keys': {
|
||||
'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8':
|
||||
'82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8',
|
||||
},
|
||||
'signatures': {},
|
||||
},
|
||||
'@othertest:fakeServer.notExisting': {
|
||||
'user_id': '@othertest:fakeServer.notExisting',
|
||||
'usage': ['master'],
|
||||
'keys': {
|
||||
'ed25519:master': 'master',
|
||||
},
|
||||
'signatures': {},
|
||||
},
|
||||
},
|
||||
'self_signing_keys': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'user_id': '@test:fakeServer.notExisting',
|
||||
'usage': ['self_signing'],
|
||||
'keys': {
|
||||
'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY':
|
||||
'F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY',
|
||||
},
|
||||
'signatures': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8':
|
||||
'afkrbGvPn5Zb5zc7Lk9cz2skI3QrzI/L0st1GS+/GATxNjMzc6vKmGu7r9cMb1GJxy4RdeUpfH3L7Fs/fNL1Dw',
|
||||
},
|
||||
},
|
||||
},
|
||||
'@othertest:fakeServer.notExisting': {
|
||||
'user_id': '@othertest:fakeServer.notExisting',
|
||||
'usage': ['self_signing'],
|
||||
'keys': {
|
||||
'ed25519:self_signing': 'self_signing',
|
||||
},
|
||||
'signatures': {},
|
||||
},
|
||||
},
|
||||
'user_signing_keys': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'user_id': '@test:fakeServer.notExisting',
|
||||
'usage': ['user_signing'],
|
||||
'keys': {
|
||||
'ed25519:0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g':
|
||||
'0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g',
|
||||
},
|
||||
'signatures': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8':
|
||||
'pvgbZxEbllaElhpiRnb7/uOIUhrglvHCFnpoxr3/5ZrWa0EK/uaefhex9eEV4uBLrHjHg2ymwdNaM7ap9+sBBg',
|
||||
},
|
||||
},
|
||||
},
|
||||
'@othertest:fakeServer.notExisting': {
|
||||
'user_id': '@othertest:fakeServer.notExisting',
|
||||
'usage': ['user_signing'],
|
||||
'keys': {
|
||||
'ed25519:user_signing': 'user_signing',
|
||||
},
|
||||
'signatures': {},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/client/r0/register': (var req) => {
|
||||
'user_id': '@testuser:example.com',
|
||||
|
@ -1751,6 +1974,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':
|
||||
|
@ -1797,8 +2023,14 @@ 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/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':
|
||||
|
@ -1831,6 +2063,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'},
|
||||
|
@ -1843,6 +2090,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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -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 = <String, dynamic>{
|
||||
'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 = <String, dynamic>{
|
||||
'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());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -107,7 +107,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));
|
||||
|
|
Loading…
Reference in a new issue