famedlysdk/lib/encryption/utils/key_verification.dart

1205 lines
34 KiB
Dart
Raw Permalink Normal View History

/*
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
2020-06-12 15:15:26 +00:00
import 'dart:async';
2020-05-17 13:25:42 +00:00
import 'dart:typed_data';
2020-05-17 13:25:42 +00:00
import 'package:canonical_json/canonical_json.dart';
import 'package:olm/olm.dart' as olm;
import 'package:pedantic/pedantic.dart';
import '../../famedlysdk.dart';
import '../../matrix_api.dart';
import '../../src/utils/logs.dart';
import '../encryption.dart';
2020-05-17 13:25:42 +00:00
/*
+-------------+ +-----------+
| 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 |<--------------------------------|
| |
*/
2020-05-22 10:12:18 +00:00
enum KeyVerificationState {
askAccept,
2020-05-25 13:30:53 +00:00
askSSSS,
2020-05-22 10:12:18 +00:00
waitingAccept,
askSas,
waitingSas,
done,
error
}
2020-05-17 13:25:42 +00:00
2020-05-30 11:55:09 +00:00
enum KeyVerificationMethod { emoji, numbers }
2020-05-18 09:44:23 +00:00
List<String> _intersect(List<String> a, List<dynamic> b) {
if (b == null || a == null) {
return [];
}
2020-05-18 09:44:23 +00:00
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;
}
2020-05-22 10:12:18 +00:00
_KeyVerificationMethod _makeVerificationMethod(
String type, KeyVerification request) {
if (type == 'm.sas.v1') {
2020-05-18 09:44:23 +00:00
return _KeyVerificationMethodSas(request: request);
}
throw 'Unkown method type';
}
2020-05-17 13:25:42 +00:00
class KeyVerification {
String transactionId;
final Encryption encryption;
Client get client => encryption.client;
2020-05-17 13:25:42 +00:00
final Room room;
final String userId;
void Function() onUpdate;
2020-05-22 10:12:18 +00:00
String get deviceId => _deviceId;
2020-05-17 13:25:42 +00:00
String _deviceId;
bool startedVerification = false;
2020-05-18 09:44:23 +00:00
_KeyVerificationMethod method;
List<String> possibleMethods;
Map<String, dynamic> startPaylaod;
2020-05-25 13:30:53 +00:00
String _nextAction;
2020-06-06 12:28:18 +00:00
List<SignableKey> _verifiedDevices;
2020-05-17 13:25:42 +00:00
DateTime lastActivity;
String lastStep;
KeyVerificationState state = KeyVerificationState.waitingAccept;
bool canceled = false;
String canceledCode;
String canceledReason;
2020-05-22 10:12:18 +00:00
KeyVerification(
{this.encryption,
this.room,
this.userId,
String deviceId,
this.onUpdate}) {
2020-05-17 13:25:42 +00:00
lastActivity = DateTime.now();
_deviceId ??= deviceId;
}
void dispose() {
2020-08-06 09:35:02 +00:00
Logs.info('[Key Verification] disposing object...');
2020-05-18 09:44:23 +00:00
method?.dispose();
2020-05-17 13:25:42 +00:00
}
static String getTransactionId(Map<String, dynamic> payload) {
2020-05-22 10:12:18 +00:00
return payload['transaction_id'] ??
(payload['m.relates_to'] is Map
? payload['m.relates_to']['event_id']
: null);
2020-05-17 13:25:42 +00:00
}
2020-05-30 11:55:09 +00:00
List<String> get knownVerificationMethods {
final methods = <String>[];
if (client.verificationMethods.contains(KeyVerificationMethod.numbers) ||
client.verificationMethods.contains(KeyVerificationMethod.emoji)) {
methods.add('m.sas.v1');
}
return methods;
}
2020-05-25 13:30:53 +00:00
Future<void> sendStart() async {
2020-05-17 13:25:42 +00:00
await send('m.key.verification.request', {
2020-05-30 11:55:09 +00:00
'methods': knownVerificationMethods,
if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch,
2020-05-17 13:25:42 +00:00
});
startedVerification = true;
2020-05-18 09:44:23 +00:00
setState(KeyVerificationState.waitingAccept);
2020-05-25 13:30:53 +00:00
lastActivity = DateTime.now();
}
Future<void> start() async {
if (room == null) {
2020-05-29 07:06:36 +00:00
transactionId = client.generateUniqueTransactionId();
}
2020-06-05 20:03:28 +00:00
if (encryption.crossSigning.enabled &&
!(await encryption.crossSigning.isCached()) &&
2020-05-27 08:33:42 +00:00
!client.isUnknownSession) {
2020-05-25 13:30:53 +00:00
setState(KeyVerificationState.askSSSS);
_nextAction = 'request';
} else {
await sendStart();
}
2020-05-17 13:25:42 +00:00
}
bool _handlePayloadLock = false;
2020-05-22 10:12:18 +00:00
Future<void> handlePayload(String type, Map<String, dynamic> payload,
[String eventId]) async {
while (_handlePayloadLock) {
await Future.delayed(Duration(milliseconds: 50));
}
_handlePayloadLock = true;
2020-08-06 09:35:02 +00:00
Logs.info(
'[Key Verification] Received type ${type}: ' + payload.toString());
2020-05-17 13:25:42 +00:00
try {
var thisLastStep = lastStep;
2020-05-17 13:25:42 +00:00
switch (type) {
case 'm.key.verification.request':
_deviceId ??= payload['from_device'];
transactionId ??= eventId ?? payload['transaction_id'];
// verify the timestamp
final now = DateTime.now();
2020-05-22 10:12:18 +00:00
final verifyTime =
DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) ||
now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
// if the request is more than 20min in the past we just silently fail it
// to not generate too many cancels
await cancel('m.timeout',
now.subtract(Duration(minutes: 20)).isAfter(verifyTime));
2020-05-17 13:25:42 +00:00
return;
}
2020-05-18 09:44:23 +00:00
// verify it has a method we can use
2020-05-22 10:12:18 +00:00
possibleMethods =
2020-05-30 11:55:09 +00:00
_intersect(knownVerificationMethods, payload['methods']);
2020-05-18 09:44:23 +00:00
if (possibleMethods.isEmpty) {
// reject it outright
await cancel('m.unknown_method');
return;
}
setState(KeyVerificationState.askAccept);
2020-05-17 13:25:42 +00:00
break;
case 'm.key.verification.ready':
_deviceId ??= payload['from_device'];
2020-05-22 10:12:18 +00:00
possibleMethods =
2020-05-30 11:55:09 +00:00
_intersect(knownVerificationMethods, payload['methods']);
2020-05-18 09:44:23 +00:00
if (possibleMethods.isEmpty) {
// reject it outright
await cancel('m.unknown_method');
return;
}
// as both parties can send a start, the last step being "ready" is race-condition prone
// as such, we better set it *before* we send our start
lastStep = type;
2020-05-18 09:44:23 +00:00
// TODO: Pick method?
method = _makeVerificationMethod(possibleMethods.first, this);
await method.sendStart();
setState(KeyVerificationState.waitingAccept);
2020-05-17 13:25:42 +00:00
break;
case 'm.key.verification.start':
_deviceId ??= payload['from_device'];
transactionId ??= eventId ?? payload['transaction_id'];
if (method != null) {
// the other side sent us a start, even though we already sent one
if (payload['method'] == method.type) {
// same method. Determine priority
final ourEntry = '${client.userID}|${client.deviceID}';
final entries = [ourEntry, '${userId}|${deviceId}'];
entries.sort();
if (entries.first == ourEntry) {
// our start won, nothing to do
return;
} else {
// the other start won, let's hand off
startedVerification = false; // it is now as if they started
thisLastStep = lastStep =
'm.key.verification.request'; // we fake the last step
method.dispose(); // in case anything got created already
}
} else {
// methods don't match up, let's cancel this
await cancel('m.unexpected_message');
return;
}
}
2020-05-17 13:25:42 +00:00
if (!(await verifyLastStep(['m.key.verification.request', null]))) {
return; // abort
}
2020-05-30 11:55:09 +00:00
if (!knownVerificationMethods.contains(payload['method'])) {
2020-05-17 13:25:42 +00:00
await cancel('m.unknown_method');
return;
}
2020-05-18 09:44:23 +00:00
method = _makeVerificationMethod(payload['method'], this);
2020-05-17 13:25:42 +00:00
if (lastStep == null) {
// validate the start time
if (room != null) {
// we just silently ignore in-room-verification starts
await cancel('m.unknown_method', true);
return;
}
// validate the specific payload
2020-05-18 09:44:23 +00:00
if (!method.validateStart(payload)) {
await cancel('m.unknown_method');
2020-05-17 13:25:42 +00:00
return;
}
2020-05-18 09:44:23 +00:00
startPaylaod = payload;
setState(KeyVerificationState.askAccept);
} else {
2020-08-06 09:35:02 +00:00
Logs.info('handling start in method.....');
2020-05-18 09:44:23 +00:00
await method.handlePayload(type, payload);
2020-05-17 13:25:42 +00:00
}
break;
case 'm.key.verification.done':
// do nothing
break;
case 'm.key.verification.cancel':
canceled = true;
canceledCode = payload['code'];
canceledReason = payload['reason'];
2020-05-18 09:44:23 +00:00
setState(KeyVerificationState.error);
2020-05-17 13:25:42 +00:00
break;
default:
if (method != null) {
await method.handlePayload(type, payload);
} else {
await cancel('m.invalid_message');
}
2020-05-18 09:44:23 +00:00
break;
2020-05-17 13:25:42 +00:00
}
if (lastStep == thisLastStep) {
lastStep = type;
}
2020-05-17 13:25:42 +00:00
} catch (err, stacktrace) {
2020-08-06 09:35:02 +00:00
Logs.error(
'[Key Verification] An error occured: ' + err.toString(), stacktrace);
await cancel('m.invalid_message');
} finally {
_handlePayloadLock = false;
2020-05-17 13:25:42 +00:00
}
}
void otherDeviceAccepted() {
canceled = true;
canceledCode = 'm.accepted';
canceledReason = 'm.accepted';
setState(KeyVerificationState.error);
}
2020-05-25 13:30:53 +00:00
Future<void> openSSSS(
2020-06-06 10:40:52 +00:00
{String passphrase, String recoveryKey, bool skip = false}) async {
2020-05-25 13:30:53 +00:00
final next = () {
if (_nextAction == 'request') {
sendStart();
} else if (_nextAction == 'done') {
if (_verifiedDevices != null) {
// and now let's sign them all in the background
2020-06-05 20:03:28 +00:00
encryption.crossSigning.sign(_verifiedDevices);
2020-05-25 13:30:53 +00:00
}
setState(KeyVerificationState.done);
}
};
if (skip) {
next();
return;
}
2020-06-05 20:03:28 +00:00
final handle = encryption.ssss.open('m.cross_signing.user_signing');
2020-06-06 10:40:52 +00:00
await handle.unlock(passphrase: passphrase, recoveryKey: recoveryKey);
2020-05-25 13:30:53 +00:00
await handle.maybeCacheAll();
next();
}
2020-05-17 13:25:42 +00:00
/// called when the user accepts an incoming verification
Future<void> acceptVerification() async {
2020-05-22 10:12:18 +00:00
if (!(await verifyLastStep(
['m.key.verification.request', 'm.key.verification.start']))) {
2020-05-17 13:25:42 +00:00
return;
}
2020-05-18 09:44:23 +00:00
setState(KeyVerificationState.waitingAccept);
2020-05-17 13:25:42 +00:00
if (lastStep == 'm.key.verification.request') {
// we need to send a ready event
await send('m.key.verification.ready', {
2020-05-18 09:44:23 +00:00
'methods': possibleMethods,
2020-05-17 13:25:42 +00:00
});
} else {
// we need to send an accept event
2020-05-18 09:44:23 +00:00
await method.handlePayload('m.key.verification.start', startPaylaod);
2020-05-17 13:25:42 +00:00
}
}
/// called when the user rejects an incoming verification
Future<void> rejectVerification() async {
2020-05-22 10:12:18 +00:00
if (!(await verifyLastStep(
['m.key.verification.request', 'm.key.verification.start']))) {
2020-05-17 13:25:42 +00:00
return;
}
await cancel('m.user');
}
Future<void> acceptSas() async {
2020-05-18 09:44:23 +00:00
if (method is _KeyVerificationMethodSas) {
await (method as _KeyVerificationMethodSas).acceptSas();
2020-05-17 13:25:42 +00:00
}
}
Future<void> rejectSas() async {
2020-05-18 09:44:23 +00:00
if (method is _KeyVerificationMethodSas) {
await (method as _KeyVerificationMethodSas).rejectSas();
}
2020-05-17 13:25:42 +00:00
}
List<int> get sasNumbers {
2020-05-18 09:44:23 +00:00
if (method is _KeyVerificationMethodSas) {
2020-05-22 10:12:18 +00:00
return _bytesToInt((method as _KeyVerificationMethodSas).makeSas(5), 13)
.map((n) => n + 1000)
.toList();
2020-05-18 09:44:23 +00:00
}
return [];
}
List<String> get sasTypes {
if (method is _KeyVerificationMethodSas) {
return (method as _KeyVerificationMethodSas).authenticationTypes;
}
return [];
2020-05-17 13:25:42 +00:00
}
List<KeyVerificationEmoji> get sasEmojis {
2020-05-18 09:44:23 +00:00
if (method is _KeyVerificationMethodSas) {
2020-05-22 10:12:18 +00:00
final numbers =
_bytesToInt((method as _KeyVerificationMethodSas).makeSas(6), 6);
2020-05-18 09:44:23 +00:00
return numbers.map((n) => KeyVerificationEmoji(n)).toList().sublist(0, 7);
}
return [];
}
2020-06-12 15:15:26 +00:00
Future<void> maybeRequestSSSSSecrets([int i = 0]) async {
final requestInterval = <int>[10, 60];
if ((!encryption.crossSigning.enabled ||
(encryption.crossSigning.enabled &&
(await encryption.crossSigning.isCached()))) &&
(!encryption.keyManager.enabled ||
(encryption.keyManager.enabled &&
(await encryption.keyManager.isCached())))) {
// no need to request cache, we already have it
return;
}
2020-06-12 15:40:08 +00:00
unawaited(encryption.ssss
.maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList()));
2020-06-12 15:32:35 +00:00
if (requestInterval.length <= i) {
2020-06-12 15:15:26 +00:00
return;
}
2020-06-12 15:40:08 +00:00
Timer(Duration(seconds: requestInterval[i]),
() => maybeRequestSSSSSecrets(i + 1));
2020-06-12 15:15:26 +00:00
}
2020-05-22 11:18:45 +00:00
Future<void> verifyKeys(Map<String, String> keys,
2020-06-06 12:28:18 +00:00
Future<bool> Function(String, SignableKey) verifier) async {
_verifiedDevices = <SignableKey>[];
2020-05-18 09:44:23 +00:00
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;
2020-05-25 13:30:53 +00:00
final key = client.userDeviceKeys[userId].getKey(verifyDeviceId);
if (key != null) {
if (!(await verifier(keyInfo, key))) {
await cancel('m.key_mismatch');
return;
}
2020-05-25 13:30:53 +00:00
_verifiedDevices.add(key);
2020-05-18 09:44:23 +00:00
}
}
// okay, we reached this far, so all the devices are verified!
2020-05-23 15:04:27 +00:00
var verifiedMasterKey = false;
final wasUnknownSession = client.isUnknownSession;
2020-05-25 13:30:53 +00:00
for (final key in _verifiedDevices) {
2020-05-27 08:33:42 +00:00
await key.setVerified(
true, false); // we don't want to sign the keys juuuust yet
2020-05-25 13:30:53 +00:00
if (key is CrossSigningKey && key.usage.contains('master')) {
verifiedMasterKey = true;
2020-05-23 15:04:27 +00:00
}
}
2020-05-25 13:30:53 +00:00
if (verifiedMasterKey && userId == client.userID) {
// it was our own master key, let's request the cross signing keys
// we do it in the background, thus no await needed here
2020-06-12 15:15:26 +00:00
unawaited(maybeRequestSSSSSecrets());
2020-05-25 13:30:53 +00:00
}
await send('m.key.verification.done', {});
var askingSSSS = false;
2020-06-05 20:03:28 +00:00
if (encryption.crossSigning.enabled &&
encryption.crossSigning.signable(_verifiedDevices)) {
2020-05-25 13:30:53 +00:00
// these keys can be signed! Let's do so
2020-06-05 20:03:28 +00:00
if (await encryption.crossSigning.isCached()) {
2020-05-25 13:30:53 +00:00
// and now let's sign them all in the background
2020-06-05 20:03:28 +00:00
unawaited(encryption.crossSigning.sign(_verifiedDevices));
} else if (!wasUnknownSession) {
2020-05-25 13:30:53 +00:00
askingSSSS = true;
}
2020-05-18 09:44:23 +00:00
}
2020-05-25 13:30:53 +00:00
if (askingSSSS) {
setState(KeyVerificationState.askSSSS);
_nextAction = 'done';
} else {
setState(KeyVerificationState.done);
}
2020-05-18 09:44:23 +00:00
}
Future<bool> verifyActivity() async {
2020-05-22 10:12:18 +00:00
if (lastActivity != null &&
lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
2020-05-18 09:44:23 +00:00
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', bool quiet = false]) async {
if (!quiet && (deviceId != null || room != null)) {
await send('m.key.verification.cancel', {
'reason': code,
'code': code,
});
}
2020-05-18 09:44:23 +00:00
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;
}
}
2020-05-17 13:25:42 +00:00
}
2020-05-18 09:44:23 +00:00
Future<void> send(String type, Map<String, dynamic> payload) async {
makePayload(payload);
2020-08-06 09:35:02 +00:00
Logs.info('[Key Verification] Sending type ${type}: ' + payload.toString());
2020-05-18 09:44:23 +00:00
if (room != null) {
2020-08-06 09:35:02 +00:00
Logs.info(
'[Key Verification] Sending to ${userId} in room ${room.id}...');
2020-05-18 09:44:23 +00:00
if (['m.key.verification.request'].contains(type)) {
payload['msgtype'] = type;
payload['to'] = userId;
2020-05-22 10:12:18 +00:00
payload['body'] =
'Attempting verification request. (${type}) Apparently your client doesn\'t support this';
2020-06-03 10:16:01 +00:00
type = EventTypes.Message;
2020-05-18 09:44:23 +00:00
}
final newTransactionId = await room.sendEvent(payload, type: type);
if (transactionId == null) {
transactionId = newTransactionId;
encryption.keyVerificationManager.addRequest(this);
2020-05-18 09:44:23 +00:00
}
} else {
2020-08-06 09:35:02 +00:00
Logs.info(
'[Key Verification] Sending to ${userId} device ${deviceId}...');
2020-08-11 16:11:51 +00:00
await client.sendToDeviceEncrypted(
2020-05-22 10:12:18 +00:00
[client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload);
2020-05-18 09:44:23 +00:00
}
}
void setState(KeyVerificationState newState) {
if (state != KeyVerificationState.error) {
state = newState;
}
if (onUpdate != null) {
onUpdate();
}
}
}
abstract class _KeyVerificationMethod {
KeyVerification request;
Encryption get encryption => request.encryption;
Client get client => request.client;
_KeyVerificationMethod({this.request});
2020-05-18 09:44:23 +00:00
Future<void> handlePayload(String type, Map<String, dynamic> payload);
bool validateStart(Map<String, dynamic> payload) {
return false;
}
2020-05-22 10:12:18 +00:00
String _type;
String get type => _type;
2020-05-18 09:44:23 +00:00
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'];
class _KeyVerificationMethodSas extends _KeyVerificationMethod {
2020-05-22 10:12:18 +00:00
_KeyVerificationMethodSas({KeyVerification request})
: super(request: request);
2020-05-18 09:44:23 +00:00
@override
2020-05-27 19:40:58 +00:00
final _type = 'm.sas.v1';
2020-05-18 09:44:23 +00:00
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();
}
2020-05-30 11:55:09 +00:00
List<String> get knownAuthentificationTypes {
final types = <String>[];
if (request.client.verificationMethods
.contains(KeyVerificationMethod.emoji)) {
types.add('emoji');
}
if (request.client.verificationMethods
.contains(KeyVerificationMethod.numbers)) {
types.add('decimal');
}
return types;
}
2020-05-18 09:44:23 +00:00
@override
Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
try {
switch (type) {
case 'm.key.verification.start':
2020-05-22 10:12:18 +00:00
if (!(await request.verifyLastStep(
['m.key.verification.request', 'm.key.verification.start']))) {
2020-05-18 09:44:23 +00:00
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':
2020-05-22 10:12:18 +00:00
if (!(await request.verifyLastStep(
['m.key.verification.accept', 'm.key.verification.start']))) {
2020-05-18 09:44:23 +00:00
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) {
2020-08-06 09:35:02 +00:00
Logs.error('[Key Verification SAS] An error occured: ' + err.toString(),
stacktrace);
2020-05-18 09:44:23 +00:00
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 {
2020-05-17 13:25:42 +00:00
final payload = <String, dynamic>{
2020-05-18 09:44:23 +00:00
'method': type,
2020-05-17 13:25:42 +00:00
'key_agreement_protocols': KNOWN_KEY_AGREEMENT_PROTOCOLS,
'hashes': KNOWN_HASHES,
'message_authentication_codes': KNOWN_MESSAGE_AUTHENTIFICATION_CODES,
2020-05-30 11:55:09 +00:00
'short_authentication_string': knownAuthentificationTypes,
2020-05-17 13:25:42 +00:00
};
2020-05-18 09:44:23 +00:00
request.makePayload(payload);
2020-05-17 13:25:42 +00:00
// We just store the canonical json in here for later verification
startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload));
2020-05-18 09:44:23 +00:00
await request.send('m.key.verification.start', payload);
2020-05-17 13:25:42 +00:00
}
2020-05-18 09:44:23 +00:00
@override
bool validateStart(Map<String, dynamic> payload) {
if (payload['method'] != type) {
2020-05-17 13:25:42 +00:00
return false;
}
2020-05-22 10:12:18 +00:00
final possibleKeyAgreementProtocols = _intersect(
KNOWN_KEY_AGREEMENT_PROTOCOLS, payload['key_agreement_protocols']);
2020-05-17 13:25:42 +00:00
if (possibleKeyAgreementProtocols.isEmpty) {
return false;
}
keyAgreementProtocol = possibleKeyAgreementProtocols.first;
final possibleHashes = _intersect(KNOWN_HASHES, payload['hashes']);
if (possibleHashes.isEmpty) {
return false;
}
hash = possibleHashes.first;
2020-05-22 10:12:18 +00:00
final possibleMessageAuthenticationCodes = _intersect(
KNOWN_MESSAGE_AUTHENTIFICATION_CODES,
payload['message_authentication_codes']);
2020-05-17 13:25:42 +00:00
if (possibleMessageAuthenticationCodes.isEmpty) {
return false;
}
messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
2020-05-22 10:12:18 +00:00
final possibleAuthenticationTypes = _intersect(
2020-05-30 11:55:09 +00:00
knownAuthentificationTypes, payload['short_authentication_string']);
2020-05-17 13:25:42 +00:00
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);
2020-05-18 09:44:23 +00:00
await request.send('m.key.verification.accept', {
'method': type,
2020-05-17 13:25:42 +00:00
'key_agreement_protocol': keyAgreementProtocol,
'hash': hash,
'message_authentication_code': messageAuthenticationCode,
'short_authentication_string': authenticationTypes,
'commitment': commitment,
});
}
bool _handleAccept(Map<String, dynamic> payload) {
2020-05-22 10:12:18 +00:00
if (!KNOWN_KEY_AGREEMENT_PROTOCOLS
.contains(payload['key_agreement_protocol'])) {
2020-05-17 13:25:42 +00:00
return false;
}
keyAgreementProtocol = payload['key_agreement_protocol'];
if (!KNOWN_HASHES.contains(payload['hash'])) {
return false;
}
hash = payload['hash'];
2020-05-22 10:12:18 +00:00
if (!KNOWN_MESSAGE_AUTHENTIFICATION_CODES
.contains(payload['message_authentication_code'])) {
2020-05-17 13:25:42 +00:00
return false;
}
messageAuthenticationCode = payload['message_authentication_code'];
2020-05-22 10:12:18 +00:00
final possibleAuthenticationTypes = _intersect(
2020-05-30 11:55:09 +00:00
knownAuthentificationTypes, payload['short_authentication_string']);
2020-05-17 13:25:42 +00:00
if (possibleAuthenticationTypes.isEmpty) {
return false;
}
authenticationTypes = possibleAuthenticationTypes;
commitment = payload['commitment'];
sas = olm.SAS();
return true;
}
Future<void> _sendKey() async {
2020-05-18 09:44:23 +00:00
await request.send('m.key.verification.key', {
2020-05-17 13:25:42 +00:00
'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;
}
2020-05-18 09:44:23 +00:00
Uint8List makeSas(int bytes) {
2020-05-17 13:25:42 +00:00
var sasInfo = '';
if (keyAgreementProtocol == 'curve25519-hkdf-sha256') {
2020-05-22 10:12:18 +00:00
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;
2020-05-17 13:25:42 +00:00
} else if (keyAgreementProtocol == 'curve25519') {
final ourInfo = client.userID + client.deviceID;
2020-05-18 09:44:23 +00:00
final theirInfo = request.userId + request.deviceId;
2020-05-22 10:12:18 +00:00
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' +
(request.startedVerification
? ourInfo + theirInfo
: theirInfo + ourInfo) +
request.transactionId;
2020-05-17 13:25:42 +00:00
} else {
throw 'Unknown key agreement protocol';
}
return sas.generate_bytes(sasInfo, bytes);
}
Future<void> _sendMac() async {
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
2020-05-22 10:12:18 +00:00
client.userID +
client.deviceID +
request.userId +
request.deviceId +
request.transactionId;
2020-05-17 13:25:42 +00:00
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}';
2020-05-22 10:12:18 +00:00
mac[deviceKeyId] =
_calculateMac(encryption.fingerprintKey, baseInfo + deviceKeyId);
2020-05-17 13:25:42 +00:00
keyList.add(deviceKeyId);
final masterKey = client.userDeviceKeys.containsKey(client.userID)
? client.userDeviceKeys[client.userID].masterKey
: null;
if (masterKey != null && masterKey.verified) {
// we have our own master key verified, let's send it!
final masterKeyId = 'ed25519:${masterKey.publicKey}';
mac[masterKeyId] =
_calculateMac(masterKey.publicKey, baseInfo + masterKeyId);
keyList.add(masterKeyId);
}
2020-05-17 13:25:42 +00:00
keyList.sort();
final keys = _calculateMac(keyList.join(','), baseInfo + 'KEY_IDS');
2020-05-18 09:44:23 +00:00
await request.send('m.key.verification.mac', {
2020-05-17 13:25:42 +00:00
'mac': mac,
'keys': keys,
});
}
Future<void> _processMac() async {
final payload = macPayload;
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
2020-05-22 10:12:18 +00:00
request.userId +
request.deviceId +
client.userID +
client.deviceID +
request.transactionId;
2020-05-17 13:25:42 +00:00
final keyList = payload['mac'].keys.toList();
keyList.sort();
2020-05-22 10:12:18 +00:00
if (payload['keys'] !=
_calculateMac(keyList.join(','), baseInfo + 'KEY_IDS')) {
2020-05-18 09:44:23 +00:00
await request.cancel('m.key_mismatch');
2020-05-17 13:25:42 +00:00
return;
}
2020-05-18 09:44:23 +00:00
if (!client.userDeviceKeys.containsKey(request.userId)) {
await request.cancel('m.key_mismatch');
2020-05-17 13:25:42 +00:00
return;
}
final mac = <String, String>{};
for (final entry in payload['mac'].entries) {
if (entry.value is String) {
mac[entry.key] = entry.value;
}
}
2020-06-06 12:28:18 +00:00
await request.verifyKeys(mac, (String mac, SignableKey key) async {
2020-05-25 13:30:53 +00:00
return mac ==
_calculateMac(key.ed25519Key, baseInfo + 'ed25519:' + key.identifier);
2020-05-17 13:25:42 +00:00
});
}
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';
}
2020-05-18 09:44:23 +00:00
String _calculateMac(String input, String info) {
if (messageAuthenticationCode == 'hkdf-hmac-sha256') {
return sas.calculate_mac(input, info);
2020-05-17 13:25:42 +00:00
} else {
2020-05-18 09:44:23 +00:00
throw 'Unknown message authentification code';
2020-05-17 13:25:42 +00:00
}
}
}
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'];
}