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 6c2a03e..56c244b 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 ac10d49..d294d1a 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -738,8 +738,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; @@ -794,7 +794,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..5735d50 --- /dev/null +++ b/lib/src/utils/key_verification.dart @@ -0,0 +1,941 @@ +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 |<--------------------------------| + | | +*/ + +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; + final Room room; + final String userId; + void Function() onUpdate; + String get deviceId => _deviceId; + String _deviceId; + bool startedVerification = false; + _KeyVerificationMethod method; + List possibleMethods; + Map startPaylaod; + + DateTime lastActivity; + String lastStep; + + KeyVerificationState state = KeyVerificationState.waitingAccept; + bool canceled = false; + String canceledCode; + String canceledReason; + + KeyVerification({this.client, this.room, this.userId, String deviceId, this.onUpdate}) { + lastActivity = DateTime.now(); + _deviceId ??= deviceId; + } + + void dispose() { + print('[Key Verification] disposing object...'); + method?.dispose(); + } + + 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': VERIFICATION_METHODS, + '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 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; + } + // 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': + 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']; + transactionId ??= eventId ?? payload['transaction_id']; + if (!(await verifyLastStep(['m.key.verification.request', null]))) { + return; // abort + } + if (!VERIFICATION_METHODS.contains(payload['method'])) { + await cancel('m.unknown_method'); + return; + } + method = _makeVerificationMethod(payload['method'], this); + if (lastStep == null) { + if (!method.validateStart(payload)) { + await cancel('m.unknown_method'); + return; + } + startPaylaod = payload; + setState(KeyVerificationState.askAccept); + } else { + await method.handlePayload(type, payload); + } + 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: + await method.handlePayload(type, payload); + break; + } + 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': possibleMethods, + }); + } else { + // we need to send an accept event + await method.handlePayload('m.key.verification.start', startPaylaod); + } + } + + /// 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 { + 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; + } + + 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); + + 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(); + request.setState(KeyVerificationState.waitingSas); + if (macPayload != null) { + await _processMac(); + } + } + + Future rejectSas() async { + await request.cancel('m.mismatched_sas'); + } + + @override + Future sendStart() async { + final payload = { + '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, + }; + request.makePayload(payload); + // We just store the canonical json in here for later verification + startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload)); + await request.send('m.key.verification.start', payload); + } + + @override + bool validateStart(Map payload) { + if (payload['method'] != type) { + 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 request.send('m.key.verification.accept', { + 'method': type, + '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 request.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 = '${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 = request.userId + request.deviceId; + sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' + (request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + request.transactionId; + } else { + throw 'Unknown key agreement protocol'; + } + return sas.generate_bytes(sasInfo, bytes); + } + + Future _sendMac() async { + final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + + client.userID + client.deviceID + + request.userId + request.deviceId + + request.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 request.send('m.key.verification.mac', { + 'mac': mac, + 'keys': keys, + }); + } + + Future _processMac() async { + final payload = macPayload; + final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + + request.userId + request.deviceId + + client.userID + client.deviceID + + request.transactionId; + + final keyList = payload['mac'].keys.toList(); + keyList.sort(); + if (payload['keys'] != _calculateMac(keyList.join(','), baseInfo + 'KEY_IDS')) { + await request.cancel('m.key_mismatch'); + return; + } + + if (!client.userDeviceKeys.containsKey(request.userId)) { + await request.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 request.verifyKeys(mac, (String mac, DeviceKeys device) async { + return mac == _calculateMac(device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId); + }); + await request.send('m.key.verification.done', {}); + if (request.state != KeyVerificationState.error) { + request.setState(KeyVerificationState.done); + } + } + + 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'; + } + + String _calculateMac(String input, String info) { + if (messageAuthenticationCode == 'hkdf-hmac-sha256') { + return sas.calculate_mac(input, info); + } else { + throw 'Unknown message authentification code'; + } + } +} + +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..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: "79868b06b3ea156f90b73abafb3bbf3ac4114cc6" + resolved-ref: f66975bd1b5cb1865eba5efe6e3a392aa5e396a5 url: "https://gitlab.com/famedly/libraries/dart-olm.git" source: git - version: "1.0.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,14 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.3" + random_string: + dependency: "direct main" + description: + name: random_string + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" recase: dependency: transitive description: @@ -459,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: @@ -494,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: @@ -599,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: @@ -634,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" diff --git a/pubspec.yaml b/pubspec.yaml index 576d016..fa38a2d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: markdown: ^2.1.3 html_unescape: ^1.0.1+3 moor: ^3.0.2 + random_string: ^2.0.1 olm: git: