diff --git a/lib/encryption.dart b/lib/encryption.dart
index ef4f347..2239ee2 100644
--- a/lib/encryption.dart
+++ b/lib/encryption.dart
@@ -20,4 +20,5 @@ library encryption;
export './encryption/encryption.dart';
export './encryption/key_manager.dart';
+export './encryption/ssss.dart';
export './encryption/utils/key_verification.dart';
diff --git a/lib/encryption/cross_signing.dart b/lib/encryption/cross_signing.dart
new file mode 100644
index 0000000..92cbb86
--- /dev/null
+++ b/lib/encryption/cross_signing.dart
@@ -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 .
+ */
+
+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 isCached() async {
+ if (!enabled) {
+ return false;
+ }
+ return (await encryption.ssss.getCached(SELF_SIGNING_KEY)) != null &&
+ (await encryption.ssss.getCached(USER_SIGNING_KEY)) != null;
+ }
+
+ Future 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 keys) {
+ for (final key in keys) {
+ if (key is CrossSigningKey && key.usage.contains('master')) {
+ return true;
+ }
+ if (key.userId == client.userID &&
+ (key is DeviceKeys) &&
+ key.identifier != client.deviceID) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ Future sign(List keys) async {
+ Uint8List selfSigningKey;
+ Uint8List userSigningKey;
+ final signedKeys = [];
+ final addSignature =
+ (SignableKey key, SignableKey signedWith, String signature) {
+ if (key == null || signedWith == null || signature == null) {
+ return;
+ }
+ final signedKey = key.cloneForSigning();
+ signedKey.signatures[signedWith.userId] = {};
+ 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();
+ }
+ }
+}
diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart
index 9038b57..940f4fa 100644
--- a/lib/encryption/encryption.dart
+++ b/lib/encryption/encryption.dart
@@ -24,6 +24,8 @@ import 'package:pedantic/pedantic.dart';
import 'key_manager.dart';
import 'olm_manager.dart';
import 'key_verification_manager.dart';
+import 'cross_signing.dart';
+import 'ssss.dart';
class Encryption {
final Client client;
@@ -42,15 +44,19 @@ class Encryption {
KeyManager keyManager;
OlmManager olmManager;
KeyVerificationManager keyVerificationManager;
+ CrossSigning crossSigning;
+ SSSS ssss;
Encryption({
this.client,
this.debug,
this.enableE2eeRecovery,
}) {
+ ssss = SSSS(this);
keyManager = KeyManager(this);
olmManager = OlmManager(this);
keyVerificationManager = KeyVerificationManager(this);
+ crossSigning = CrossSigning(this);
}
Future init(String olmAccount) async {
@@ -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 handleEventUpdate(EventUpdate update) async {
+ if (update.type == 'ephemeral') {
+ return;
+ }
+ if (update.eventType.startsWith('m.key.verification.') ||
+ (update.eventType == 'm.room.message' &&
+ (update.content['content']['msgtype'] is String) &&
+ update.content['content']['msgtype']
+ .startsWith('m.key.verification.'))) {
+ // "just" key verification, no need to do this in sync
+ unawaited(keyVerificationManager.handleEventUpdate(update));
+ }
}
Future decryptToDeviceEvent(ToDeviceEvent event) async {
diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart
index 4782695..d9b2a88 100644
--- a/lib/encryption/key_manager.dart
+++ b/lib/encryption/key_manager.dart
@@ -27,6 +27,8 @@ import './encryption.dart';
import './utils/session_key.dart';
import './utils/outbound_group_session.dart';
+const MEGOLM_KEY = 'm.megolm_backup.v1';
+
class KeyManager {
final Encryption encryption;
Client get client => encryption.client;
@@ -37,7 +39,29 @@ class KeyManager {
final Set _loadedOutboundGroupSessions = {};
final Set _requestedSessionIds = {};
- KeyManager(this.encryption);
+ KeyManager(this.encryption) {
+ encryption.ssss.setValidator(MEGOLM_KEY, (String secret) async {
+ final keyObj = olm.PkDecryption();
+ try {
+ final info = await 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 isCached() async {
+ if (!enabled) {
+ return false;
+ }
+ return (await encryption.ssss.getCached(MEGOLM_KEY)) != null;
+ }
+
+ Future 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 decrypted;
+ try {
+ decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'],
+ sessionData['mac'], sessionData['ciphertext']));
+ } catch (err) {
+ print('[LibOlm] Error decrypting room key: ' + err.toString());
+ }
+ if (decrypted != null) {
+ decrypted['session_id'] = sessionId;
+ decrypted['room_id'] = roomId;
+ setInboundGroupSession(
+ roomId, sessionId, decrypted['sender_key'], decrypted,
+ forwarded: true);
+ }
+ }
+ }
+ } finally {
+ decryption.free();
+ }
+ }
+
+ Future loadSingleKey(String roomId, String sessionId) async {
+ final info = await 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 request(Room room, String sessionId, String senderKey) async {
+ Future request(Room room, String sessionId, String senderKey,
+ {bool tryOnlineBackup = true}) async {
+ if (tryOnlineBackup) {
+ // let's first check our online key backup store thingy...
+ var hadPreviously =
+ getInboundGroupSession(room.id, sessionId, senderKey) != null;
+ try {
+ await loadSingleKey(room.id, sessionId);
+ } catch (err, stacktrace) {
+ print('[KeyManager] Failed to access online key backup: ' +
+ err.toString());
+ print(stacktrace);
+ }
+ if (!hadPreviously &&
+ getInboundGroupSession(room.id, sessionId, senderKey) != null) {
+ return; // we managed to load the session from online backup, no need to care about it now
+ }
+ }
// while we just send the to-device event to '*', we still need to save the
// devices themself to know where to send the cancel to after receiving a reply
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
}
diff --git a/lib/encryption/key_verification_manager.dart b/lib/encryption/key_verification_manager.dart
index f8720f4..de82074 100644
--- a/lib/encryption/key_verification_manager.dart
+++ b/lib/encryption/key_verification_manager.dart
@@ -29,6 +29,7 @@ class KeyVerificationManager {
final Map _requests = {};
Future cleanup() async {
+ final Set entriesToDispose = {};
for (final entry in _requests.entries) {
var dispose = entry.value.canceled ||
entry.value.state == KeyVerificationState.done ||
@@ -38,9 +39,12 @@ class KeyVerificationManager {
}
if (dispose) {
entry.value.dispose();
- _requests.remove(entry.key);
+ entriesToDispose.add(entry.key);
}
}
+ for (final k in entriesToDispose) {
+ _requests.remove(k);
+ }
}
void addRequest(KeyVerification request) {
@@ -51,7 +55,8 @@ class KeyVerificationManager {
}
Future handleToDeviceEvent(ToDeviceEvent event) async {
- if (!event.type.startsWith('m.key.verification')) {
+ if (!event.type.startsWith('m.key.verification') ||
+ client.verificationMethods.isEmpty) {
return;
}
// we have key verification going on!
@@ -75,6 +80,54 @@ class KeyVerificationManager {
}
}
+ Future handleEventUpdate(EventUpdate update) async {
+ final event = update.content;
+ final type = event['type'].startsWith('m.key.verification.')
+ ? event['type']
+ : event['content']['msgtype'];
+ if (type == null ||
+ !type.startsWith('m.key.verification.') ||
+ client.verificationMethods.isEmpty) {
+ return;
+ }
+ if (type == 'm.key.verification.request') {
+ event['content']['timestamp'] = event['origin_server_ts'];
+ }
+
+ final transactionId =
+ KeyVerification.getTransactionId(event['content']) ?? event['event_id'];
+
+ if (_requests.containsKey(transactionId)) {
+ final req = _requests[transactionId];
+ 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();
diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart
index 43871c0..b08ab1b 100644
--- a/lib/encryption/olm_manager.dart
+++ b/lib/encryption/olm_manager.dart
@@ -96,6 +96,10 @@ class OlmManager {
return payload;
}
+ String signString(String s) {
+ return _olmAccount.sign(s);
+ }
+
/// Checks the signature of a signed json object.
bool checkJsonSignature(String key, Map signedJson,
String userId, String deviceId) {
diff --git a/lib/encryption/ssss.dart b/lib/encryption/ssss.dart
new file mode 100644
index 0000000..9349a14
--- /dev/null
+++ b/lib/encryption/ssss.dart
@@ -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 .
+ */
+
+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 = [
+ '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 = {};
+ final _validators = 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 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 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 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 store(
+ String type, String secret, String keyId, Uint8List key) async {
+ final encrypted = encryptAes(secret, key, type);
+ final content = {
+ 'encrypted': {},
+ };
+ content['encrypted'][keyId] = {
+ 'iv': encrypted.iv,
+ 'ciphertext': encrypted.ciphertext,
+ 'mac': encrypted.mac,
+ };
+ // store the thing in your account data
+ await client.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 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 maybeRequestAll(List devices) async {
+ for (final type in CACHE_TYPES) {
+ final secret = await getCached(type);
+ if (secret == null) {
+ await request(type, devices);
+ }
+ }
+ }
+
+ Future request(String type, List devices) async {
+ // only send to own, verified devices
+ print('[SSSS] Requesting type ${type}...');
+ devices.removeWhere((DeviceKeys d) =>
+ d.userId != client.userID ||
+ !d.verified ||
+ d.blocked ||
+ d.deviceId == client.deviceID);
+ if (devices.isEmpty) {
+ print('[SSSS] Warn: No devices');
+ return;
+ }
+ final requestId = 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 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 keyIdsFromType(String type) {
+ final data = client.accountData[type];
+ if (data == null) {
+ return null;
+ }
+ if (data.content['encrypted'] is Map) {
+ final Set keys = {};
+ 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 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 getStored(String type) async {
+ return await ssss.getStored(type, keyId, privateKey);
+ }
+
+ Future store(String type, String secret) async {
+ await ssss.store(type, secret, keyId, privateKey);
+ }
+
+ Future maybeCacheAll() async {
+ await ssss.maybeCacheAll(keyId, privateKey);
+ }
+}
diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart
index 3698477..90a2479 100644
--- a/lib/encryption/utils/key_verification.dart
+++ b/lib/encryption/utils/key_verification.dart
@@ -16,9 +16,10 @@
* along with this program. If not, see .
*/
+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 _intersect(List a, List b) {
if (b == null || a == null) {
return [];
@@ -103,11 +107,9 @@ List _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 possibleMethods;
Map startPaylaod;
+ String _nextAction;
+ List _verifiedDevices;
DateTime lastActivity;
String lastStep;
@@ -157,22 +161,44 @@ class KeyVerification {
: null);
}
- Future start() async {
- if (room == null) {
- transactionId = randomString(512);
+ List get knownVerificationMethods {
+ final methods = [];
+ if (client.verificationMethods.contains(KeyVerificationMethod.numbers) ||
+ client.verificationMethods.contains(KeyVerificationMethod.emoji)) {
+ methods.add('m.sas.v1');
}
+ return methods;
+ }
+
+ Future sendStart() async {
await send('m.key.verification.request', {
- 'methods': VERIFICATION_METHODS,
- 'timestamp': DateTime.now().millisecondsSinceEpoch,
+ 'methods': knownVerificationMethods,
+ if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch,
});
startedVerification = true;
setState(KeyVerificationState.waitingAccept);
+ lastActivity = DateTime.now();
+ }
+
+ Future 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 handlePayload(String type, Map 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 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 acceptVerification() async {
if (!(await verifyLastStep(
@@ -318,9 +404,29 @@ class KeyVerification {
return [];
}
+ Future maybeRequestSSSSSecrets([int i = 0]) async {
+ final requestInterval = [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().toList()));
+ if (requestInterval.length <= i) {
+ return;
+ }
+ Timer(Duration(seconds: requestInterval[i]),
+ () => maybeRequestSSSSSecrets(i + 1));
+ }
+
Future verifyKeys(Map keys,
- Future Function(String, DeviceKeys) verifier) async {
- final verifiedDevices = [];
+ Future Function(String, SignableKey) verifier) async {
+ _verifiedDevices = [];
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 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 get knownAuthentificationTypes {
+ final types = [];
+ if (request.client.verificationMethods
+ .contains(KeyVerificationMethod.emoji)) {
+ types.add('emoji');
+ }
+ if (request.client.verificationMethods
+ .contains(KeyVerificationMethod.numbers)) {
+ types.add('decimal');
+ }
+ return types;
+ }
+
@override
Future handlePayload(String type, Map payload) async {
try {
@@ -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) {
diff --git a/lib/matrix_api.dart b/lib/matrix_api.dart
index d364cf8..be120a3 100644
--- a/lib/matrix_api.dart
+++ b/lib/matrix_api.dart
@@ -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';
diff --git a/lib/matrix_api/matrix_api.dart b/lib/matrix_api/matrix_api.dart
index 80effc0..d0d716d 100644
--- a/lib/matrix_api/matrix_api.dart
+++ b/lib/matrix_api/matrix_api.dart
@@ -36,8 +36,8 @@ import 'package:mime_type/mime_type.dart';
import 'package:moor/moor.dart';
import 'model/device.dart';
-import 'model/matrix_device_keys.dart';
import 'model/matrix_event.dart';
+import 'model/matrix_keys.dart';
import 'model/event_context.dart';
import 'model/events_sync_update.dart';
import 'model/login_response.dart';
@@ -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 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 uploadKeySignatures(
+ List keys) async {
+ final payload = {};
+ for (final key in keys) {
+ if (key.identifier == null ||
+ key.signatures == null ||
+ key.signatures.isEmpty) {
+ continue;
+ }
+ if (!payload.containsKey(key.userId)) {
+ payload[key.userId] = {};
+ }
+ if (payload[key.userId].containsKey(key.identifier)) {
+ // we need to merge signature objects
+ payload[key.userId][key.identifier]['signatures']
+ .addAll(key.signatures);
+ } else {
+ // we can just add signatures
+ payload[key.userId][key.identifier] = key.toJson();
+ }
+ }
+ final response = await request(
+ RequestType.POST,
+ '/client/r0/keys/signatures/upload',
+ data: payload,
+ );
+ return UploadKeySignaturesResponse.fromJson(response);
+ }
+
/// Gets all currently active pushers for the authenticated user.
/// https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-pushers
Future> requestPushers() async {
@@ -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 createRoomKeysBackup(
+ RoomKeysAlgorithmType algorithm, Map 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 getRoomKeysBackup([String version]) async {
+ var url = '/client/unstable/room_keys/version';
+ if (version != null) {
+ url += '/${Uri.encodeComponent(version)}';
+ }
+ final ret = await request(
+ RequestType.GET,
+ url,
+ );
+ return RoomKeysVersionResponse.fromJson(ret);
+ }
+
+ /// Updates a room key backup
+ /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-version-version
+ Future updateRoomKeysBackup(String version,
+ RoomKeysAlgorithmType algorithm, Map 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 deleteRoomKeysBackup(String version) async {
+ await request(
+ RequestType.DELETE,
+ '/client/unstable/room_keys/version/${Uri.encodeComponent(version)}',
+ );
+ }
+
+ /// Stores a single room key
+ /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys-roomid-sessionid
+ Future storeRoomKeysSingleKey(String roomId,
+ String sessionId, String version, RoomKeysSingleKey session) async {
+ final ret = await request(
+ RequestType.PUT,
+ '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}',
+ data: session.toJson(),
+ );
+ return RoomKeysUpdateResponse.fromJson(ret);
+ }
+
+ /// Gets a single room key
+ /// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-keys-roomid-sessionid
+ Future getRoomKeysSingleKey(
+ String roomId, String sessionId, String version) async {
+ final ret = await request(
+ RequestType.GET,
+ '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}',
+ );
+ return RoomKeysSingleKey.fromJson(ret);
+ }
+
+ /// Deletes a single room key
+ /// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid-sessionid
+ Future deleteRoomKeysSingleKey(
+ String roomId, String sessionId, String version) async {
+ final ret = await request(
+ RequestType.DELETE,
+ '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}',
+ );
+ return RoomKeysUpdateResponse.fromJson(ret);
+ }
+
+ /// Stores room keys for a room
+ /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys-roomid
+ Future storeRoomKeysRoom(
+ String roomId, String version, RoomKeysRoom keys) async {
+ final ret = await request(
+ RequestType.PUT,
+ '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}?version=${Uri.encodeComponent(version)}',
+ data: keys.toJson(),
+ );
+ return RoomKeysUpdateResponse.fromJson(ret);
+ }
+
+ /// Gets room keys for a room
+ /// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-keys-roomid
+ Future getRoomKeysRoom(String roomId, String version) async {
+ final ret = await request(
+ RequestType.GET,
+ '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}?version=${Uri.encodeComponent(version)}',
+ );
+ return RoomKeysRoom.fromJson(ret);
+ }
+
+ /// Deletes room ekys for a room
+ /// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid
+ Future deleteRoomKeysRoom(
+ String roomId, String version) async {
+ final ret = await request(
+ RequestType.DELETE,
+ '/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}?version=${Uri.encodeComponent(version)}',
+ );
+ return RoomKeysUpdateResponse.fromJson(ret);
+ }
+
+ /// Store multiple room keys
+ /// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys
+ Future storeRoomKeys(
+ String version, RoomKeys keys) async {
+ final ret = await request(
+ RequestType.PUT,
+ '/client/unstable/room_keys/keys?version=${Uri.encodeComponent(version)}',
+ data: keys.toJson(),
+ );
+ return RoomKeysUpdateResponse.fromJson(ret);
+ }
+
+ /// get all room keys
+ /// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-keys
+ Future getRoomKeys(String version) async {
+ final ret = await request(
+ RequestType.GET,
+ '/client/unstable/room_keys/keys?version=${Uri.encodeComponent(version)}',
+ );
+ return RoomKeys.fromJson(ret);
+ }
+
+ /// delete all room keys
+ /// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys
+ Future deleteRoomKeys(String version) async {
+ final ret = await request(
+ RequestType.DELETE,
+ '/client/unstable/room_keys/keys?version=${Uri.encodeComponent(version)}',
+ );
+ return RoomKeysUpdateResponse.fromJson(ret);
+ }
}
diff --git a/lib/matrix_api/model/keys_query_response.dart b/lib/matrix_api/model/keys_query_response.dart
index 8c6bb32..57bc16e 100644
--- a/lib/matrix_api/model/keys_query_response.dart
+++ b/lib/matrix_api/model/keys_query_response.dart
@@ -16,11 +16,14 @@
* along with this program. If not, see .
*/
-import 'matrix_device_keys.dart';
+import 'matrix_keys.dart';
class KeysQueryResponse {
Map failures;
Map> deviceKeys;
+ Map masterKeys;
+ Map selfSigningKeys;
+ Map userSigningKeys;
KeysQueryResponse.fromJson(Map json) {
failures = Map.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 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;
}
}
diff --git a/lib/matrix_api/model/matrix_device_keys.dart b/lib/matrix_api/model/matrix_keys.dart
similarity index 52%
rename from lib/matrix_api/model/matrix_device_keys.dart
rename to lib/matrix_api/model/matrix_keys.dart
index 95a225b..c0f0038 100644
--- a/lib/matrix_api/model/matrix_device_keys.dart
+++ b/lib/matrix_api/model/matrix_keys.dart
@@ -16,38 +16,28 @@
* along with this program. If not, see .
*/
-class MatrixDeviceKeys {
+class MatrixSignableKey {
String userId;
- String deviceId;
- List algorithms;
+ String identifier;
Map keys;
Map> signatures;
Map 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 _json;
- MatrixDeviceKeys(
- this.userId,
- this.deviceId,
- this.algorithms,
- this.keys,
- this.signatures, {
- this.unsigned,
- });
-
- MatrixDeviceKeys.fromJson(Map json) {
+ MatrixSignableKey.fromJson(Map json) {
_json = json;
userId = json['user_id'];
- deviceId = json['device_id'];
- algorithms = json['algorithms'].cast();
keys = Map.from(json['keys']);
- signatures = Map>.from(
- (json['signatures'] as Map)
- .map((k, v) => MapEntry(k, Map.from(v))));
- unsigned = json['unsigned'] != null
+ signatures = json['signatures'] is Map
+ ? Map>.from((json['signatures'] as Map)
+ .map((k, v) => MapEntry(k, Map.from(v))))
+ : null;
+ unsigned = json['unsigned'] is Map
? Map.from(json['unsigned'])
: null;
}
@@ -55,8 +45,6 @@ class MatrixDeviceKeys {
Map toJson() {
final data = _json ?? {};
data['user_id'] = userId;
- data['device_id'] = deviceId;
- data['algorithms'] = algorithms;
data['keys'] = keys;
if (signatures != null) {
@@ -68,3 +56,60 @@ class MatrixDeviceKeys {
return data;
}
}
+
+class MatrixCrossSigningKey extends MatrixSignableKey {
+ List usage;
+ String get publicKey => identifier;
+
+ MatrixCrossSigningKey(
+ String userId,
+ this.usage,
+ Map keys,
+ Map> signatures, {
+ Map unsigned,
+ }) : super(userId, keys?.values?.first, keys, signatures, unsigned: unsigned);
+
+ @override
+ MatrixCrossSigningKey.fromJson(Map json)
+ : super.fromJson(json) {
+ usage = List.from(json['usage']);
+ identifier = keys?.values?.first;
+ }
+
+ @override
+ Map toJson() {
+ final data = super.toJson();
+ data['usage'] = usage;
+ return data;
+ }
+}
+
+class MatrixDeviceKeys extends MatrixSignableKey {
+ String get deviceId => identifier;
+ List algorithms;
+ String get deviceDisplayName =>
+ unsigned != null ? unsigned['device_display_name'] : null;
+
+ MatrixDeviceKeys(
+ String userId,
+ String deviceId,
+ this.algorithms,
+ Map keys,
+ Map> signatures, {
+ Map unsigned,
+ }) : super(userId, deviceId, keys, signatures, unsigned: unsigned);
+
+ @override
+ MatrixDeviceKeys.fromJson(Map json) : super.fromJson(json) {
+ identifier = json['device_id'];
+ algorithms = json['algorithms'].cast();
+ }
+
+ @override
+ Map toJson() {
+ final data = super.toJson();
+ data['device_id'] = deviceId;
+ data['algorithms'] = algorithms;
+ return data;
+ }
+}
diff --git a/lib/matrix_api/model/room_keys_info.dart b/lib/matrix_api/model/room_keys_info.dart
new file mode 100644
index 0000000..f7a2cfe
--- /dev/null
+++ b/lib/matrix_api/model/room_keys_info.dart
@@ -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 .
+ */
+
+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 authData;
+ int count;
+ String etag;
+ String version;
+
+ RoomKeysVersionResponse.fromJson(Map 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 toJson() {
+ final data = {};
+ data['algorithm'] = algorithm?.algorithmString;
+ data['auth_data'] = authData;
+ data['count'] = count;
+ data['etag'] = etag;
+ data['version'] = version;
+ return data;
+ }
+}
diff --git a/lib/matrix_api/model/room_keys_keys.dart b/lib/matrix_api/model/room_keys_keys.dart
new file mode 100644
index 0000000..3b2c88e
--- /dev/null
+++ b/lib/matrix_api/model/room_keys_keys.dart
@@ -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 .
+ */
+
+class RoomKeysSingleKey {
+ int firstMessageIndex;
+ int forwardedCount;
+ bool isVerified;
+ Map sessionData;
+
+ RoomKeysSingleKey.fromJson(Map json) {
+ firstMessageIndex = json['first_message_index'];
+ forwardedCount = json['forwarded_count'];
+ isVerified = json['is_verified'];
+ sessionData = json['session_data'];
+ }
+
+ Map toJson() {
+ final data = {};
+ data['first_message_index'] = firstMessageIndex;
+ data['forwarded_count'] = forwardedCount;
+ data['is_verified'] = isVerified;
+ data['session_data'] = sessionData;
+ return data;
+ }
+}
+
+class RoomKeysRoom {
+ Map sessions;
+
+ RoomKeysRoom.fromJson(Map json) {
+ sessions = (json['sessions'] as Map)
+ .map((k, v) => MapEntry(k, RoomKeysSingleKey.fromJson(v)));
+ }
+
+ Map toJson() {
+ final data = {};
+ data['sessions'] = sessions.map((k, v) => MapEntry(k, v.toJson()));
+ return data;
+ }
+}
+
+class RoomKeys {
+ Map rooms;
+
+ RoomKeys.fromJson(Map json) {
+ rooms = (json['rooms'] as Map)
+ .map((k, v) => MapEntry(k, RoomKeysRoom.fromJson(v)));
+ }
+
+ Map toJson() {
+ final data = {};
+ data['rooms'] = rooms.map((k, v) => MapEntry(k, v.toJson()));
+ return data;
+ }
+}
+
+class RoomKeysUpdateResponse {
+ String etag;
+ int count;
+
+ RoomKeysUpdateResponse.fromJson(Map json) {
+ etag = json['etag']; // synapse replies an int but docs say string?
+ count = json['count'];
+ }
+
+ Map toJson() {
+ final data = {};
+ data['etag'] = etag;
+ data['count'] = count;
+ return data;
+ }
+}
diff --git a/lib/matrix_api/model/upload_key_signatures_response.dart b/lib/matrix_api/model/upload_key_signatures_response.dart
new file mode 100644
index 0000000..325aa5e
--- /dev/null
+++ b/lib/matrix_api/model/upload_key_signatures_response.dart
@@ -0,0 +1,55 @@
+/*
+ * Famedly Matrix SDK
+ * Copyright (C) 2020 Famedly GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import 'matrix_exception.dart';
+
+class UploadKeySignaturesResponse {
+ Map> failures;
+
+ UploadKeySignaturesResponse.fromJson(Map json) {
+ failures = json['failures'] != null
+ ? (json['failures'] as Map).map(
+ (k, v) => MapEntry(
+ k,
+ (v as Map).map((k, v) => MapEntry(
+ k,
+ MatrixException.fromJson(v),
+ )),
+ ),
+ )
+ : null;
+ }
+
+ Map toJson() {
+ final data = {};
+ if (failures != null) {
+ data['failures'] = failures.map(
+ (k, v) => MapEntry(
+ k,
+ v.map(
+ (k, v) => MapEntry(
+ k,
+ v.raw,
+ ),
+ ),
+ ),
+ );
+ }
+ return data;
+ }
+}
diff --git a/lib/src/client.dart b/lib/src/client.dart
index bbf87e7..e61b13e 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -56,16 +56,23 @@ class Client {
Encryption encryption;
+ Set verificationMethods;
+
/// Create a client
/// clientName = unique identifier of this client
/// debug: Print debug output?
/// database: The database instance to use
/// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions
+ /// verificationMethods: A set of all the verification methods this client can handle. Includes:
+ /// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
+ /// KeyVerificationMethod.emoji: Compare emojis
Client(this.clientName,
{this.debug = false,
this.database,
this.enableE2eeRecovery = false,
+ this.verificationMethods,
http.Client httpClient}) {
+ verificationMethods ??= {};
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 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 = {};
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.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.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 message, {
bool encrypted = true,
List toUsers,
+ bool onlyVerified = false,
}) async {
if (encrypted && !encryptionEnabled) return;
- // Don't send this message to blocked devices.
+ // Don't send this message to blocked devices, and if specified onlyVerified
+ // then only send it to verified devices
if (deviceKeys.isNotEmpty) {
deviceKeys.removeWhere((DeviceKeys deviceKeys) =>
- deviceKeys.blocked || deviceKeys.deviceId == deviceID);
+ deviceKeys.blocked ||
+ deviceKeys.deviceId == deviceID ||
+ (onlyVerified && !deviceKeys.verified));
if (deviceKeys.isEmpty) return;
}
diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart
index 136ce64..92d8c6b 100644
--- a/lib/src/database/database.dart
+++ b/lib/src/database/database.dart
@@ -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