From c13f66c85fa42c9b03d5fb15cbb875f28fdf5850 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 25 May 2020 15:30:53 +0200 Subject: [PATCH] in theory sign others keys --- lib/src/client.dart | 8 ++ lib/src/cross_signing.dart | 126 +++++++++++++++++++++++++++ lib/src/ssss.dart | 25 ++++-- lib/src/utils/device_keys_list.dart | 42 +++++++-- lib/src/utils/key_verification.dart | 127 ++++++++++++++++------------ 5 files changed, 261 insertions(+), 67 deletions(-) create mode 100644 lib/src/cross_signing.dart diff --git a/lib/src/client.dart b/lib/src/client.dart index c5e80681..42f348db 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -48,6 +48,7 @@ import 'package:pedantic/pedantic.dart'; import 'event.dart'; import 'room.dart'; import 'ssss.dart'; +import 'cross_signing.dart'; import 'sync/event_update.dart'; import 'sync/room_update.dart'; import 'sync/user_update.dart'; @@ -81,6 +82,7 @@ class Client { bool enableE2eeRecovery; SSSS ssss; + CrossSigning crossSigning; /// Create a client /// clientName = unique identifier of this client @@ -90,6 +92,7 @@ class Client { Client(this.clientName, {this.debug = false, this.database, this.enableE2eeRecovery = false}) { ssss = SSSS(this); + crossSigning = CrossSigning(this); onLoginStateChanged.stream.listen((loginState) { print('LoginState: ${loginState.toString()}'); }); @@ -1845,6 +1848,11 @@ class Client { return payload; } + /// Just gets the signature of a string + String signString(String s) { + return _olmAccount.sign(s); + } + /// Checks the signature of a signed json object. bool checkJsonSignature(String key, Map signedJson, String userId, String deviceId) { diff --git a/lib/src/cross_signing.dart b/lib/src/cross_signing.dart new file mode 100644 index 00000000..af1c3fba --- /dev/null +++ b/lib/src/cross_signing.dart @@ -0,0 +1,126 @@ +import 'dart:typed_data'; +import 'dart:convert'; + +import 'package:olm/olm.dart' as olm; + +import 'client.dart'; +import 'utils/device_keys_list.dart'; + +const SELF_SIGNING_KEY = 'm.cross_signing.self_signing'; +const USER_SIGNING_KEY = 'm.cross_signing.user_signing'; +const MASTER_KEY = 'm.cross_signing.master'; + +class CrossSigning { + final Client client; + CrossSigning(this.client); + + bool get enabled => + client.accountData[SELF_SIGNING_KEY] != null && + client.accountData[USER_SIGNING_KEY] != null && + client.accountData[MASTER_KEY] != null; + + Future isCached() async { + if (!enabled) { + return false; + } + return (await client.ssss.getCached(SELF_SIGNING_KEY)) != null && + (await client.ssss.getCached(USER_SIGNING_KEY)) != null; + } + + bool signable(List keys) { + for (final key in keys) { + if (key is CrossSigningKey && key.usage.contains('master')) { + return true; + } + if (key.userId == client.userID && + (key is DeviceKeys) && + key.identifier != client.deviceID) { + return true; + } + } + return false; + } + + Future sign(List keys) async { + Uint8List selfSigningKey; + Uint8List userSigningKey; + final signatures = {}; + var signedKey = false; + final addSignature = + (SignedKey key, SignedKey signedWith, String signature) { + if (key == null || signedWith == null || signature == null) { + return; + } + if (!signatures.containsKey(key.userId)) { + signatures[key.userId] = {}; + } + if (!signatures[key.userId].containsKey(key.identifier)) { + signatures[key.userId][key.identifier] = key.toJson(); + } + if (!signatures[key.userId][key.identifier].containsKey('signatures')) { + signatures[key.userId][key.identifier] + ['signatures'] = {}; + } + if (!signatures[key.userId][key.identifier]['signatures'] + .containsKey(signedWith.userId)) { + signatures[key.userId][key.identifier]['signatures'] + [signedWith.userId] = {}; + } + signatures[key.userId][key.identifier]['signatures'][signedWith.userId] + [signedWith.identifier] = signature; + signedKey = true; + }; + for (final key in keys) { + if (key.userId == client.userID) { + // we are singing a key of ourself + if (key is CrossSigningKey) { + if (key.usage.contains('master')) { + // okay, we'll sign our own master key + final signature = client.signString(key.signingContent); + addSignature( + key, + client.userDeviceKeys[client.userID].deviceKeys[client.deviceID], + signature); + } + // we don't care about signing other cross-signing keys + } else if (key.identifier != client.deviceID) { + // okay, we'll sign a device key with our self signing key + selfSigningKey ??= + base64.decode(await client.ssss.getCached(SELF_SIGNING_KEY) ?? ''); + if (selfSigningKey != null) { + final signature = _sign(key.signingContent, selfSigningKey); + addSignature(key, client.userDeviceKeys[client.userID].selfSigningKey, + signature); + } + } + } else if (key is CrossSigningKey && key.usage.contains('master')) { + // we are signing someone elses master key + userSigningKey ??= + base64.decode(await client.ssss.getCached(USER_SIGNING_KEY) ?? ''); + if (userSigningKey != null) { + final signature = _sign(key.signingContent, userSigningKey); + addSignature( + key, client.userDeviceKeys[client.userID].userSigningKey, signature); + } + } + } + if (signedKey) { + // post our new keys! + await client.jsonRequest( + type: HTTPType.POST, + action: '/client/r0/keys/signatures/upload', + data: signatures, + ); + } + } + + String _sign(String canonicalJson, Uint8List key) { + final keyObj = olm.PkSigning(); + keyObj.init_with_seed(key); + try { + return keyObj.sign(canonicalJson); + } finally { + keyObj.free(); + } + } +} diff --git a/lib/src/ssss.dart b/lib/src/ssss.dart index 856229b0..9657d414 100644 --- a/lib/src/ssss.dart +++ b/lib/src/ssss.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:encrypt/encrypt.dart'; import 'package:crypto/crypto.dart'; -import "package:base58check/base58.dart"; +import 'package:base58check/base58.dart'; import 'package:password_hash/password_hash.dart'; import 'package:random_string/random_string.dart'; @@ -21,7 +21,7 @@ const ZERO_STR = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'; const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; -const base58 = const Base58Codec(BASE58_ALPHABET); +const base58 = Base58Codec(BASE58_ALPHABET); const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm const AES_BLOCKSIZE = 16; @@ -38,7 +38,6 @@ class SSSS { b[0] = 1; final aesKey = Hmac(sha256, prk.bytes).convert(utf8.encode(name) + b); b[0] = 2; - final a = aesKey.bytes + utf8.encode(name) + b; final hmacKey = Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b); return _DerivedKeys(aesKey: aesKey.bytes, hmacKey: hmacKey.bytes); @@ -239,6 +238,15 @@ class SSSS { } } + Future maybeCacheAll(String keyId, Uint8List key) async { + for (final type in CACHE_TYPES) { + final secret = await getCached(type); + if (secret == null) { + await getStored(type, keyId, key); + } + } + } + Future maybeRequestAll(List devices) async { for (final type in CACHE_TYPES) { final secret = await getCached(type); @@ -347,8 +355,7 @@ class SSSS { return null; } if (data.content['encrypted'] is Map) { - final keys = Set(); - String maybeKey; + final Set keys = {}; for (final key in data.content['encrypted'].keys) { keys.add(key); } @@ -369,9 +376,7 @@ class SSSS { } OpenSSSS open([String identifier]) { - if (identifier == null) { - identifier = defaultKeyId; - } + identifier ??= defaultKeyId; if (identifier == null) { throw 'Dont know what to open'; } @@ -454,4 +459,8 @@ class OpenSSSS { Future getStored(String type) async { return await ssss.getStored(type, keyId, privateKey); } + + Future maybeCacheAll() async { + await ssss.maybeCacheAll(keyId, privateKey); + } } diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 928d1c2e..75ec0c2c 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -15,6 +15,28 @@ class DeviceKeysList { Map deviceKeys = {}; Map crossSigningKeys = {}; + SignedKey getKey(String id) { + if (deviceKeys.containsKey(id)) { + return deviceKeys[id]; + } + if (crossSigningKeys.containsKey(id)) { + return crossSigningKeys[id]; + } + return null; + } + + CrossSigningKey getCrossSigningKey(String type) { + final keys = crossSigningKeys.values.where((k) => k.usage.contains(type)); + if (keys.isEmpty) { + return null; + } + return keys.first; + } + + CrossSigningKey get masterKey => getCrossSigningKey('master'); + CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing'); + CrossSigningKey get userSigningKey => getCrossSigningKey('user_signing'); + DeviceKeysList.fromDb( DbUserDeviceKey dbEntry, List childEntries, @@ -73,7 +95,7 @@ class DeviceKeysList { DeviceKeysList(this.userId); } -abstract class _SignedKey { +abstract class SignedKey { Client client; String userId; String identifier; @@ -106,7 +128,7 @@ abstract class _SignedKey { } } - String _getSigningContent() { + String get signingContent { final data = Map.from(content); // some old data might have the custom verified and blocked keys data.remove('verified'); @@ -121,7 +143,7 @@ abstract class _SignedKey { final olmutil = olm.Utility(); var valid = false; try { - olmutil.ed25519_verify(pubKey, _getSigningContent(), signature); + olmutil.ed25519_verify(pubKey, signingContent, signature); valid = true; } catch (_) { // bad signature @@ -152,7 +174,7 @@ abstract class _SignedKey { continue; } final keyId = fullKeyId.substring('ed25519:'.length); - _SignedKey key; + SignedKey key; if (client.userDeviceKeys[otherUserId].deviceKeys.containsKey(keyId)) { key = client.userDeviceKeys[otherUserId].deviceKeys[keyId]; } else if (client.userDeviceKeys[otherUserId].crossSigningKeys @@ -204,6 +226,10 @@ abstract class _SignedKey { return false; } + Future setVerified(bool newVerified); + + Future setBlocked(bool newBlocked); + Map toJson() { final data = Map.from(content); // some old data may have the verified and blocked keys which are unneeded now @@ -216,19 +242,21 @@ abstract class _SignedKey { String toString() => json.encode(toJson()); } -class CrossSigningKey extends _SignedKey { +class CrossSigningKey extends SignedKey { String get publicKey => identifier; List usage; bool get isValid => userId != null && publicKey != null && keys != null && ed25519Key != null; + @override Future setVerified(bool newVerified) { _verified = newVerified; return client.database?.setVerifiedUserCrossSigningKey( newVerified, client.id, userId, publicKey); } + @override Future setBlocked(bool newBlocked) { blocked = newBlocked; return client.database?.setBlockedUserCrossSigningKey( @@ -267,7 +295,7 @@ class CrossSigningKey extends _SignedKey { } } -class DeviceKeys extends _SignedKey { +class DeviceKeys extends SignedKey { String get deviceId => identifier; List algorithms; Map unsigned; @@ -281,12 +309,14 @@ class DeviceKeys extends _SignedKey { curve25519Key != null && ed25519Key != null; + @override Future setVerified(bool newVerified) { _verified = newVerified; return client.database ?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId); } + @override Future setBlocked(bool newBlocked) { blocked = newBlocked; for (var room in client.rooms) { diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 9f30d828..d0d9d6c1 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:random_string/random_string.dart'; import 'package:canonical_json/canonical_json.dart'; +import 'package:pedantic/pedantic.dart'; import 'package:olm/olm.dart' as olm; import 'device_keys_list.dart'; import '../client.dart'; @@ -44,6 +45,7 @@ import '../room.dart'; enum KeyVerificationState { askAccept, + askSSSS, waitingAccept, askSas, waitingSas, @@ -103,6 +105,8 @@ class KeyVerification { _KeyVerificationMethod method; List possibleMethods; Map startPaylaod; + String _nextAction; + List _verifiedDevices; DateTime lastActivity; String lastStep; @@ -130,7 +134,7 @@ class KeyVerification { : null); } - Future start() async { + Future sendStart() async { if (room == null) { transactionId = randomString(512) + DateTime.now().millisecondsSinceEpoch.toString(); @@ -141,6 +145,17 @@ class KeyVerification { }); startedVerification = true; setState(KeyVerificationState.waitingAccept); + lastActivity = DateTime.now(); + } + + Future start() async { + if (client.crossSigning.enabled && + !(await client.crossSigning.isCached())) { + setState(KeyVerificationState.askSSSS); + _nextAction = 'request'; + } else { + await sendStart(); + } } Future handlePayload(String type, Map payload, @@ -228,6 +243,29 @@ class KeyVerification { } } + Future openSSSS( + {String password, String recoveryKey, bool skip = false}) async { + final next = () { + if (_nextAction == 'request') { + sendStart(); + } else if (_nextAction == 'done') { + if (_verifiedDevices != null) { + // and now let's sign them all in the background + client.crossSigning.sign(_verifiedDevices); + } + setState(KeyVerificationState.done); + } + }; + if (skip) { + next(); + return; + } + final handle = client.ssss.open('m.cross_signing.user_signing'); + await handle.unlock(password: password, recoveryKey: recoveryKey); + await handle.maybeCacheAll(); + next(); + } + /// called when the user accepts an incoming verification Future acceptVerification() async { if (!(await verifyLastStep( @@ -293,8 +331,8 @@ class KeyVerification { } Future verifyKeys(Map keys, - Future Function(String, dynamic) verifier) async { - final verifiedDevices = []; + Future Function(String, SignedKey) verifier) async { + _verifiedDevices = []; if (!client.userDeviceKeys.containsKey(userId)) { await cancel('m.key_mismatch'); @@ -304,53 +342,48 @@ class KeyVerification { final keyId = entry.key; final verifyDeviceId = keyId.substring('ed25519:'.length); final keyInfo = entry.value; - if (client.userDeviceKeys[userId].deviceKeys - .containsKey(verifyDeviceId)) { - if (!(await verifier(keyInfo, - client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]))) { + final key = client.userDeviceKeys[userId].getKey(verifyDeviceId); + if (key != null) { + if (!(await verifier(keyInfo, key))) { await cancel('m.key_mismatch'); return; } - verifiedDevices.add(verifyDeviceId); - } else if (client.userDeviceKeys[userId].crossSigningKeys - .containsKey(verifyDeviceId)) { - // this is a cross signing key! - if (!(await verifier(keyInfo, - client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId]))) { - await cancel('m.key_mismatch'); - return; - } - verifiedDevices.add(verifyDeviceId); + _verifiedDevices.add(key); } } // okay, we reached this far, so all the devices are verified! var verifiedMasterKey = false; - final verifiedUserDevices = []; - for (final verifyDeviceId in verifiedDevices) { - if (client.userDeviceKeys[userId].deviceKeys - .containsKey(verifyDeviceId)) { - final key = client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]; - await key.setVerified(true); - verifiedUserDevices.add(key); - } else if (client.userDeviceKeys[userId].crossSigningKeys - .containsKey(verifyDeviceId)) { - final key = - client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId]; - await key.setVerified(true); - if (key.usage.contains('master')) { - verifiedMasterKey = true; - } + for (final key in _verifiedDevices) { + await key.setVerified(true); + if (key is CrossSigningKey && key.usage.contains('master')) { + verifiedMasterKey = true; } } - if (verifiedMasterKey) { - if (userId == client.userID) { - // it was our own master key, let's request the cross signing keys - // we do it in the background, thus no await needed here - client.ssss.maybeRequestAll(verifiedUserDevices); + if (verifiedMasterKey && userId == client.userID) { + // it was our own master key, let's request the cross signing keys + // we do it in the background, thus no await needed here + unawaited(client.ssss.maybeRequestAll( + _verifiedDevices.whereType().toList())); + } + await send('m.key.verification.done', {}); + + var askingSSSS = false; + if (client.crossSigning.enabled && + client.crossSigning.signable(_verifiedDevices)) { + // these keys can be signed! Let's do so + if (await client.crossSigning.isCached()) { + // and now let's sign them all in the background + unawaited(client.crossSigning.sign(_verifiedDevices)); } else { - // it was someone elses master key, let's sign it + askingSSSS = true; } } + if (askingSSSS) { + setState(KeyVerificationState.askSSSS); + _nextAction = 'done'; + } else { + setState(KeyVerificationState.done); + } } Future verifyActivity() async { @@ -729,22 +762,10 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { mac[entry.key] = entry.value; } } - await request.verifyKeys(mac, (String mac, dynamic device) async { - if (device is DeviceKeys) { - return mac == - _calculateMac( - device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId); - } else if (device is CrossSigningKey) { - return mac == - _calculateMac( - device.ed25519Key, baseInfo + 'ed25519:' + device.publicKey); - } - return false; + await request.verifyKeys(mac, (String mac, SignedKey key) async { + return mac == + _calculateMac(key.ed25519Key, baseInfo + 'ed25519:' + key.identifier); }); - await request.send('m.key.verification.done', {}); - if (request.state != KeyVerificationState.error) { - request.setState(KeyVerificationState.done); - } } String _makeCommitment(String pubKey, String canonicalJson) {