From 0219a42c07654cb4087a285eabd21831b9ff98fd Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sun, 17 May 2020 15:25:42 +0200 Subject: [PATCH 1/6] implement SAS --- lib/famedlysdk.dart | 1 + lib/src/client.dart | 57 ++ lib/src/room.dart | 6 +- lib/src/utils/device_keys_list.dart | 8 + lib/src/utils/key_verification.dart | 841 ++++++++++++++++++++++++++++ pubspec.lock | 19 +- pubspec.yaml | 10 +- 7 files changed, 928 insertions(+), 14 deletions(-) create mode 100644 lib/src/utils/key_verification.dart diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart index 25845a5..b8d778b 100644 --- a/lib/famedlysdk.dart +++ b/lib/famedlysdk.dart @@ -27,6 +27,7 @@ export 'package:famedlysdk/src/sync/room_update.dart'; export 'package:famedlysdk/src/sync/event_update.dart'; export 'package:famedlysdk/src/sync/user_update.dart'; export 'package:famedlysdk/src/utils/device_keys_list.dart'; +export 'package:famedlysdk/src/utils/key_verification.dart'; export 'package:famedlysdk/src/utils/matrix_exception.dart'; export 'package:famedlysdk/src/utils/matrix_file.dart'; export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart'; diff --git a/lib/src/client.dart b/lib/src/client.dart index 560c50d..1b521d6 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -56,6 +56,7 @@ import 'utils/profile.dart'; import 'database/database.dart' show Database; import 'utils/pusher.dart'; import 'utils/well_known_informations.dart'; +import 'utils/key_verification.dart'; typedef RoomSorter = int Function(Room a, Room b); @@ -633,6 +634,11 @@ class Client { final StreamController onRoomKeyRequest = StreamController.broadcast(); + /// Will be called when another device is requesting verification with this device. + final StreamController onKeyVerificationRequest = StreamController.broadcast(); + + final Map _keyVerificationRequests = {}; + /// Matrix synchronisation is done with https long polling. This needs a /// timeout which is usually 30 seconds. int syncTimeoutSec = 30; @@ -968,6 +974,7 @@ class Client { } prevBatch = syncResp['next_batch']; await _updateUserDeviceKeys(); + _cleanupKeyVerificationRequests(); if (hash == _syncRequest.hashCode) unawaited(_sync()); } on MatrixException catch (exception) { onError.add(exception); @@ -1055,6 +1062,28 @@ class Client { } } + void _cleanupKeyVerificationRequests() { + for (final entry in _keyVerificationRequests.entries) { + (() async { + var dispose = entry.value.canceled || entry.value.state == KeyVerificationState.done || entry.value.state == KeyVerificationState.error; + if (!dispose) { + dispose = !(await entry.value.verifyActivity()); + } + if (dispose) { + entry.value.dispose(); + _keyVerificationRequests.remove(entry.key); + } + })(); + } + } + + void addKeyVerificationRequest(KeyVerification request) { + if (request.transactionId == null) { + return; + } + _keyVerificationRequests[request.transactionId] = request; + } + void _handleToDeviceEvents(List events) { for (var i = 0; i < events.length; i++) { var isValid = events[i] is Map && @@ -1078,10 +1107,38 @@ class Client { } } _updateRoomsByToDeviceEvent(toDeviceEvent); + if (toDeviceEvent.type.startsWith('m.key.verification.')) { + _handleToDeviceKeyVerificationRequest(toDeviceEvent); + } onToDeviceEvent.add(toDeviceEvent); } } + void _handleToDeviceKeyVerificationRequest(ToDeviceEvent toDeviceEvent) { + if (!toDeviceEvent.type.startsWith('m.key.verification.')) { + return; + } + // we have key verification going on! + final transactionId = KeyVerification.getTransactionId(toDeviceEvent.content); + if (transactionId != null) { + if (_keyVerificationRequests.containsKey(transactionId)) { + _keyVerificationRequests[transactionId].handlePayload(toDeviceEvent.type, toDeviceEvent.content); + } else { + final newKeyRequest = KeyVerification(client: this, userId: toDeviceEvent.sender); + newKeyRequest.handlePayload(toDeviceEvent.type, toDeviceEvent.content).then((res) { + if (newKeyRequest.state != KeyVerificationState.askAccept) { + // okay, something went wrong (unknown transaction id?), just dispose it + newKeyRequest.dispose(); + } else { + // we have a new request! Let's broadcast it! + _keyVerificationRequests[transactionId] = newKeyRequest; + onKeyVerificationRequest.add(newKeyRequest); + } + }); + } + } + } + void _handleRooms(Map rooms, Membership membership, List Function()> dbActions) { rooms.forEach((String id, dynamic room) { diff --git a/lib/src/room.dart b/lib/src/room.dart index 3457b92..15ff171 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -740,8 +740,8 @@ class Room { /// Sends an event to this room with this json as a content. Returns the /// event ID generated from the server. Future sendEvent(Map content, - {String txid, Event inReplyTo}) async { - final type = 'm.room.message'; + {String type, String txid, Event inReplyTo}) async { + type = type ?? 'm.room.message'; final sendType = (encrypted && client.encryptionEnabled) ? 'm.room.encrypted' : type; @@ -796,7 +796,7 @@ class Room { type: HTTPType.PUT, action: '/client/r0/rooms/${id}/send/$sendType/$messageID', data: encrypted && client.encryptionEnabled - ? await encryptGroupMessagePayload(content) + ? await encryptGroupMessagePayload(content, type: type) : content); final String res = response['event_id']; eventUpdate.content['status'] = 1; diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index b4f5ba4..8a76e5d 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import '../client.dart'; import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey; import '../event.dart'; +import 'key_verification.dart'; class DeviceKeysList { String userId; @@ -137,4 +138,11 @@ class DeviceKeys { data['blocked'] = blocked; return data; } + + KeyVerification startVerification(Client client) { + final request = KeyVerification(client: client, userId: userId, deviceId: deviceId); + request.start(); + client.addKeyVerificationRequest(request); + return request; + } } diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart new file mode 100644 index 0000000..593206d --- /dev/null +++ b/lib/src/utils/key_verification.dart @@ -0,0 +1,841 @@ +import 'dart:typed_data'; +import 'package:random_string/random_string.dart'; +import 'package:canonical_json/canonical_json.dart'; +import 'package:olm/olm.dart' as olm; +import 'device_keys_list.dart'; +import '../client.dart'; +import '../room.dart'; + +/* + +-------------+ +-----------+ + | AliceDevice | | BobDevice | + +-------------+ +-----------+ + | | + | (m.key.verification.request) | + |-------------------------------->| (ASK FOR VERIFICATION REQUEST) + | | + | (m.key.verification.ready) | + |<--------------------------------| + | | + | (m.key.verification.start) | we will probably not send this + |<--------------------------------| for simplicities sake + | | + | m.key.verification.start | + |-------------------------------->| (ASK FOR VERIFICATION REQUEST) + | | + | m.key.verification.accept | + |<--------------------------------| + | | + | m.key.verification.key | + |-------------------------------->| + | | + | m.key.verification.key | + |<--------------------------------| + | | + | COMPARE EMOJI / NUMBERS | + | | + | m.key.verification.mac | + |-------------------------------->| success + | | + | m.key.verification.mac | + success |<--------------------------------| + | | +*/ + +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 } + +class KeyVerification { + String transactionId; + final Client client; + final Room room; + final String userId; + 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; + + DateTime lastActivity; + String lastStep; + + KeyVerificationState state = KeyVerificationState.waitingAccept; + bool canceled = false; + String canceledCode; + String canceledReason; + + Map macPayload; + + KeyVerification({this.client, this.room, this.userId, String deviceId, this.onUpdate}) { + lastActivity = DateTime.now(); + _deviceId ??= deviceId; + } + + void dispose() { + print('[Key Verification] disposing object...'); + sas?.free(); + } + + static String getTransactionId(Map payload) { + return payload['transaction_id'] ?? ( + payload['m.relates_to'] is Map ? payload['m.relates_to']['event_id'] : null + ); + } + + Future start() async { + if (room == null) { + transactionId = randomString(512); + } + await send('m.key.verification.request', { + 'methods': ['m.sas.v1'], + 'timestamp': DateTime.now().millisecondsSinceEpoch, + }); + startedVerification = true; + _setState(KeyVerificationState.waitingAccept); + } + + Future handlePayload(String type, Map payload, [String eventId]) async { + print('[Key Verification] Received type ${type}: ' + payload.toString()); + try { + switch (type) { + 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']); + if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) || now.add(Duration(minutes: 5)).isBefore(verifyTime)) { + await cancel('m.timeout'); + return; + } + _setState(KeyVerificationState.askAccept); + break; + case 'm.key.verification.ready': + await _sendStart(); + _setState(KeyVerificationState.waitingAccept); + break; + case 'm.key.verification.start': + _deviceId ??= payload['from_device']; + transactionId ??= eventId ?? payload['transaction_id']; + if (!(await verifyLastStep(['m.key.verification.request', null]))) { + return; // abort + } + if (!_validateStart(payload)) { + await cancel('m.unknown_method'); + return; + } + 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'); + 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(); + } + break; + case 'm.key.verification.done': + // do nothing + break; + case 'm.key.verification.cancel': + canceled = true; + canceledCode = payload['code']; + canceledReason = payload['reason']; + _setState(KeyVerificationState.error); + break; + default: + return; + } + lastStep = type; + } catch (err, stacktrace) { + print('[Key Verification] An error occured: ' + err.toString()); + print(stacktrace); + if (deviceId != null) { + await cancel('m.invalid_message'); + } + } + } + + /// called when the user accepts an incoming verification + Future acceptVerification() async { + if (!(await verifyLastStep(['m.key.verification.request', 'm.key.verification.start']))) { + return; + } + _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'], + }); + } else { + // we need to send an accept event + await _sendAccept(); + } + } + + /// called when the user rejects an incoming verification + Future rejectVerification() async { + if (!(await verifyLastStep(['m.key.verification.request', 'm.key.verification.start']))) { + return; + } + await cancel('m.user'); + } + + Future acceptSas() async { + await _sendMac(); + _setState(KeyVerificationState.waitingSas); + if (macPayload != null) { + await _processMac(); + } + } + + Future rejectSas() async { + await cancel('m.mismatched_sas'); + } + + List get sasNumbers { + return _bytesToInt(_makeSas(5), 13); + } + + List get sasEmojis { + final numbers = _bytesToInt(_makeSas(6), 6); + return numbers.map((n) => KeyVerificationEmoji(n)).toList().sublist(0, 7); + } + + Future _sendStart() async { + final payload = { + 'method': 'm.sas.v1', + '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); + // We just store the canonical json in here for later verification + startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload)); + await send('m.key.verification.start', payload); + } + + bool _validateStart(Map payload) { + if (payload['method'] != 'm.sas.v1') { + return false; + } + final possibleKeyAgreementProtocols = _intersect(KNOWN_KEY_AGREEMENT_PROTOCOLS, payload['key_agreement_protocols']); + if (possibleKeyAgreementProtocols.isEmpty) { + return false; + } + keyAgreementProtocol = possibleKeyAgreementProtocols.first; + final possibleHashes = _intersect(KNOWN_HASHES, payload['hashes']); + if (possibleHashes.isEmpty) { + return false; + } + hash = possibleHashes.first; + final possibleMessageAuthenticationCodes = _intersect(KNOWN_MESSAGE_AUTHENTIFICATION_CODES, payload['message_authentication_codes']); + if (possibleMessageAuthenticationCodes.isEmpty) { + return false; + } + messageAuthenticationCode = possibleMessageAuthenticationCodes.first; + final possibleAuthenticationTypes = _intersect(KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']); + if (possibleAuthenticationTypes.isEmpty) { + return false; + } + authenticationTypes = possibleAuthenticationTypes; + startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload)); + return true; + } + + Future _sendAccept() async { + sas = olm.SAS(); + commitment = _makeCommitment(sas.get_pubkey(), startCanonicalJson); + await send('m.key.verification.accept', { + 'method': 'm.sas.v1', + 'key_agreement_protocol': keyAgreementProtocol, + 'hash': hash, + 'message_authentication_code': messageAuthenticationCode, + 'short_authentication_string': authenticationTypes, + 'commitment': commitment, + }); + } + + bool _handleAccept(Map payload) { + if (!KNOWN_KEY_AGREEMENT_PROTOCOLS.contains(payload['key_agreement_protocol'])) { + return false; + } + keyAgreementProtocol = payload['key_agreement_protocol']; + if (!KNOWN_HASHES.contains(payload['hash'])) { + return false; + } + hash = payload['hash']; + if (!KNOWN_MESSAGE_AUTHENTIFICATION_CODES.contains(payload['message_authentication_code'])) { + return false; + } + messageAuthenticationCode = payload['message_authentication_code']; + final possibleAuthenticationTypes = _intersect(KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']); + if (possibleAuthenticationTypes.isEmpty) { + return false; + } + authenticationTypes = possibleAuthenticationTypes; + commitment = payload['commitment']; + sas = olm.SAS(); + return true; + } + + Future _sendKey() async { + await send('m.key.verification.key', { + 'key': sas.get_pubkey(), + }); + } + + void _handleKey(Map payload) { + theirPublicKey = payload['key']; + sas.set_their_key(payload['key']); + } + + bool _validateCommitment() { + final checkCommitment = _makeCommitment(theirPublicKey, startCanonicalJson); + return commitment == checkCommitment; + } + + 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; + } 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; + } else { + throw 'Unknown key agreement protocol'; + } + print('++++++++++++++++'); + print(keyAgreementProtocol); + print(sasInfo); + return sas.generate_bytes(sasInfo, bytes); + } + + Future _sendMac() async { + final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + + client.userID + client.deviceID + + userId + deviceId + + transactionId; + final mac = {}; + final keyList = []; + + // now add all the keys we want the other to verify + // for now it is just our device key, once we have cross-signing + // we would also add the cross signing key here + final deviceKeyId = 'ed25519:${client.deviceID}'; + mac[deviceKeyId] = _calculateMac(client.fingerprintKey, baseInfo + deviceKeyId); + keyList.add(deviceKeyId); + + keyList.sort(); + final keys = _calculateMac(keyList.join(','), baseInfo + 'KEY_IDS'); + await send('m.key.verification.mac', { + 'mac': mac, + 'keys': keys, + }); + } + + Future _processMac() async { + final payload = macPayload; + final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + + userId + deviceId + + client.userID + client.deviceID + + transactionId; + + final keyList = payload['mac'].keys.toList(); + keyList.sort(); + if (payload['keys'] != _calculateMac(keyList.join(','), baseInfo + 'KEY_IDS')) { + await cancel('m.key_mismatch'); + return; + } + + if (!client.userDeviceKeys.containsKey(userId)) { + await cancel('m.key_mismatch'); + return; + } + final mac = {}; + for (final entry in payload['mac'].entries) { + if (entry.value is String) { + mac[entry.key] = entry.value; + } + } + await _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); + } + } + + 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(); + final ret = olmutil.sha256(pubKey + canonicalJson); + olmutil.free(); + return ret; + } + 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); + } + } 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(); + } + } + + 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 = [ + { + 'emoji': '\u{1F436}', + 'name': 'Dog', + }, + { + 'emoji': '\u{1F431}', + 'name': 'Cat', + }, + { + 'emoji': '\u{1F981}', + 'name': 'Lion', + }, + { + 'emoji': '\u{1F40E}', + 'name': 'Horse', + }, + { + 'emoji': '\u{1F984}', + 'name': 'Unicorn', + }, + { + 'emoji': '\u{1F437}', + 'name': 'Pig', + }, + { + 'emoji': '\u{1F418}', + 'name': 'Elephant', + }, + { + 'emoji': '\u{1F430}', + 'name': 'Rabbit', + }, + { + 'emoji': '\u{1F43C}', + 'name': 'Panda', + }, + { + 'emoji': '\u{1F413}', + 'name': 'Rooster', + }, + { + 'emoji': '\u{1F427}', + 'name': 'Penguin', + }, + { + 'emoji': '\u{1F422}', + 'name': 'Turtle', + }, + { + 'emoji': '\u{1F41F}', + 'name': 'Fish', + }, + { + 'emoji': '\u{1F419}', + 'name': 'Octopus', + }, + { + 'emoji': '\u{1F98B}', + 'name': 'Butterfly', + }, + { + 'emoji': '\u{1F337}', + 'name': 'Flower', + }, + { + 'emoji': '\u{1F333}', + 'name': 'Tree', + }, + { + 'emoji': '\u{1F335}', + 'name': 'Cactus', + }, + { + 'emoji': '\u{1F344}', + 'name': 'Mushroom', + }, + { + 'emoji': '\u{1F30F}', + 'name': 'Globe', + }, + { + 'emoji': '\u{1F319}', + 'name': 'Moon', + }, + { + 'emoji': '\u{2601}\u{FE0F}', + 'name': 'Cloud', + }, + { + 'emoji': '\u{1F525}', + 'name': 'Fire', + }, + { + 'emoji': '\u{1F34C}', + 'name': 'Banana', + }, + { + 'emoji': '\u{1F34E}', + 'name': 'Apple', + }, + { + 'emoji': '\u{1F353}', + 'name': 'Strawberry', + }, + { + 'emoji': '\u{1F33D}', + 'name': 'Corn', + }, + { + 'emoji': '\u{1F355}', + 'name': 'Pizza', + }, + { + 'emoji': '\u{1F382}', + 'name': 'Cake', + }, + { + 'emoji': '\u{2764}\u{FE0F}', + 'name': 'Heart', + }, + { + 'emoji': '\u{1F600}', + 'name': 'Smiley', + }, + { + 'emoji': '\u{1F916}', + 'name': 'Robot', + }, + { + 'emoji': '\u{1F3A9}', + 'name': 'Hat', + }, + { + 'emoji': '\u{1F453}', + 'name': 'Glasses', + }, + { + 'emoji': '\u{1F527}', + 'name': 'Spanner', + }, + { + 'emoji': '\u{1F385}', + 'name': 'Santa', + }, + { + 'emoji': '\u{1F44D}', + 'name': 'Thumbs Up', + }, + { + 'emoji': '\u{2602}\u{FE0F}', + 'name': 'Umbrella', + }, + { + 'emoji': '\u{231B}', + 'name': 'Hourglass', + }, + { + 'emoji': '\u{23F0}', + 'name': 'Clock', + }, + { + 'emoji': '\u{1F381}', + 'name': 'Gift', + }, + { + 'emoji': '\u{1F4A1}', + 'name': 'Light Bulb', + }, + { + 'emoji': '\u{1F4D5}', + 'name': 'Book', + }, + { + 'emoji': '\u{270F}\u{FE0F}', + 'name': 'Pencil', + }, + { + 'emoji': '\u{1F4CE}', + 'name': 'Paperclip', + }, + { + 'emoji': '\u{2702}\u{FE0F}', + 'name': 'Scissors', + }, + { + 'emoji': '\u{1F512}', + 'name': 'Lock', + }, + { + 'emoji': '\u{1F511}', + 'name': 'Key', + }, + { + 'emoji': '\u{1F528}', + 'name': 'Hammer', + }, + { + 'emoji': '\u{260E}\u{FE0F}', + 'name': 'Telephone', + }, + { + 'emoji': '\u{1F3C1}', + 'name': 'Flag', + }, + { + 'emoji': '\u{1F682}', + 'name': 'Train', + }, + { + 'emoji': '\u{1F6B2}', + 'name': 'Bicycle', + }, + { + 'emoji': '\u{2708}\u{FE0F}', + 'name': 'Aeroplane', + }, + { + 'emoji': '\u{1F680}', + 'name': 'Rocket', + }, + { + 'emoji': '\u{1F3C6}', + 'name': 'Trophy', + }, + { + 'emoji': '\u{26BD}', + 'name': 'Ball', + }, + { + 'emoji': '\u{1F3B8}', + 'name': 'Guitar', + }, + { + 'emoji': '\u{1F3BA}', + 'name': 'Trumpet', + }, + { + 'emoji': '\u{1F514}', + 'name': 'Bell', + }, + { + 'emoji': '\u{2693}', + 'name': 'Anchor', + }, + { + 'emoji': '\u{1F3A7}', + 'name': 'Headphones', + }, + { + 'emoji': '\u{1F4C1}', + 'name': 'Folder', + }, + { + 'emoji': '\u{1F4CC}', + 'name': 'Pin', + }, +]; + +class KeyVerificationEmoji { + final int number; + KeyVerificationEmoji(this.number); + + String get emoji => _emojiMap[number]['emoji']; + String get name => _emojiMap[number]['name']; +} diff --git a/pubspec.lock b/pubspec.lock index d354e3e..271790e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -298,11 +298,9 @@ packages: matrix_file_e2ee: dependency: "direct main" description: - path: "." - ref: "1.x.y" - resolved-ref: "32edeff765369a7a77a0822f4b19302ca24a017b" - url: "https://gitlab.com/famedly/libraries/matrix_file_e2ee.git" - source: git + path: "/home/sorunome/repos/famedly/matrix_file_e2ee" + relative: false + source: path version: "1.0.3" meta: dependency: transitive @@ -364,8 +362,8 @@ packages: dependency: "direct main" description: path: "." - ref: "1.x.y" - resolved-ref: "79868b06b3ea156f90b73abafb3bbf3ac4114cc6" + ref: "2ef8828859e0c4f6064038e45d89be3e46f8013c" + resolved-ref: "2ef8828859e0c4f6064038e45d89be3e46f8013c" url: "https://gitlab.com/famedly/libraries/dart-olm.git" source: git version: "1.0.0" @@ -439,6 +437,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" + random_string: + dependency: "direct main" + description: + name: random_string + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" recase: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 576d016..342322e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,16 +15,18 @@ dependencies: markdown: ^2.1.3 html_unescape: ^1.0.1+3 moor: ^3.0.2 + random_string: ^2.0.1 olm: git: url: https://gitlab.com/famedly/libraries/dart-olm.git - ref: 1.x.y + ref: 2ef8828859e0c4f6064038e45d89be3e46f8013c matrix_file_e2ee: - git: - url: https://gitlab.com/famedly/libraries/matrix_file_e2ee.git - ref: 1.x.y + path: /home/sorunome/repos/famedly/matrix_file_e2ee +# git: +# url: https://gitlab.com/famedly/libraries/matrix_file_e2ee.git +# ref: 1.x.y dev_dependencies: test: ^1.0.0 From e87053b4f153b2e0ae46bffbe91e13fc772a1f5a Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sun, 17 May 2020 15:32:06 +0200 Subject: [PATCH 2/6] forgot to add 1000 to the numbers --- lib/src/utils/key_verification.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 593206d..4a150bd 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -245,7 +245,7 @@ class KeyVerification { } List get sasNumbers { - return _bytesToInt(_makeSas(5), 13); + return _bytesToInt(_makeSas(5), 13).map((n) => n + 1000).toList(); } List get sasEmojis { From 3b9be3546a87def69c10675f2d66e4697ab7dc1b Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sun, 17 May 2020 20:02:28 +0200 Subject: [PATCH 3/6] proper pubspec --- pubspec.lock | 14 ++++++++------ pubspec.yaml | 9 ++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 271790e..eadd60f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -298,9 +298,11 @@ packages: matrix_file_e2ee: dependency: "direct main" description: - path: "/home/sorunome/repos/famedly/matrix_file_e2ee" - relative: false - source: path + path: "." + ref: "1.x.y" + resolved-ref: "32edeff765369a7a77a0822f4b19302ca24a017b" + url: "https://gitlab.com/famedly/libraries/matrix_file_e2ee.git" + source: git version: "1.0.3" meta: dependency: transitive @@ -362,11 +364,11 @@ packages: dependency: "direct main" description: path: "." - ref: "2ef8828859e0c4f6064038e45d89be3e46f8013c" - resolved-ref: "2ef8828859e0c4f6064038e45d89be3e46f8013c" + ref: "1.x.y" + resolved-ref: "7b6a91343e2af47ce78a7ecf7532b94b563b2d04" url: "https://gitlab.com/famedly/libraries/dart-olm.git" source: git - version: "1.0.0" + version: "1.1.0" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 342322e..fa38a2d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,13 +20,12 @@ dependencies: olm: git: url: https://gitlab.com/famedly/libraries/dart-olm.git - ref: 2ef8828859e0c4f6064038e45d89be3e46f8013c + ref: 1.x.y matrix_file_e2ee: - path: /home/sorunome/repos/famedly/matrix_file_e2ee -# git: -# url: https://gitlab.com/famedly/libraries/matrix_file_e2ee.git -# ref: 1.x.y + git: + url: https://gitlab.com/famedly/libraries/matrix_file_e2ee.git + ref: 1.x.y dev_dependencies: test: ^1.0.0 From a4c693558dac65641587c674f0291446cd9d50f4 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 18 May 2020 11:44:23 +0200 Subject: [PATCH 4/6] 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 = [ From 50889f9f30d6d083c3c5cf8b669418d82343e5ff Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 18 May 2020 11:45:51 +0200 Subject: [PATCH 5/6] flutter analyze --- lib/src/utils/key_verification.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 3636351..89700df 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -381,8 +381,6 @@ abstract class _KeyVerificationMethod { client = request.client; } - static String type; - Future handlePayload(String type, Map payload); bool validateStart(Map payload) { return false; @@ -399,7 +397,6 @@ 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; From 2b8f4b0d19df91caf002052ce793bd1a28630cc3 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 18 May 2020 12:39:03 +0200 Subject: [PATCH 6/6] remove print statement for good --- lib/src/utils/key_verification.dart | 2 - pubspec.lock | 87 ++++++++++++++++------------- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/lib/src/utils/key_verification.dart b/lib/src/utils/key_verification.dart index 89700df..5735d50 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/src/utils/key_verification.dart @@ -595,8 +595,6 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { } else { throw 'Unknown key agreement protocol'; } - // this is needed, else things don't match up? WTF?! - print(sasInfo); return sas.generate_bytes(sasInfo, bytes); } diff --git a/pubspec.lock b/pubspec.lock index eadd60f..7eb42f4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -35,28 +35,28 @@ packages: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.5.2" + version: "1.6.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.4.1" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "2.0.0" build: dependency: transitive description: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "1.3.0" build_config: dependency: transitive description: @@ -77,28 +77,28 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.3.7" + version: "1.3.9" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.10.0" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "5.1.0" + version: "5.2.0" built_collection: dependency: transitive description: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "4.2.2" + version: "4.3.2" built_value: dependency: transitive description: @@ -119,7 +119,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.3" checked_yaml: dependency: transitive description: @@ -147,7 +147,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.11" + version: "1.14.12" convert: dependency: transitive description: @@ -168,14 +168,14 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.4" csslib: dependency: transitive description: name: csslib url: "https://pub.dartlang.org" source: hosted - version: "0.16.0" + version: "0.16.1" dart_style: dependency: transitive description: @@ -196,14 +196,14 @@ packages: name: fixnum url: "https://pub.dartlang.org" source: hosted - version: "0.10.9" + version: "0.10.11" glob: dependency: transitive description: name: glob url: "https://pub.dartlang.org" source: hosted - version: "1.1.7" + version: "1.2.0" graphs: dependency: transitive description: @@ -217,7 +217,7 @@ packages: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.14.0+2" + version: "0.14.0+3" html_unescape: dependency: "direct main" description: @@ -238,14 +238,14 @@ packages: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.3" + version: "3.1.4" image: dependency: "direct main" description: @@ -259,7 +259,7 @@ packages: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.3" + version: "0.3.4" js: dependency: transitive description: @@ -280,7 +280,7 @@ packages: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.3+2" + version: "0.11.4" markdown: dependency: "direct main" description: @@ -310,7 +310,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.7" + version: "1.1.8" mime: dependency: transitive description: @@ -353,6 +353,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + node_interop: + dependency: transitive + description: + name: node_interop + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + node_io: + dependency: transitive + description: + name: node_io + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" node_preamble: dependency: transitive description: @@ -365,10 +379,10 @@ packages: description: path: "." ref: "1.x.y" - resolved-ref: "7b6a91343e2af47ce78a7ecf7532b94b563b2d04" + resolved-ref: f66975bd1b5cb1865eba5efe6e3a392aa5e396a5 url: "https://gitlab.com/famedly/libraries/dart-olm.git" source: git - version: "1.1.0" + version: "1.1.1" package_config: dependency: transitive description: @@ -376,20 +390,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.9.3" - package_resolver: - dependency: transitive - description: - name: package_resolver - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.10" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.4" + version: "1.7.0" pedantic: dependency: "direct dev" description: @@ -424,7 +431,7 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.2" + version: "1.4.4" pubspec_parse: dependency: transitive description: @@ -438,7 +445,7 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.3" random_string: dependency: "direct main" description: @@ -466,7 +473,7 @@ packages: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.0" shelf_static: dependency: transitive description: @@ -501,14 +508,14 @@ packages: name: source_maps url: "https://pub.dartlang.org" source: hosted - version: "0.10.8" + version: "0.10.9" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.7.0" sqlparser: dependency: transitive description: @@ -606,28 +613,28 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "4.0.4" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+10" + version: "0.9.7+15" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.0.13" + version: "1.1.0" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol url: "https://pub.dartlang.org" source: hosted - version: "0.5.2" + version: "0.5.4" xml: dependency: transitive description: @@ -641,6 +648,6 @@ packages: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.1.16" + version: "2.2.1" sdks: dart: ">=2.7.0 <3.0.0"