From a4c693558dac65641587c674f0291446cd9d50f4 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 18 May 2020 11:44:23 +0200 Subject: [PATCH] generalize verification methods --- lib/src/utils/key_verification.dart | 599 ++++++++++++++++------------ 1 file changed, 352 insertions(+), 247 deletions(-) diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 4a150bd..3636351 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -42,13 +42,47 @@ import '../room.dart'; | | */ -final KNOWN_KEY_AGREEMENT_PROTOCOLS = ['curve25519-hkdf-sha256', 'curve25519']; -final KNOWN_HASHES = ['sha256']; -final KNOWN_MESSAGE_AUTHENTIFICATION_CODES = ['hkdf-hmac-sha256']; -final KNOWN_AUTHENTICATION_TYPES = ['emoji', 'decimal']; - enum KeyVerificationState { askAccept, waitingAccept, askSas, waitingSas, done, error } +List _intersect(List a, List b) { + final res = []; + for (final v in a) { + if (b.contains(v)) { + res.add(v); + } + } + return res; +} + +List _bytesToInt(Uint8List bytes, int totalBits) { + final ret = []; + var current = 0; + var numBits = 0; + for (final byte in bytes) { + for (final bit in [7, 6, 5, 4, 3, 2, 1, 0]) { + numBits++; + if ((byte & (1 << bit)) > 0) { + current += 1 << (totalBits - numBits); + } + if (numBits >= totalBits) { + ret.add(current); + current = 0; + numBits = 0; + } + } + } + return ret; +} + +final VERIFICATION_METHODS = [_KeyVerificationMethodSas.type]; + +_KeyVerificationMethod _makeVerificationMethod(String type, KeyVerification request) { + if (type == _KeyVerificationMethodSas.type) { + return _KeyVerificationMethodSas(request: request); + } + throw 'Unkown method type'; +} + class KeyVerification { String transactionId; final Client client; @@ -57,16 +91,10 @@ class KeyVerification { void Function() onUpdate; String get deviceId => _deviceId; String _deviceId; - olm.SAS sas; bool startedVerification = false; - - String keyAgreementProtocol; - String hash; - String messageAuthenticationCode; - List authenticationTypes; - String startCanonicalJson; - String commitment; - String theirPublicKey; + _KeyVerificationMethod method; + List possibleMethods; + Map startPaylaod; DateTime lastActivity; String lastStep; @@ -76,8 +104,6 @@ class KeyVerification { String canceledCode; String canceledReason; - Map macPayload; - KeyVerification({this.client, this.room, this.userId, String deviceId, this.onUpdate}) { lastActivity = DateTime.now(); _deviceId ??= deviceId; @@ -85,7 +111,7 @@ class KeyVerification { void dispose() { print('[Key Verification] disposing object...'); - sas?.free(); + method?.dispose(); } static String getTransactionId(Map payload) { @@ -99,11 +125,11 @@ class KeyVerification { transactionId = randomString(512); } await send('m.key.verification.request', { - 'methods': ['m.sas.v1'], + 'methods': VERIFICATION_METHODS, 'timestamp': DateTime.now().millisecondsSinceEpoch, }); startedVerification = true; - _setState(KeyVerificationState.waitingAccept); + setState(KeyVerificationState.waitingAccept); } Future handlePayload(String type, Map payload, [String eventId]) async { @@ -113,12 +139,6 @@ class KeyVerification { case 'm.key.verification.request': _deviceId ??= payload['from_device']; transactionId ??= eventId ?? payload['transaction_id']; - // verify it has a method we can use - if (!(payload['methods'] is List && payload['methods'].contains('m.sas.v1'))) { - // reject it outright - await cancel('m.unknown_method'); - return; - } // verify the timestamp final now = DateTime.now(); final verifyTime = DateTime.fromMillisecondsSinceEpoch(payload['timestamp']); @@ -126,11 +146,26 @@ class KeyVerification { await cancel('m.timeout'); return; } - _setState(KeyVerificationState.askAccept); + // verify it has a method we can use + possibleMethods = _intersect(VERIFICATION_METHODS, payload['methods']); + if (possibleMethods.isEmpty) { + // reject it outright + await cancel('m.unknown_method'); + return; + } + setState(KeyVerificationState.askAccept); break; case 'm.key.verification.ready': - await _sendStart(); - _setState(KeyVerificationState.waitingAccept); + possibleMethods = _intersect(VERIFICATION_METHODS, payload['methods']); + if (possibleMethods.isEmpty) { + // reject it outright + await cancel('m.unknown_method'); + return; + } + // TODO: Pick method? + method = _makeVerificationMethod(possibleMethods.first, this); + await method.sendStart(); + setState(KeyVerificationState.waitingAccept); break; case 'm.key.verification.start': _deviceId ??= payload['from_device']; @@ -138,51 +173,20 @@ class KeyVerification { if (!(await verifyLastStep(['m.key.verification.request', null]))) { return; // abort } - if (!_validateStart(payload)) { + if (!VERIFICATION_METHODS.contains(payload['method'])) { await cancel('m.unknown_method'); return; } + method = _makeVerificationMethod(payload['method'], this); if (lastStep == null) { - // we need to ask the user for verification - _setState(KeyVerificationState.askAccept); - } else { - await _sendAccept(); - } - break; - case 'm.key.verification.accept': - if (!(await verifyLastStep(['m.key.verification.ready', null]))) { - return; - } - if (!_handleAccept(payload)) { - await cancel('m.unknown_method'); - return; - } - await _sendKey(); - break; - case 'm.key.verification.key': - if (!(await verifyLastStep(['m.key.verification.accept', 'm.key.verification.start']))) { - return; - } - _handleKey(payload); - if (lastStep == 'm.key.verification.start') { - // we need to send our key - await _sendKey(); - } else { - // we already sent our key, time to verify the commitment being valid - if (!_validateCommitment()) { - await cancel('m.mismatched_commitment'); + if (!method.validateStart(payload)) { + await cancel('m.unknown_method'); return; } - } - _setState(KeyVerificationState.askSas); - break; - case 'm.key.verification.mac': - if (!(await verifyLastStep(['m.key.verification.key']))) { - return; - } - macPayload = payload; - if (state == KeyVerificationState.waitingSas) { - await _processMac(); + startPaylaod = payload; + setState(KeyVerificationState.askAccept); + } else { + await method.handlePayload(type, payload); } break; case 'm.key.verification.done': @@ -192,10 +196,11 @@ class KeyVerification { canceled = true; canceledCode = payload['code']; canceledReason = payload['reason']; - _setState(KeyVerificationState.error); + setState(KeyVerificationState.error); break; default: - return; + await method.handlePayload(type, payload); + break; } lastStep = type; } catch (err, stacktrace) { @@ -212,15 +217,15 @@ class KeyVerification { if (!(await verifyLastStep(['m.key.verification.request', 'm.key.verification.start']))) { return; } - _setState(KeyVerificationState.waitingAccept); + setState(KeyVerificationState.waitingAccept); if (lastStep == 'm.key.verification.request') { // we need to send a ready event await send('m.key.verification.ready', { - 'methods': ['m.sas.v1'], + 'methods': possibleMethods, }); } else { // we need to send an accept event - await _sendAccept(); + await method.handlePayload('m.key.verification.start', startPaylaod); } } @@ -232,43 +237,276 @@ class KeyVerification { await cancel('m.user'); } + Future acceptSas() async { + if (method is _KeyVerificationMethodSas) { + await (method as _KeyVerificationMethodSas).acceptSas(); + } + } + + Future rejectSas() async { + if (method is _KeyVerificationMethodSas) { + await (method as _KeyVerificationMethodSas).rejectSas(); + } + } + + List get sasNumbers { + if (method is _KeyVerificationMethodSas) { + return _bytesToInt((method as _KeyVerificationMethodSas).makeSas(5), 13).map((n) => n + 1000).toList(); + } + return []; + } + + List get sasTypes { + if (method is _KeyVerificationMethodSas) { + return (method as _KeyVerificationMethodSas).authenticationTypes; + } + return []; + } + + List get sasEmojis { + if (method is _KeyVerificationMethodSas) { + final numbers = _bytesToInt((method as _KeyVerificationMethodSas).makeSas(6), 6); + return numbers.map((n) => KeyVerificationEmoji(n)).toList().sublist(0, 7); + } + return []; + } + + Future verifyKeys(Map keys, Future Function(String, DeviceKeys) verifier) async { + final verifiedDevices = []; + + if (!client.userDeviceKeys.containsKey(userId)) { + await cancel('m.key_mismatch'); + return; + } + for (final entry in keys.entries) { + 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]))) { + 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 + } + } + // 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); + } + } + + Future verifyActivity() async { + if (lastActivity != null && lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) { + lastActivity = DateTime.now(); + return true; + } + await cancel('m.timeout'); + return false; + } + + Future verifyLastStep(List checkLastStep) async { + if (!(await verifyActivity())) { + return false; + } + if (checkLastStep.contains(lastStep)) { + return true; + } + await cancel('m.unexpected_message'); + return false; + } + + Future cancel([String code = 'm.unknown']) async { + await send('m.key.verification.cancel', { + 'reason': code, + 'code': code, + }); + canceled = true; + canceledCode = code; + setState(KeyVerificationState.error); + } + + void makePayload(Map payload) { + payload['from_device'] = client.deviceID; + if (transactionId != null) { + if (room != null) { + payload['m.relates_to'] = { + 'rel_type': 'm.reference', + 'event_id': transactionId, + }; + } else { + payload['transaction_id'] = transactionId; + } + } + } + + Future send(String type, Map payload) async { + makePayload(payload); + print('[Key Verification] Sending type ${type}: ' + payload.toString()); + print('[Key Verification] Sending to ${userId} device ${deviceId}'); + if (room != null) { + if (['m.key.verification.request'].contains(type)) { + payload['msgtype'] = type; + payload['to'] = userId; + payload['body'] = 'Attempting verification request. (${type}) Apparently your client doesn\'t support this'; + type = 'm.room.message'; + } + final newTransactionId = await room.sendEvent(payload, type: type); + if (transactionId == null) { + transactionId = newTransactionId; + client.addKeyVerificationRequest(this); + } + } else { + await client.sendToDevice([client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload); + } + } + + void setState(KeyVerificationState newState) { + if (state != KeyVerificationState.error) { + state = newState; + } + if (onUpdate != null) { + onUpdate(); + } + } +} + +abstract class _KeyVerificationMethod { + KeyVerification request; + Client client; + _KeyVerificationMethod({this.request}) { + client = request.client; + } + + static String type; + + Future handlePayload(String type, Map payload); + bool validateStart(Map payload) { + return false; + } + Future sendStart(); + void dispose() {} +} + +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); + + @override + static String type = 'm.sas.v1'; + + String keyAgreementProtocol; + String hash; + String messageAuthenticationCode; + List authenticationTypes; + String startCanonicalJson; + String commitment; + String theirPublicKey; + Map macPayload; + olm.SAS sas; + + @override + void dispose() { + sas?.free(); + } + + @override + Future handlePayload(String type, Map payload) async { + try { + switch (type) { + case 'm.key.verification.start': + if (!(await request.verifyLastStep(['m.key.verification.request', 'm.key.verification.start']))) { + return; // abort + } + if (!validateStart(payload)) { + await request.cancel('m.unknown_method'); + return; + } + await _sendAccept(); + break; + case 'm.key.verification.accept': + if (!(await request.verifyLastStep(['m.key.verification.ready']))) { + return; + } + if (!_handleAccept(payload)) { + await request.cancel('m.unknown_method'); + return; + } + await _sendKey(); + break; + case 'm.key.verification.key': + if (!(await request.verifyLastStep(['m.key.verification.accept', 'm.key.verification.start']))) { + return; + } + _handleKey(payload); + if (request.lastStep == 'm.key.verification.start') { + // we need to send our key + await _sendKey(); + } else { + // we already sent our key, time to verify the commitment being valid + if (!_validateCommitment()) { + await request.cancel('m.mismatched_commitment'); + return; + } + } + request.setState(KeyVerificationState.askSas); + break; + case 'm.key.verification.mac': + if (!(await request.verifyLastStep(['m.key.verification.key']))) { + return; + } + macPayload = payload; + if (request.state == KeyVerificationState.waitingSas) { + await _processMac(); + } + break; + } + } catch (err, stacktrace) { + print('[Key Verification SAS] An error occured: ' + err.toString()); + print(stacktrace); + if (request.deviceId != null) { + await request.cancel('m.invalid_message'); + } + } + } + Future acceptSas() async { await _sendMac(); - _setState(KeyVerificationState.waitingSas); + request.setState(KeyVerificationState.waitingSas); if (macPayload != null) { await _processMac(); } } Future rejectSas() async { - await cancel('m.mismatched_sas'); + await request.cancel('m.mismatched_sas'); } - List get sasNumbers { - return _bytesToInt(_makeSas(5), 13).map((n) => n + 1000).toList(); - } - - List get sasEmojis { - final numbers = _bytesToInt(_makeSas(6), 6); - return numbers.map((n) => KeyVerificationEmoji(n)).toList().sublist(0, 7); - } - - Future _sendStart() async { + @override + Future sendStart() async { final payload = { - 'method': 'm.sas.v1', + 'method': type, 'key_agreement_protocols': KNOWN_KEY_AGREEMENT_PROTOCOLS, 'hashes': KNOWN_HASHES, 'message_authentication_codes': KNOWN_MESSAGE_AUTHENTIFICATION_CODES, 'short_authentication_string': KNOWN_AUTHENTICATION_TYPES, }; - _makePayload(payload); + request.makePayload(payload); // We just store the canonical json in here for later verification startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload)); - await send('m.key.verification.start', payload); + await request.send('m.key.verification.start', payload); } - bool _validateStart(Map payload) { - if (payload['method'] != 'm.sas.v1') { + @override + bool validateStart(Map payload) { + if (payload['method'] != type) { return false; } final possibleKeyAgreementProtocols = _intersect(KNOWN_KEY_AGREEMENT_PROTOCOLS, payload['key_agreement_protocols']); @@ -298,8 +536,8 @@ class KeyVerification { Future _sendAccept() async { sas = olm.SAS(); commitment = _makeCommitment(sas.get_pubkey(), startCanonicalJson); - await send('m.key.verification.accept', { - 'method': 'm.sas.v1', + await request.send('m.key.verification.accept', { + 'method': type, 'key_agreement_protocol': keyAgreementProtocol, 'hash': hash, 'message_authentication_code': messageAuthenticationCode, @@ -332,7 +570,7 @@ class KeyVerification { } Future _sendKey() async { - await send('m.key.verification.key', { + await request.send('m.key.verification.key', { 'key': sas.get_pubkey(), }); } @@ -347,21 +585,20 @@ class KeyVerification { return commitment == checkCommitment; } - Uint8List _makeSas(int bytes) { + Uint8List makeSas(int bytes) { var sasInfo = ''; if (keyAgreementProtocol == 'curve25519-hkdf-sha256') { final ourInfo = '${client.userID}|${client.deviceID}|${sas.get_pubkey()}|'; - final theirInfo = '${userId}|${deviceId}|${theirPublicKey}|'; - sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' + (startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + transactionId; + final theirInfo = '${request.userId}|${request.deviceId}|${theirPublicKey}|'; + sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' + (request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + request.transactionId; } else if (keyAgreementProtocol == 'curve25519') { final ourInfo = client.userID + client.deviceID; - final theirInfo = userId + deviceId; - sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' + (startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + transactionId; + final theirInfo = request.userId + request.deviceId; + sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' + (request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + request.transactionId; } else { throw 'Unknown key agreement protocol'; } - print('++++++++++++++++'); - print(keyAgreementProtocol); + // this is needed, else things don't match up? WTF?! print(sasInfo); return sas.generate_bytes(sasInfo, bytes); } @@ -369,8 +606,8 @@ class KeyVerification { Future _sendMac() async { final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + client.userID + client.deviceID + - userId + deviceId + - transactionId; + request.userId + request.deviceId + + request.transactionId; final mac = {}; final keyList = []; @@ -383,7 +620,7 @@ class KeyVerification { keyList.sort(); final keys = _calculateMac(keyList.join(','), baseInfo + 'KEY_IDS'); - await send('m.key.verification.mac', { + await request.send('m.key.verification.mac', { 'mac': mac, 'keys': keys, }); @@ -392,19 +629,19 @@ class KeyVerification { Future _processMac() async { final payload = macPayload; final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + - userId + deviceId + + request.userId + request.deviceId + client.userID + client.deviceID + - transactionId; + request.transactionId; final keyList = payload['mac'].keys.toList(); keyList.sort(); if (payload['keys'] != _calculateMac(keyList.join(','), baseInfo + 'KEY_IDS')) { - await cancel('m.key_mismatch'); + await request.cancel('m.key_mismatch'); return; } - if (!client.userDeviceKeys.containsKey(userId)) { - await cancel('m.key_mismatch'); + if (!client.userDeviceKeys.containsKey(request.userId)) { + await request.cancel('m.key_mismatch'); return; } final mac = {}; @@ -413,81 +650,15 @@ class KeyVerification { mac[entry.key] = entry.value; } } - await _verifyKeys(mac, (String mac, DeviceKeys device) async { + await request.verifyKeys(mac, (String mac, DeviceKeys device) async { return mac == _calculateMac(device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId); }); - await send('m.key.verification.done', {}); - if (state != KeyVerificationState.error) { - _setState(KeyVerificationState.done); + await request.send('m.key.verification.done', {}); + if (request.state != KeyVerificationState.error) { + request.setState(KeyVerificationState.done); } } - Future _verifyKeys(Map keys, Future Function(String, DeviceKeys) verifier) async { - final verifiedDevices = []; - - if (!client.userDeviceKeys.containsKey(userId)) { - await cancel('m.key_mismatch'); - return; - } - for (final entry in keys.entries) { - 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]))) { - 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 - } - } - // 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); - } - } - - String _calculateMac(String input, String info) { - if (messageAuthenticationCode == 'hkdf-hmac-sha256') { - return sas.calculate_mac(input, info); - } else { - throw 'Unknown message authentification code'; - } - } - - Future verifyActivity() async { - if (lastActivity != null && lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) { - lastActivity = DateTime.now(); - return true; - } - await cancel('m.timeout'); - return false; - } - - Future verifyLastStep(List checkLastStep) async { - if (!(await verifyActivity())) { - return false; - } - if (checkLastStep.contains(lastStep)) { - return true; - } - await cancel('m.unexpected_message'); - return false; - } - - Future cancel([String code = 'm.unknown']) async { - await send('m.key.verification.cancel', { - 'reason': code, - 'code': code, - }); - canceled = true; - canceledCode = code; - _setState(KeyVerificationState.error); - } - String _makeCommitment(String pubKey, String canonicalJson) { if (hash == 'sha256') { final olmutil = olm.Utility(); @@ -498,79 +669,13 @@ class KeyVerification { throw 'Unknown hash method'; } - void _makePayload(Map payload) { - payload['from_device'] = client.deviceID; - if (transactionId != null) { - if (room != null) { - payload['m.relates_to'] = { - 'rel_type': 'm.reference', - 'event_id': transactionId, - }; - } else { - payload['transaction_id'] = transactionId; - } - } - } - - Future send(String type, Map payload) async { - _makePayload(payload); - print('[Key Verification] Sending type ${type}: ' + payload.toString()); - print('[Key Verification] Sending to ${userId} device ${deviceId}'); - if (room != null) { - if (['m.key.verification.request'].contains(type)) { - payload['msgtype'] = type; - payload['to'] = userId; - payload['body'] = 'Attempting verification request. (${type}) Apparently your client doesn\'t support this'; - type = 'm.room.message'; - } - final newTransactionId = await room.sendEvent(payload, type: type); - if (transactionId == null) { - transactionId = newTransactionId; - client.addKeyVerificationRequest(this); - } + String _calculateMac(String input, String info) { + if (messageAuthenticationCode == 'hkdf-hmac-sha256') { + return sas.calculate_mac(input, info); } else { - await client.sendToDevice([client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload); + throw 'Unknown message authentification code'; } } - - void _setState(KeyVerificationState newState) { - if (state != KeyVerificationState.error) { - state = newState; - } - if (onUpdate != null) { - onUpdate(); - } - } - - List _intersect(List a, List b) { - final res = []; - for (final v in a) { - if (b.contains(v)) { - res.add(v); - } - } - return res; - } - - List _bytesToInt(Uint8List bytes, int totalBits) { - final ret = []; - var current = 0; - var numBits = 0; - for (final byte in bytes) { - for (final bit in [7, 6, 5, 4, 3, 2, 1, 0]) { - numBits++; - if ((byte & (1 << bit)) > 0) { - current += 1 << (totalBits - numBits); - } - if (numBits >= totalBits) { - ret.add(current); - current = 0; - numBits = 0; - } - } - } - return ret; - } } const _emojiMap = [