Merge branch 'soru/emoji-verification' into 'master'
implement SAS, which is needed for emoji verification See merge request famedly/famedlysdk!300
This commit is contained in:
commit
11a83725d1
|
@ -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/event_update.dart';
|
||||||
export 'package:famedlysdk/src/sync/user_update.dart';
|
export 'package:famedlysdk/src/sync/user_update.dart';
|
||||||
export 'package:famedlysdk/src/utils/device_keys_list.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_exception.dart';
|
||||||
export 'package:famedlysdk/src/utils/matrix_file.dart';
|
export 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||||
export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart';
|
export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart';
|
||||||
|
|
|
@ -56,6 +56,7 @@ import 'utils/profile.dart';
|
||||||
import 'database/database.dart' show Database;
|
import 'database/database.dart' show Database;
|
||||||
import 'utils/pusher.dart';
|
import 'utils/pusher.dart';
|
||||||
import 'utils/well_known_informations.dart';
|
import 'utils/well_known_informations.dart';
|
||||||
|
import 'utils/key_verification.dart';
|
||||||
|
|
||||||
typedef RoomSorter = int Function(Room a, Room b);
|
typedef RoomSorter = int Function(Room a, Room b);
|
||||||
|
|
||||||
|
@ -633,6 +634,11 @@ class Client {
|
||||||
final StreamController<RoomKeyRequest> onRoomKeyRequest =
|
final StreamController<RoomKeyRequest> onRoomKeyRequest =
|
||||||
StreamController.broadcast();
|
StreamController.broadcast();
|
||||||
|
|
||||||
|
/// Will be called when another device is requesting verification with this device.
|
||||||
|
final StreamController<KeyVerification> onKeyVerificationRequest = StreamController.broadcast();
|
||||||
|
|
||||||
|
final Map<String, KeyVerification> _keyVerificationRequests = {};
|
||||||
|
|
||||||
/// Matrix synchronisation is done with https long polling. This needs a
|
/// Matrix synchronisation is done with https long polling. This needs a
|
||||||
/// timeout which is usually 30 seconds.
|
/// timeout which is usually 30 seconds.
|
||||||
int syncTimeoutSec = 30;
|
int syncTimeoutSec = 30;
|
||||||
|
@ -968,6 +974,7 @@ class Client {
|
||||||
}
|
}
|
||||||
prevBatch = syncResp['next_batch'];
|
prevBatch = syncResp['next_batch'];
|
||||||
await _updateUserDeviceKeys();
|
await _updateUserDeviceKeys();
|
||||||
|
_cleanupKeyVerificationRequests();
|
||||||
if (hash == _syncRequest.hashCode) unawaited(_sync());
|
if (hash == _syncRequest.hashCode) unawaited(_sync());
|
||||||
} on MatrixException catch (exception) {
|
} on MatrixException catch (exception) {
|
||||||
onError.add(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<dynamic> events) {
|
void _handleToDeviceEvents(List<dynamic> events) {
|
||||||
for (var i = 0; i < events.length; i++) {
|
for (var i = 0; i < events.length; i++) {
|
||||||
var isValid = events[i] is Map &&
|
var isValid = events[i] is Map &&
|
||||||
|
@ -1078,10 +1107,38 @@ class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_updateRoomsByToDeviceEvent(toDeviceEvent);
|
_updateRoomsByToDeviceEvent(toDeviceEvent);
|
||||||
|
if (toDeviceEvent.type.startsWith('m.key.verification.')) {
|
||||||
|
_handleToDeviceKeyVerificationRequest(toDeviceEvent);
|
||||||
|
}
|
||||||
onToDeviceEvent.add(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<String, dynamic> rooms, Membership membership,
|
void _handleRooms(Map<String, dynamic> rooms, Membership membership,
|
||||||
List<Future<dynamic> Function()> dbActions) {
|
List<Future<dynamic> Function()> dbActions) {
|
||||||
rooms.forEach((String id, dynamic room) {
|
rooms.forEach((String id, dynamic room) {
|
||||||
|
|
|
@ -738,8 +738,8 @@ class Room {
|
||||||
/// Sends an event to this room with this json as a content. Returns the
|
/// Sends an event to this room with this json as a content. Returns the
|
||||||
/// event ID generated from the server.
|
/// event ID generated from the server.
|
||||||
Future<String> sendEvent(Map<String, dynamic> content,
|
Future<String> sendEvent(Map<String, dynamic> content,
|
||||||
{String txid, Event inReplyTo}) async {
|
{String type, String txid, Event inReplyTo}) async {
|
||||||
final type = 'm.room.message';
|
type = type ?? 'm.room.message';
|
||||||
final sendType =
|
final sendType =
|
||||||
(encrypted && client.encryptionEnabled) ? 'm.room.encrypted' : type;
|
(encrypted && client.encryptionEnabled) ? 'm.room.encrypted' : type;
|
||||||
|
|
||||||
|
@ -794,7 +794,7 @@ class Room {
|
||||||
type: HTTPType.PUT,
|
type: HTTPType.PUT,
|
||||||
action: '/client/r0/rooms/${id}/send/$sendType/$messageID',
|
action: '/client/r0/rooms/${id}/send/$sendType/$messageID',
|
||||||
data: encrypted && client.encryptionEnabled
|
data: encrypted && client.encryptionEnabled
|
||||||
? await encryptGroupMessagePayload(content)
|
? await encryptGroupMessagePayload(content, type: type)
|
||||||
: content);
|
: content);
|
||||||
final String res = response['event_id'];
|
final String res = response['event_id'];
|
||||||
eventUpdate.content['status'] = 1;
|
eventUpdate.content['status'] = 1;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
||||||
import '../client.dart';
|
import '../client.dart';
|
||||||
import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey;
|
import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey;
|
||||||
import '../event.dart';
|
import '../event.dart';
|
||||||
|
import 'key_verification.dart';
|
||||||
|
|
||||||
class DeviceKeysList {
|
class DeviceKeysList {
|
||||||
String userId;
|
String userId;
|
||||||
|
@ -137,4 +138,11 @@ class DeviceKeys {
|
||||||
data['blocked'] = blocked;
|
data['blocked'] = blocked;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KeyVerification startVerification(Client client) {
|
||||||
|
final request = KeyVerification(client: client, userId: userId, deviceId: deviceId);
|
||||||
|
request.start();
|
||||||
|
client.addKeyVerificationRequest(request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
941
lib/src/utils/key_verification.dart
Normal file
941
lib/src/utils/key_verification.dart
Normal file
|
@ -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<String> _intersect(List<String> a, List<dynamic> b) {
|
||||||
|
final res = <String>[];
|
||||||
|
for (final v in a) {
|
||||||
|
if (b.contains(v)) {
|
||||||
|
res.add(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> _bytesToInt(Uint8List bytes, int totalBits) {
|
||||||
|
final ret = <int>[];
|
||||||
|
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<String> possibleMethods;
|
||||||
|
Map<String, dynamic> 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<String, dynamic> payload) {
|
||||||
|
return payload['transaction_id'] ?? (
|
||||||
|
payload['m.relates_to'] is Map ? payload['m.relates_to']['event_id'] : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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<void> handlePayload(String type, Map<String, dynamic> 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<void> 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<void> rejectVerification() async {
|
||||||
|
if (!(await verifyLastStep(['m.key.verification.request', 'm.key.verification.start']))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await cancel('m.user');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> acceptSas() async {
|
||||||
|
if (method is _KeyVerificationMethodSas) {
|
||||||
|
await (method as _KeyVerificationMethodSas).acceptSas();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> rejectSas() async {
|
||||||
|
if (method is _KeyVerificationMethodSas) {
|
||||||
|
await (method as _KeyVerificationMethodSas).rejectSas();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> get sasNumbers {
|
||||||
|
if (method is _KeyVerificationMethodSas) {
|
||||||
|
return _bytesToInt((method as _KeyVerificationMethodSas).makeSas(5), 13).map((n) => n + 1000).toList();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> get sasTypes {
|
||||||
|
if (method is _KeyVerificationMethodSas) {
|
||||||
|
return (method as _KeyVerificationMethodSas).authenticationTypes;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<KeyVerificationEmoji> 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<void> verifyKeys(Map<String, String> keys, Future<bool> Function(String, DeviceKeys) verifier) async {
|
||||||
|
final verifiedDevices = <String>[];
|
||||||
|
|
||||||
|
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<bool> 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<bool> verifyLastStep(List<String> checkLastStep) async {
|
||||||
|
if (!(await verifyActivity())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (checkLastStep.contains(lastStep)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await cancel('m.unexpected_message');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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<String, dynamic> 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<void> send(String type, Map<String, dynamic> 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<void> handlePayload(String type, Map<String, dynamic> payload);
|
||||||
|
bool validateStart(Map<String, dynamic> payload) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Future<void> 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<String> authenticationTypes;
|
||||||
|
String startCanonicalJson;
|
||||||
|
String commitment;
|
||||||
|
String theirPublicKey;
|
||||||
|
Map<String, dynamic> macPayload;
|
||||||
|
olm.SAS sas;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
sas?.free();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> handlePayload(String type, Map<String, dynamic> 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<void> acceptSas() async {
|
||||||
|
await _sendMac();
|
||||||
|
request.setState(KeyVerificationState.waitingSas);
|
||||||
|
if (macPayload != null) {
|
||||||
|
await _processMac();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> rejectSas() async {
|
||||||
|
await request.cancel('m.mismatched_sas');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> sendStart() async {
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
'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<String, dynamic> 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<void> _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<String, dynamic> 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<void> _sendKey() async {
|
||||||
|
await request.send('m.key.verification.key', {
|
||||||
|
'key': sas.get_pubkey(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleKey(Map<String, dynamic> 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<void> _sendMac() async {
|
||||||
|
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
|
||||||
|
client.userID + client.deviceID +
|
||||||
|
request.userId + request.deviceId +
|
||||||
|
request.transactionId;
|
||||||
|
final mac = <String, String>{};
|
||||||
|
final keyList = <String>[];
|
||||||
|
|
||||||
|
// 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<void> _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 = <String, String>{};
|
||||||
|
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'];
|
||||||
|
}
|
94
pubspec.lock
94
pubspec.lock
|
@ -35,28 +35,28 @@ packages:
|
||||||
name: args
|
name: args
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.2"
|
version: "1.6.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.4.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: boolean_selector
|
name: boolean_selector
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "2.0.0"
|
||||||
build:
|
build:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build
|
name: build
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.3.0"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -77,28 +77,28 @@ packages:
|
||||||
name: build_resolvers
|
name: build_resolvers
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.7"
|
version: "1.3.9"
|
||||||
build_runner:
|
build_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.10.0"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_runner_core
|
name: build_runner_core
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
version: "5.2.0"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: built_collection
|
name: built_collection
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.2"
|
version: "4.3.2"
|
||||||
built_value:
|
built_value:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -119,7 +119,7 @@ packages:
|
||||||
name: charcode
|
name: charcode
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.3"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -147,7 +147,7 @@ packages:
|
||||||
name: collection
|
name: collection
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.14.11"
|
version: "1.14.12"
|
||||||
convert:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -168,14 +168,14 @@ packages:
|
||||||
name: crypto
|
name: crypto
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.4"
|
||||||
csslib:
|
csslib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: csslib
|
name: csslib
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.16.0"
|
version: "0.16.1"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -196,14 +196,14 @@ packages:
|
||||||
name: fixnum
|
name: fixnum
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.10.9"
|
version: "0.10.11"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: glob
|
name: glob
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.7"
|
version: "1.2.0"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -217,7 +217,7 @@ packages:
|
||||||
name: html
|
name: html
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.14.0+2"
|
version: "0.14.0+3"
|
||||||
html_unescape:
|
html_unescape:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -238,14 +238,14 @@ packages:
|
||||||
name: http_multi_server
|
name: http_multi_server
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.2.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.4"
|
||||||
image:
|
image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -259,7 +259,7 @@ packages:
|
||||||
name: io
|
name: io
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.3"
|
version: "0.3.4"
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -280,7 +280,7 @@ packages:
|
||||||
name: logging
|
name: logging
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.3+2"
|
version: "0.11.4"
|
||||||
markdown:
|
markdown:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -310,7 +310,7 @@ packages:
|
||||||
name: meta
|
name: meta
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.7"
|
version: "1.1.8"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -353,6 +353,20 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
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:
|
node_preamble:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -365,10 +379,10 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "1.x.y"
|
ref: "1.x.y"
|
||||||
resolved-ref: "79868b06b3ea156f90b73abafb3bbf3ac4114cc6"
|
resolved-ref: f66975bd1b5cb1865eba5efe6e3a392aa5e396a5
|
||||||
url: "https://gitlab.com/famedly/libraries/dart-olm.git"
|
url: "https://gitlab.com/famedly/libraries/dart-olm.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.0"
|
version: "1.1.1"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -376,20 +390,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.3"
|
version: "1.9.3"
|
||||||
package_resolver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: package_resolver
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.10"
|
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.4"
|
version: "1.7.0"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
@ -424,7 +431,7 @@ packages:
|
||||||
name: pub_semver
|
name: pub_semver
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.2"
|
version: "1.4.4"
|
||||||
pubspec_parse:
|
pubspec_parse:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -438,7 +445,14 @@ packages:
|
||||||
name: quiver
|
name: quiver
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
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:
|
recase:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -459,7 +473,7 @@ packages:
|
||||||
name: shelf_packages_handler
|
name: shelf_packages_handler
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "2.0.0"
|
||||||
shelf_static:
|
shelf_static:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -494,14 +508,14 @@ packages:
|
||||||
name: source_maps
|
name: source_maps
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.10.8"
|
version: "0.10.9"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.5"
|
version: "1.7.0"
|
||||||
sqlparser:
|
sqlparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -599,28 +613,28 @@ packages:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "4.0.4"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: watcher
|
name: watcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.7+10"
|
version: "0.9.7+15"
|
||||||
web_socket_channel:
|
web_socket_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.13"
|
version: "1.1.0"
|
||||||
webkit_inspection_protocol:
|
webkit_inspection_protocol:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webkit_inspection_protocol
|
name: webkit_inspection_protocol
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.2"
|
version: "0.5.4"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -634,6 +648,6 @@ packages:
|
||||||
name: yaml
|
name: yaml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.16"
|
version: "2.2.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.7.0 <3.0.0"
|
dart: ">=2.7.0 <3.0.0"
|
||||||
|
|
|
@ -15,6 +15,7 @@ dependencies:
|
||||||
markdown: ^2.1.3
|
markdown: ^2.1.3
|
||||||
html_unescape: ^1.0.1+3
|
html_unescape: ^1.0.1+3
|
||||||
moor: ^3.0.2
|
moor: ^3.0.2
|
||||||
|
random_string: ^2.0.1
|
||||||
|
|
||||||
olm:
|
olm:
|
||||||
git:
|
git:
|
||||||
|
|
Loading…
Reference in a new issue