Merge branch 'soru/cross-signing' into 'master'

Cross-Signing

See merge request famedly/famedlysdk!319
This commit is contained in:
Sorunome 2020-06-25 07:53:30 +00:00
commit 5dda0c3623
37 changed files with 4610 additions and 300 deletions

View file

@ -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';

View 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();
}
}
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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();

View file

@ -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
View 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);
}
}

View file

@ -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) {

View file

@ -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';

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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
];
}

View file

@ -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;

View file

@ -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']);
}

View file

@ -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;

View file

@ -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:

View file

@ -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:

View file

@ -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

View file

@ -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],

View file

@ -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);
});
});
}

View 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);
});
});
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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',

View file

@ -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);
});
});
}

View 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);
});
});
}

View 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);
});
});
}

View file

@ -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';

View file

@ -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,
},
},
};
}

View file

@ -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());
});
});
}

View file

@ -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));