Line data Source code
1 : /*
2 : * Famedly Matrix SDK
3 : * Copyright (C) 2020, 2021 Famedly GmbH
4 : *
5 : * This program is free software: you can redistribute it and/or modify
6 : * it under the terms of the GNU Affero General Public License as
7 : * published by the Free Software Foundation, either version 3 of the
8 : * License, or (at your option) any later version.
9 : *
10 : * This program is distributed in the hope that it will be useful,
11 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : * GNU Affero General Public License for more details.
14 : *
15 : * You should have received a copy of the GNU Affero General Public License
16 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 : */
18 :
19 : import 'dart:async';
20 : import 'dart:convert';
21 : import 'dart:typed_data';
22 :
23 : import 'package:canonical_json/canonical_json.dart';
24 : import 'package:typed_data/typed_data.dart';
25 : import 'package:vodozemac/vodozemac.dart' as vod;
26 :
27 : import 'package:matrix/encryption/encryption.dart';
28 : import 'package:matrix/encryption/utils/base64_unpadded.dart';
29 : import 'package:matrix/matrix.dart';
30 : import 'package:matrix/src/utils/crypto/crypto.dart' as uc;
31 :
32 : /*
33 : +-------------+ +-----------+
34 : | AliceDevice | | BobDevice |
35 : +-------------+ +-----------+
36 : | |
37 : | (m.key.verification.request) |
38 : |-------------------------------->| (ASK FOR VERIFICATION REQUEST)
39 : | |
40 : | (m.key.verification.ready) |
41 : |<--------------------------------|
42 : | |
43 : | (m.key.verification.start) | we will probably not send this
44 : |<--------------------------------| for simplicities sake
45 : | |
46 : | m.key.verification.start |
47 : |-------------------------------->| (ASK FOR VERIFICATION REQUEST)
48 : | |
49 : | m.key.verification.accept |
50 : |<--------------------------------|
51 : | |
52 : | m.key.verification.key |
53 : |-------------------------------->|
54 : | |
55 : | m.key.verification.key |
56 : |<--------------------------------|
57 : | |
58 : | COMPARE EMOJI / NUMBERS |
59 : | |
60 : | m.key.verification.mac |
61 : |-------------------------------->| success
62 : | |
63 : | m.key.verification.mac |
64 : success |<--------------------------------|
65 : | |
66 : */
67 :
68 : /// QR key verification
69 : /// You create possible methods from `client.verificationMethods` on device A
70 : /// and send a request using `request.start()` which calls `sendRequest()` your client
71 : /// now is in `waitingAccept` state, where ideally your client would now show some
72 : /// waiting indicator.
73 : ///
74 : /// On device B you now get a `m.key.verification.request`, you check the
75 : /// `methods` from the request payload and see if anything is possible.
76 : /// If not you cancel the request. (should this be cancelled? couldn't another device handle this?)
77 : /// you the set the state to `askAccept`.
78 : ///
79 : /// Your client would now show a button to accept/decline the request.
80 : /// The user can then use `acceptVerification()`to accept the verification which
81 : /// then sends a `m.key.verification.ready`. This also calls `generateQrCode()`
82 : /// in it which populates the `request.qrData` depending on the qr mode.
83 : /// B now sets the state `askChoice`
84 : ///
85 : /// On device A you now get the ready event, which setups the `possibleMethods`
86 : /// and `qrData` on A's side. Similarly A now sets their state to `askChoice`
87 : ///
88 : /// At his point both sides are on the `askChoice` state.
89 : ///
90 : /// BACKWARDS COMPATIBILITY HACK:
91 : /// To work well with sdks prior to QR verification (0.20.5 and older), start will
92 : /// be sent with ready itself if only sas is supported. This avoids weird glare
93 : /// issues faced with start from both sides if clients are not on the same sdk
94 : /// version (0.20.5 vs next)
95 : /// https://matrix.to/#/!KBwfdofYJUmnsVoqwn:famedly.de/$wlHXlLQJdfrqKAF5KkuQrXydwOhY_uyqfH4ReasZqnA?via=neko.dev&via=famedly.de&via=lihotzki.de
96 :
97 : /// Here your clients would ideally show a list of the `possibleMethods` and the
98 : /// user can choose one. For QR specifically, you can show the QR code on the
99 : /// device which supports showing the qr code and the device which supports
100 : /// scanning can scan this code.
101 : ///
102 : /// Assuming device A scans device B's code, device A would now send a `m.key.verification.start`,
103 : /// you do this using the `continueVerificatio()` method. You can pass
104 : /// `m.reciprocate.v1` or `m.sas.v1` here, and also attach the qrData here.
105 : /// This then calls `verifyQrData()` internally, which sets the `randomSharedSecretForQRCode`
106 : /// to the one from the QR code. Device A is now set to `showQRSuccess` state and shows
107 : /// a green sheild. (Maybe add a text below saying tell device B you scanned the
108 : /// code successfully.)
109 : ///
110 : /// (some keys magic happens here, check `verifyQrData()`, `verifyKeysQR()` to know more)
111 : ///
112 : /// On device B you get the `m.key.verification.start` event. The secret sent in
113 : /// the start request is then verified, device B is then set to the `confirmQRScan`
114 : /// state. Your device should show a dialog to confirm from B that A's device shows
115 : /// the green shield (is in the done state). Once B confirms this physically, you
116 : /// call the `acceptQRScanConfirmation()` function, which then does some keys
117 : /// magic and sets B's state to `done`.
118 : ///
119 : /// A gets the `m.key.verification.done` messsage and sends a done back, both
120 : /// users can now dismiss the verification dialog safely.
121 :
122 : enum KeyVerificationState {
123 : askChoice,
124 : askAccept,
125 : askSSSS,
126 : waitingAccept,
127 : askSas,
128 : showQRSuccess, // scanner after QR scan was successfull
129 : confirmQRScan, // shower after getting start
130 : waitingSas,
131 : done,
132 : error
133 : }
134 :
135 : enum KeyVerificationMethod { emoji, numbers, qrShow, qrScan, reciprocate }
136 :
137 2 : bool isQrSupported(List knownVerificationMethods, List possibleMethods) {
138 2 : return knownVerificationMethods.contains(EventTypes.QRShow) &&
139 2 : possibleMethods.contains(EventTypes.QRScan) ||
140 2 : knownVerificationMethods.contains(EventTypes.QRScan) &&
141 2 : possibleMethods.contains(EventTypes.QRShow);
142 : }
143 :
144 1 : List<String> _intersect(List<String>? a, List<dynamic>? b) =>
145 3 : (b == null || a == null) ? [] : a.where(b.contains).toList();
146 :
147 2 : List<String> _calculatePossibleMethods(
148 : List<String> knownMethods,
149 : List<dynamic> payloadMethods,
150 : ) {
151 2 : final output = <String>[];
152 2 : final copyKnownMethods = List<String>.from(knownMethods);
153 2 : final copyPayloadMethods = List.from(payloadMethods);
154 :
155 : copyKnownMethods
156 6 : .removeWhere((element) => !copyPayloadMethods.contains(element));
157 :
158 : // remove qr modes for now, check if they are possible and add later
159 6 : copyKnownMethods.removeWhere((element) => element.startsWith('m.qr_code'));
160 2 : output.addAll(copyKnownMethods);
161 :
162 2 : if (isQrSupported(knownMethods, payloadMethods)) {
163 : // scan/show combo found, add whichever is known to us to our possible methods.
164 2 : if (payloadMethods.contains(EventTypes.QRScan) &&
165 2 : knownMethods.contains(EventTypes.QRShow)) {
166 2 : output.add(EventTypes.QRShow);
167 : }
168 2 : if (payloadMethods.contains(EventTypes.QRShow) &&
169 2 : knownMethods.contains(EventTypes.QRScan)) {
170 2 : output.add(EventTypes.QRScan);
171 : }
172 : } else {
173 2 : output.remove(EventTypes.Reciprocate);
174 : }
175 :
176 : return output;
177 : }
178 :
179 1 : List<int> _bytesToInt(Uint8List bytes, int totalBits) {
180 1 : final ret = <int>[];
181 : var current = 0;
182 : var numBits = 0;
183 2 : for (final byte in bytes) {
184 2 : for (final bit in [7, 6, 5, 4, 3, 2, 1, 0]) {
185 1 : numBits++;
186 5 : current |= ((byte >> bit) & 1) << (totalBits - numBits);
187 1 : if (numBits >= totalBits) {
188 1 : ret.add(current);
189 : current = 0;
190 : numBits = 0;
191 : }
192 : }
193 : }
194 : return ret;
195 : }
196 :
197 2 : _KeyVerificationMethod _makeVerificationMethod(
198 : String type,
199 : KeyVerification request,
200 : ) {
201 2 : if (type == EventTypes.Sas) {
202 2 : return _KeyVerificationMethodSas(request: request);
203 : }
204 2 : if (type == EventTypes.Reciprocate) {
205 2 : return _KeyVerificationMethodQRReciprocate(request: request);
206 : }
207 0 : throw Exception('Unkown method type');
208 : }
209 :
210 : class KeyVerification {
211 : String? transactionId;
212 : final Encryption encryption;
213 9 : Client get client => encryption.client;
214 : final Room? room;
215 : final String userId;
216 : void Function()? onUpdate;
217 6 : String? get deviceId => _deviceId;
218 : String? _deviceId;
219 : bool startedVerification = false;
220 : _KeyVerificationMethod? _method;
221 :
222 : List<String> possibleMethods = [];
223 : List<String> oppositePossibleMethods = [];
224 :
225 : Map<String, dynamic>? startPayload;
226 : String? _nextAction;
227 : List<SignableKey> _verifiedDevices = [];
228 :
229 : DateTime lastActivity;
230 : String? lastStep;
231 :
232 : KeyVerificationState state = KeyVerificationState.waitingAccept;
233 : bool canceled = false;
234 : String? canceledCode;
235 : String? canceledReason;
236 2 : bool get isDone =>
237 2 : canceled ||
238 8 : {KeyVerificationState.error, KeyVerificationState.done}.contains(state);
239 :
240 : // qr stuff
241 : QRCode? qrCode;
242 : String? randomSharedSecretForQRCode;
243 : SignableKey? keyToVerify;
244 3 : KeyVerification({
245 : required this.encryption,
246 : this.room,
247 : required this.userId,
248 : String? deviceId,
249 : this.onUpdate,
250 : }) : _deviceId = deviceId,
251 3 : lastActivity = DateTime.now();
252 :
253 3 : void dispose() {
254 6 : Logs().i('[Key Verification] disposing object...');
255 3 : randomSharedSecretForQRCode = null;
256 5 : _method?.dispose();
257 : }
258 :
259 2 : static String? getTransactionId(Map<String, dynamic> payload) {
260 2 : return payload['transaction_id'] ??
261 2 : (payload['m.relates_to'] is Map
262 2 : ? payload['m.relates_to']['event_id']
263 : : null);
264 : }
265 :
266 3 : List<String> get knownVerificationMethods {
267 : final methods = <String>{};
268 9 : if (client.verificationMethods.contains(KeyVerificationMethod.numbers) ||
269 3 : client.verificationMethods.contains(KeyVerificationMethod.emoji)) {
270 2 : methods.add(EventTypes.Sas);
271 : }
272 :
273 : /// `qrCanWork` - qr cannot work if we are verifying another master key but our own is unverified
274 12 : final qrCanWork = (userId == client.userID) ||
275 14 : ((client.userDeviceKeys[client.userID]?.masterKey?.verified ?? false));
276 :
277 9 : if (client.verificationMethods.contains(KeyVerificationMethod.qrShow) &&
278 : qrCanWork) {
279 2 : methods.add(EventTypes.QRShow);
280 2 : methods.add(EventTypes.Reciprocate);
281 : }
282 9 : if (client.verificationMethods.contains(KeyVerificationMethod.qrScan) &&
283 : qrCanWork) {
284 2 : methods.add(EventTypes.QRScan);
285 2 : methods.add(EventTypes.Reciprocate);
286 : }
287 :
288 3 : return methods.toList();
289 : }
290 :
291 : /// Once you get a ready event, i.e both sides are in a `askChoice` state,
292 : /// send either `m.reciprocate.v1` or `m.sas.v1` here. If you continue with
293 : /// qr, send the qrData you just scanned
294 2 : Future<void> continueVerification(
295 : String type, {
296 : Uint8List? qrDataRawBytes,
297 : }) async {
298 : bool qrChecksOut = false;
299 4 : if (possibleMethods.contains(type)) {
300 : if (qrDataRawBytes != null) {
301 2 : qrChecksOut = await verifyQrData(qrDataRawBytes);
302 : // after this scanners state is done
303 : }
304 2 : if (type != EventTypes.Reciprocate || qrChecksOut) {
305 4 : final method = _method = _makeVerificationMethod(type, this);
306 2 : await method.sendStart();
307 2 : if (type == EventTypes.Sas) {
308 0 : setState(KeyVerificationState.waitingAccept);
309 : }
310 0 : } else if (type == EventTypes.Reciprocate && !qrChecksOut) {
311 0 : Logs().e('[KeyVerification] qr did not check out');
312 0 : await cancel('m.invalid_key');
313 : }
314 : } else {
315 4 : Logs().e(
316 : '[KeyVerification] tried to continue verification with a unknown method',
317 : );
318 2 : await cancel('m.unknown_method');
319 : }
320 : }
321 :
322 3 : Future<void> sendRequest() async {
323 3 : await send(
324 : EventTypes.KeyVerificationRequest,
325 3 : {
326 6 : 'methods': knownVerificationMethods,
327 9 : if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch,
328 : },
329 : );
330 3 : startedVerification = true;
331 3 : setState(KeyVerificationState.waitingAccept);
332 6 : lastActivity = DateTime.now();
333 : }
334 :
335 3 : Future<void> start() async {
336 3 : if (room == null) {
337 6 : transactionId = client.generateUniqueTransactionId();
338 : }
339 9 : if (encryption.crossSigning.enabled &&
340 9 : !(await encryption.crossSigning.isCached()) &&
341 4 : !client.isUnknownSession) {
342 2 : setState(KeyVerificationState.askSSSS);
343 2 : _nextAction = 'request';
344 : } else {
345 3 : await sendRequest();
346 : }
347 : }
348 :
349 : bool _handlePayloadLock = false;
350 :
351 2 : QRMode getOurQRMode() {
352 : QRMode mode = QRMode.verifyOtherUser;
353 8 : if (client.userID == userId) {
354 2 : if (client.encryption != null &&
355 3 : client.encryption!.enabled &&
356 7 : (client.userDeviceKeys[client.userID]?.masterKey?.directVerified ??
357 : false)) {
358 : mode = QRMode.verifySelfTrusted;
359 : } else {
360 : mode = QRMode.verifySelfUntrusted;
361 : }
362 : }
363 : return mode;
364 : }
365 :
366 2 : Future<void> handlePayload(
367 : String type,
368 : Map<String, dynamic> payload, [
369 : String? eventId,
370 : ]) async {
371 2 : if (isDone) {
372 : return; // no need to do anything with already canceled requests
373 : }
374 2 : while (_handlePayloadLock) {
375 0 : await Future.delayed(Duration(milliseconds: 50));
376 : }
377 2 : _handlePayloadLock = true;
378 6 : Logs().i('[Key Verification] Received type $type: $payload');
379 : try {
380 2 : var thisLastStep = lastStep;
381 : switch (type) {
382 2 : case EventTypes.KeyVerificationRequest:
383 4 : _deviceId ??= payload['from_device'];
384 3 : transactionId ??= eventId ?? payload['transaction_id'];
385 : // verify the timestamp
386 2 : final now = DateTime.now();
387 : final verifyTime =
388 4 : DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
389 6 : if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) ||
390 6 : now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
391 : // if the request is more than 20min in the past we just silently fail it
392 : // to not generate too many cancels
393 0 : await cancel(
394 : 'm.timeout',
395 0 : now.subtract(Duration(minutes: 20)).isAfter(verifyTime),
396 : );
397 : return;
398 : }
399 :
400 : // ensure we have the other sides keys
401 14 : if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
402 0 : await client.updateUserDeviceKeys(additionalUsers: {userId});
403 0 : if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
404 0 : await cancel('im.fluffychat.unknown_device');
405 : return;
406 : }
407 : }
408 :
409 6 : oppositePossibleMethods = List<String>.from(payload['methods']);
410 : // verify it has a method we can use
411 4 : possibleMethods = _calculatePossibleMethods(
412 2 : knownVerificationMethods,
413 2 : payload['methods'],
414 : );
415 4 : if (possibleMethods.isEmpty) {
416 : // reject it outright
417 0 : await cancel('m.unknown_method');
418 : return;
419 : }
420 :
421 2 : setState(KeyVerificationState.askAccept);
422 : break;
423 2 : case EventTypes.KeyVerificationReady:
424 4 : if (deviceId == '*') {
425 2 : _deviceId = payload['from_device']; // gotta set the real device id
426 1 : transactionId ??= eventId ?? payload['transaction_id'];
427 : // and broadcast the cancel to the other devices
428 1 : final devices = List<DeviceKeys>.from(
429 6 : client.userDeviceKeys[userId]?.deviceKeys.values ??
430 0 : Iterable.empty(),
431 : );
432 1 : devices.removeWhere(
433 6 : (d) => {deviceId, client.deviceID}.contains(d.deviceId),
434 : );
435 1 : final cancelPayload = <String, dynamic>{
436 : 'reason': 'Another device accepted the request',
437 : 'code': 'm.accepted',
438 : };
439 1 : makePayload(cancelPayload);
440 2 : await client.sendToDeviceEncrypted(
441 : devices,
442 : EventTypes.KeyVerificationCancel,
443 : cancelPayload,
444 : );
445 : }
446 3 : _deviceId ??= payload['from_device'];
447 :
448 : // ensure we have the other sides keys
449 14 : if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
450 0 : await client.updateUserDeviceKeys(additionalUsers: {userId});
451 0 : if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
452 0 : await cancel('im.fluffychat.unknown_device');
453 : return;
454 : }
455 : }
456 :
457 6 : oppositePossibleMethods = List<String>.from(payload['methods']);
458 4 : possibleMethods = _calculatePossibleMethods(
459 2 : knownVerificationMethods,
460 2 : payload['methods'],
461 : );
462 4 : if (possibleMethods.isEmpty) {
463 : // reject it outright
464 0 : await cancel('m.unknown_method');
465 : return;
466 : }
467 : // as both parties can send a start, the last step being "ready" is race-condition prone
468 : // as such, we better set it *before* we send our start
469 2 : lastStep = type;
470 :
471 : // setup QRData from outgoing request (incoming ready)
472 4 : qrCode = await generateQrCode();
473 :
474 : // play nice with sdks < 0.20.5
475 : // https://matrix.to/#/!KBwfdofYJUmnsVoqwn:famedly.de/$wlHXlLQJdfrqKAF5KkuQrXydwOhY_uyqfH4ReasZqnA?via=neko.dev&via=famedly.de&via=lihotzki.de
476 6 : if (!isQrSupported(knownVerificationMethods, payload['methods'])) {
477 4 : if (knownVerificationMethods.contains(EventTypes.Sas)) {
478 2 : final method = _method =
479 6 : _makeVerificationMethod(possibleMethods.first, this);
480 2 : await method.sendStart();
481 2 : setState(KeyVerificationState.waitingAccept);
482 : }
483 : } else {
484 : // allow user to choose
485 2 : setState(KeyVerificationState.askChoice);
486 : }
487 :
488 : break;
489 2 : case EventTypes.KeyVerificationStart:
490 2 : _deviceId ??= payload['from_device'];
491 2 : transactionId ??= eventId ?? payload['transaction_id'];
492 2 : if (_method != null) {
493 : // the other side sent us a start, even though we already sent one
494 0 : if (payload['method'] == _method!.type) {
495 : // same method. Determine priority
496 0 : final ourEntry = '${client.userID}|${client.deviceID}';
497 0 : final entries = [ourEntry, '$userId|$deviceId'];
498 0 : entries.sort();
499 0 : if (entries.first == ourEntry) {
500 : // our start won, nothing to do
501 : return;
502 : } else {
503 : // the other start won, let's hand off
504 0 : startedVerification = false; // it is now as if they started
505 0 : thisLastStep = lastStep =
506 : EventTypes.KeyVerificationRequest; // we fake the last step
507 0 : _method!.dispose(); // in case anything got created already
508 : }
509 : } else {
510 : // methods don't match up, let's cancel this
511 0 : await cancel('m.unexpected_message');
512 : return;
513 : }
514 : }
515 4 : if (!(await verifyLastStep([
516 : EventTypes.KeyVerificationRequest,
517 : EventTypes.KeyVerificationReady,
518 : ]))) {
519 : return; // abort
520 : }
521 6 : if (!knownVerificationMethods.contains(payload['method'])) {
522 0 : await cancel('m.unknown_method');
523 : return;
524 : }
525 :
526 4 : if (lastStep == EventTypes.KeyVerificationRequest) {
527 6 : if (!possibleMethods.contains(payload['method'])) {
528 1 : await cancel('m.unknown_method');
529 : return;
530 : }
531 : }
532 :
533 : // ensure we have the other sides keys
534 14 : if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
535 0 : await client.updateUserDeviceKeys(additionalUsers: {userId});
536 0 : if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
537 0 : await cancel('im.fluffychat.unknown_device');
538 : return;
539 : }
540 : }
541 :
542 6 : _method = _makeVerificationMethod(payload['method'], this);
543 2 : if (lastStep == null) {
544 : // validate the start time
545 0 : if (room != null) {
546 : // we just silently ignore in-room-verification starts
547 0 : await cancel('m.unknown_method', true);
548 : return;
549 : }
550 : // validate the specific payload
551 0 : if (!_method!.validateStart(payload)) {
552 0 : await cancel('m.unknown_method');
553 : return;
554 : }
555 0 : startPayload = payload;
556 0 : setState(KeyVerificationState.askAccept);
557 : } else {
558 4 : Logs().i('handling start in method.....');
559 4 : await _method!.handlePayload(type, payload);
560 : }
561 : break;
562 2 : case EventTypes.KeyVerificationDone:
563 4 : if (state == KeyVerificationState.showQRSuccess) {
564 4 : await send(EventTypes.KeyVerificationDone, {});
565 2 : setState(KeyVerificationState.done);
566 : }
567 : break;
568 1 : case EventTypes.KeyVerificationCancel:
569 1 : canceled = true;
570 2 : canceledCode = payload['code'];
571 2 : canceledReason = payload['reason'];
572 1 : setState(KeyVerificationState.error);
573 : break;
574 : default:
575 1 : final method = _method;
576 : if (method != null) {
577 1 : await method.handlePayload(type, payload);
578 : } else {
579 0 : await cancel('m.invalid_message');
580 : }
581 : break;
582 : }
583 4 : if (lastStep == thisLastStep) {
584 2 : lastStep = type;
585 : }
586 : } catch (err, stacktrace) {
587 0 : Logs().e('[Key Verification] An error occured', err, stacktrace);
588 0 : await cancel('m.invalid_message');
589 : } finally {
590 2 : _handlePayloadLock = false;
591 : }
592 : }
593 :
594 1 : void otherDeviceAccepted() {
595 1 : canceled = true;
596 1 : canceledCode = 'm.accepted';
597 1 : canceledReason = 'm.accepted';
598 1 : setState(KeyVerificationState.error);
599 : }
600 :
601 2 : Future<void> openSSSS({
602 : String? passphrase,
603 : String? recoveryKey,
604 : String? keyOrPassphrase,
605 : bool skip = false,
606 : }) async {
607 2 : Future<void> next() async {
608 4 : if (_nextAction == 'request') {
609 2 : await sendRequest();
610 4 : } else if (_nextAction == 'done') {
611 : // and now let's sign them all in the background
612 10 : unawaited(encryption.crossSigning.sign(_verifiedDevices));
613 2 : setState(KeyVerificationState.done);
614 0 : } else if (_nextAction == 'showQRSuccess') {
615 0 : setState(KeyVerificationState.showQRSuccess);
616 : }
617 : }
618 :
619 : if (skip) {
620 0 : await next();
621 : return;
622 : }
623 6 : final handle = encryption.ssss.open(EventTypes.CrossSigningUserSigning);
624 2 : await handle.unlock(
625 : passphrase: passphrase,
626 : recoveryKey: recoveryKey,
627 : keyOrPassphrase: keyOrPassphrase,
628 : );
629 2 : await handle.maybeCacheAll();
630 2 : await next();
631 : }
632 :
633 : /// called when the user accepts an incoming verification
634 2 : Future<void> acceptVerification() async {
635 4 : if (!(await verifyLastStep([
636 : EventTypes.KeyVerificationRequest,
637 : EventTypes.KeyVerificationStart,
638 : ]))) {
639 : return;
640 : }
641 2 : setState(KeyVerificationState.waitingAccept);
642 4 : if (lastStep == EventTypes.KeyVerificationRequest) {
643 : final copyKnownVerificationMethods =
644 4 : List<String>.from(knownVerificationMethods);
645 : // qr code only works when atleast one side has verified master key
646 8 : if (userId == client.userID) {
647 8 : if (!(client.userDeviceKeys[client.userID]?.deviceKeys[deviceId]
648 1 : ?.hasValidSignatureChain(verifiedByTheirMasterKey: true) ??
649 : false) &&
650 7 : !(client.userDeviceKeys[client.userID]?.masterKey?.verified ??
651 : false)) {
652 : copyKnownVerificationMethods
653 3 : .removeWhere((element) => element.startsWith('m.qr_code'));
654 1 : copyKnownVerificationMethods.remove(EventTypes.Reciprocate);
655 :
656 : // we are removing stuff only using the old possibleMethods should be ok here.
657 2 : final copyPossibleMethods = List<String>.from(possibleMethods);
658 2 : possibleMethods = _calculatePossibleMethods(
659 : copyKnownVerificationMethods,
660 : copyPossibleMethods,
661 : );
662 : }
663 : }
664 : // we need to send a ready event
665 4 : await send(EventTypes.KeyVerificationReady, {
666 : 'methods': copyKnownVerificationMethods,
667 : });
668 : // setup QRData from incoming request (outgoing ready)
669 4 : qrCode = await generateQrCode();
670 2 : setState(KeyVerificationState.askChoice);
671 : } else {
672 : // we need to send an accept event
673 0 : await _method!
674 0 : .handlePayload(EventTypes.KeyVerificationStart, startPayload!);
675 : }
676 : }
677 :
678 : /// called when the user rejects an incoming verification
679 1 : Future<void> rejectVerification() async {
680 1 : if (isDone) {
681 : return;
682 : }
683 2 : if (!(await verifyLastStep([
684 : EventTypes.KeyVerificationRequest,
685 : EventTypes.KeyVerificationStart,
686 : ]))) {
687 : return;
688 : }
689 1 : await cancel('m.user');
690 : }
691 :
692 : /// call this to confirm that your other device has shown a shield and is in
693 : /// `done` state.
694 2 : Future<void> acceptQRScanConfirmation() async {
695 4 : if (_method is _KeyVerificationMethodQRReciprocate &&
696 4 : state == KeyVerificationState.confirmQRScan) {
697 2 : await (_method as _KeyVerificationMethodQRReciprocate)
698 2 : .acceptQRScanConfirmation();
699 : }
700 : }
701 :
702 1 : Future<void> acceptSas() async {
703 2 : if (_method is _KeyVerificationMethodSas) {
704 2 : await (_method as _KeyVerificationMethodSas).acceptSas();
705 : }
706 : }
707 :
708 1 : Future<void> rejectSas() async {
709 2 : if (_method is _KeyVerificationMethodSas) {
710 2 : await (_method as _KeyVerificationMethodSas).rejectSas();
711 : }
712 : }
713 :
714 1 : List<int> get sasNumbers {
715 2 : if (_method is _KeyVerificationMethodSas) {
716 3 : return _bytesToInt((_method as _KeyVerificationMethodSas).makeSas(5), 13)
717 3 : .map((n) => n + 1000)
718 1 : .toList();
719 : }
720 0 : return [];
721 : }
722 :
723 1 : List<String> get sasTypes {
724 2 : if (_method is _KeyVerificationMethodSas) {
725 2 : return (_method as _KeyVerificationMethodSas).authenticationTypes ?? [];
726 : }
727 0 : return [];
728 : }
729 :
730 1 : List<KeyVerificationEmoji> get sasEmojis {
731 2 : if (_method is _KeyVerificationMethodSas) {
732 : final numbers =
733 3 : _bytesToInt((_method as _KeyVerificationMethodSas).makeSas(6), 6);
734 5 : return numbers.map((n) => KeyVerificationEmoji(n)).toList().sublist(0, 7);
735 : }
736 0 : return [];
737 : }
738 :
739 1 : Future<void> maybeRequestSSSSSecrets([int i = 0]) async {
740 1 : final requestInterval = <int>[10, 60];
741 3 : if ((!encryption.crossSigning.enabled ||
742 3 : (encryption.crossSigning.enabled &&
743 3 : (await encryption.crossSigning.isCached()))) &&
744 0 : (!encryption.keyManager.enabled ||
745 0 : (encryption.keyManager.enabled &&
746 0 : (await encryption.keyManager.isCached())))) {
747 : // no need to request cache, we already have it
748 : return;
749 : }
750 1 : unawaited(
751 2 : encryption.ssss
752 4 : .maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList()),
753 : );
754 2 : if (requestInterval.length <= i) {
755 : return;
756 : }
757 1 : Timer(
758 2 : Duration(seconds: requestInterval[i]),
759 0 : () => maybeRequestSSSSSecrets(i + 1),
760 : );
761 : }
762 :
763 1 : Future<void> verifyKeysSAS(
764 : Map<String, String> keys,
765 : Future<bool> Function(String, SignableKey) verifier,
766 : ) async {
767 2 : _verifiedDevices = <SignableKey>[];
768 :
769 4 : final userDeviceKey = client.userDeviceKeys[userId];
770 : if (userDeviceKey == null) {
771 0 : await cancel('m.key_mismatch');
772 : return;
773 : }
774 2 : for (final entry in keys.entries) {
775 1 : final keyId = entry.key;
776 2 : final verifyDeviceId = keyId.substring('ed25519:'.length);
777 1 : final keyInfo = entry.value;
778 1 : final key = userDeviceKey.getKey(verifyDeviceId);
779 : if (key != null) {
780 1 : if (!(await verifier(keyInfo, key))) {
781 0 : await cancel('m.key_mismatch');
782 : return;
783 : }
784 2 : _verifiedDevices.add(key);
785 : }
786 : }
787 : // okay, we reached this far, so all the devices are verified!
788 : var verifiedMasterKey = false;
789 2 : final wasUnknownSession = client.isUnknownSession;
790 2 : for (final key in _verifiedDevices) {
791 1 : await key.setVerified(
792 : true,
793 : false,
794 : ); // we don't want to sign the keys juuuust yet
795 3 : if (key is CrossSigningKey && key.usage.contains('master')) {
796 : verifiedMasterKey = true;
797 : }
798 : }
799 4 : if (verifiedMasterKey && userId == client.userID) {
800 : // it was our own master key, let's request the cross signing keys
801 : // we do it in the background, thus no await needed here
802 : // ignore: unawaited_futures
803 0 : maybeRequestSSSSSecrets();
804 : }
805 2 : await send(EventTypes.KeyVerificationDone, {});
806 :
807 : var askingSSSS = false;
808 3 : if (encryption.crossSigning.enabled &&
809 4 : encryption.crossSigning.signable(_verifiedDevices)) {
810 : // these keys can be signed! Let's do so
811 3 : if (await encryption.crossSigning.isCached()) {
812 : // we want to make sure the verification state is correct for the other party after this event is handled.
813 : // Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
814 0 : await encryption.crossSigning.sign(_verifiedDevices);
815 : } else if (!wasUnknownSession) {
816 : askingSSSS = true;
817 : }
818 : }
819 : if (askingSSSS) {
820 1 : setState(KeyVerificationState.askSSSS);
821 1 : _nextAction = 'done';
822 : } else {
823 1 : setState(KeyVerificationState.done);
824 : }
825 : }
826 :
827 : /// shower is true only for reciprocated verifications (shower side)
828 2 : Future<void> verifyKeysQR(SignableKey key, {bool shower = true}) async {
829 : var verifiedMasterKey = false;
830 4 : final wasUnknownSession = client.isUnknownSession;
831 :
832 2 : key.setDirectVerified(true);
833 6 : if (key is CrossSigningKey && key.usage.contains('master')) {
834 : verifiedMasterKey = true;
835 : }
836 :
837 8 : if (verifiedMasterKey && userId == client.userID) {
838 : // it was our own master key, let's request the cross signing keys
839 : // we do it in the background, thus no await needed here
840 : // ignore: unawaited_futures
841 1 : maybeRequestSSSSSecrets();
842 : }
843 : if (shower) {
844 4 : await send(EventTypes.KeyVerificationDone, {});
845 : }
846 4 : final keyList = List<SignableKey>.from([key]);
847 : var askingSSSS = false;
848 6 : if (encryption.crossSigning.enabled &&
849 6 : encryption.crossSigning.signable(keyList)) {
850 : // these keys can be signed! Let's do so
851 6 : if (await encryption.crossSigning.isCached()) {
852 : // we want to make sure the verification state is correct for the other party after this event is handled.
853 : // Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
854 6 : await encryption.crossSigning.sign(keyList);
855 : } else if (!wasUnknownSession) {
856 : askingSSSS = true;
857 : }
858 : }
859 : if (askingSSSS) {
860 : // no need to worry about shower/scanner here because if scanner was
861 : // verified, ssss is already
862 1 : setState(KeyVerificationState.askSSSS);
863 : if (shower) {
864 1 : _nextAction = 'done';
865 : } else {
866 0 : _nextAction = 'showQRSuccess';
867 : }
868 : } else {
869 : if (shower) {
870 2 : setState(KeyVerificationState.done);
871 : } else {
872 2 : setState(KeyVerificationState.showQRSuccess);
873 : }
874 : }
875 : }
876 :
877 2 : Future<bool> verifyActivity() async {
878 10 : if (lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
879 4 : lastActivity = DateTime.now();
880 : return true;
881 : }
882 0 : await cancel('m.timeout');
883 : return false;
884 : }
885 :
886 2 : Future<bool> verifyLastStep(List<String?> checkLastStep) async {
887 2 : if (!(await verifyActivity())) {
888 : return false;
889 : }
890 4 : if (checkLastStep.contains(lastStep)) {
891 : return true;
892 : }
893 0 : Logs().e(
894 0 : '[KeyVerificaton] lastStep mismatch cancelling, expected from ${checkLastStep.toString()} was ${lastStep.toString()}',
895 : );
896 0 : await cancel('m.unexpected_message');
897 : return false;
898 : }
899 :
900 2 : Future<void> cancel([String code = 'm.unknown', bool quiet = false]) async {
901 3 : if (!quiet && (deviceId != null || room != null)) {
902 4 : await send(EventTypes.KeyVerificationCancel, {
903 : 'reason': code,
904 : 'code': code,
905 : });
906 : }
907 2 : canceled = true;
908 2 : canceledCode = code;
909 2 : setState(KeyVerificationState.error);
910 : }
911 :
912 3 : void makePayload(Map<String, dynamic> payload) {
913 9 : payload['from_device'] = client.deviceID;
914 3 : if (transactionId != null) {
915 3 : if (room != null) {
916 2 : payload['m.relates_to'] = {
917 : 'rel_type': 'm.reference',
918 1 : 'event_id': transactionId,
919 : };
920 : } else {
921 4 : payload['transaction_id'] = transactionId;
922 : }
923 : }
924 : }
925 :
926 3 : Future<void> send(
927 : String type,
928 : Map<String, dynamic> payload,
929 : ) async {
930 3 : makePayload(payload);
931 9 : Logs().i('[Key Verification] Sending type $type: $payload');
932 3 : if (room != null) {
933 12 : Logs().i('[Key Verification] Sending to $userId in room ${room!.id}...');
934 4 : if ({EventTypes.KeyVerificationRequest}.contains(type)) {
935 2 : payload['msgtype'] = type;
936 4 : payload['to'] = userId;
937 2 : payload['body'] =
938 2 : 'Attempting verification request. ($type) Apparently your client doesn\'t support this';
939 : type = EventTypes.Message;
940 : }
941 4 : final newTransactionId = await room!.sendEvent(payload, type: type);
942 2 : if (transactionId == null) {
943 2 : transactionId = newTransactionId;
944 6 : encryption.keyVerificationManager.addRequest(this);
945 : }
946 : } else {
947 10 : Logs().i('[Key Verification] Sending to $userId device $deviceId...');
948 4 : if (deviceId == '*') {
949 : if ({
950 1 : EventTypes.KeyVerificationRequest,
951 1 : EventTypes.KeyVerificationCancel,
952 1 : }.contains(type)) {
953 : final deviceKeys =
954 7 : client.userDeviceKeys[userId]?.deviceKeys.values.where(
955 2 : (deviceKey) => deviceKey.hasValidSignatureChain(
956 : verifiedByTheirMasterKey: true,
957 : ),
958 : );
959 :
960 : if (deviceKeys != null) {
961 2 : await client.sendToDeviceEncrypted(
962 1 : deviceKeys.toList(),
963 : type,
964 : payload,
965 : );
966 : }
967 : } else {
968 0 : Logs().e(
969 0 : '[Key Verification] Tried to broadcast and un-broadcastable type: $type',
970 : );
971 : }
972 : } else {
973 14 : if (client.userDeviceKeys[userId]?.deviceKeys[deviceId] != null) {
974 4 : await client.sendToDeviceEncrypted(
975 16 : [client.userDeviceKeys[userId]!.deviceKeys[deviceId]!],
976 : type,
977 : payload,
978 : );
979 : } else {
980 0 : Logs().e('[Key Verification] Unknown device');
981 : }
982 : }
983 : }
984 : }
985 :
986 3 : void setState(KeyVerificationState newState) {
987 6 : if (state != KeyVerificationState.error) {
988 3 : state = newState;
989 : }
990 :
991 3 : onUpdate?.call();
992 : }
993 :
994 : static const String prefix = 'MATRIX';
995 : static const int version = 0x02;
996 :
997 2 : Future<bool> verifyQrData(Uint8List qrDataRawBytes) async {
998 : final data = qrDataRawBytes;
999 : // hardcoded stuff + 2 keys + secret
1000 18 : if (data.length < 10 + 32 + 32 + 8 + utf8.encode(transactionId!).length) {
1001 : return false;
1002 : }
1003 4 : if (data[6] != version) return false;
1004 : final remoteQrMode =
1005 10 : QRMode.values.singleWhere((mode) => mode.code == data[7]);
1006 6 : if (ascii.decode(data.sublist(0, 6)) != prefix) return false;
1007 4 : if (data[6] != version) return false;
1008 8 : final tmpBuf = Uint8List.fromList([data[8], data[9]]);
1009 6 : final encodedTxnLen = ByteData.view(tmpBuf.buffer).getUint16(0);
1010 10 : if (utf8.decode(data.sublist(10, 10 + encodedTxnLen)) != transactionId) {
1011 : return false;
1012 : }
1013 4 : final keys = client.userDeviceKeys;
1014 :
1015 6 : final ownKeys = keys[client.userID];
1016 4 : final otherUserKeys = keys[userId];
1017 2 : final ownMasterKey = ownKeys?.getCrossSigningKey('master');
1018 6 : final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
1019 4 : final ownOtherDeviceKey = ownKeys?.getKey(deviceId!);
1020 2 : final otherUserMasterKey = otherUserKeys?.masterKey;
1021 :
1022 2 : final secondKey = encodeBase64Unpadded(
1023 12 : data.sublist(10 + encodedTxnLen + 32, 10 + encodedTxnLen + 32 + 32),
1024 : );
1025 : final randomSharedSecret =
1026 10 : encodeBase64Unpadded(data.sublist(10 + encodedTxnLen + 32 + 32));
1027 :
1028 : /// `request.randomSharedSecretForQRCode` is overwritten below to send with `sendStart`
1029 4 : if ({QRMode.verifyOtherUser, QRMode.verifySelfUntrusted}
1030 2 : .contains(remoteQrMode)) {
1031 2 : if (!(ownMasterKey?.verified ?? false)) {
1032 0 : Logs().e(
1033 : '[KeyVerification] verifyQrData because you were in mode 0/2 and had untrusted msk',
1034 : );
1035 : return false;
1036 : }
1037 : }
1038 :
1039 2 : if (remoteQrMode == QRMode.verifyOtherUser &&
1040 : otherUserMasterKey != null &&
1041 : ownMasterKey != null) {
1042 2 : if (secondKey == ownMasterKey.ed25519Key) {
1043 1 : randomSharedSecretForQRCode = randomSharedSecret;
1044 1 : await verifyKeysQR(otherUserMasterKey, shower: false);
1045 : return true;
1046 : }
1047 1 : } else if (remoteQrMode == QRMode.verifySelfTrusted &&
1048 : ownMasterKey != null &&
1049 : ownDeviceKey != null) {
1050 2 : if (secondKey == ownDeviceKey.ed25519Key) {
1051 1 : randomSharedSecretForQRCode = randomSharedSecret;
1052 1 : await verifyKeysQR(ownMasterKey, shower: false);
1053 : return true;
1054 : }
1055 1 : } else if (remoteQrMode == QRMode.verifySelfUntrusted &&
1056 : ownOtherDeviceKey != null &&
1057 : ownMasterKey != null) {
1058 2 : if (secondKey == ownMasterKey.ed25519Key) {
1059 1 : randomSharedSecretForQRCode = randomSharedSecret;
1060 1 : await verifyKeysQR(ownOtherDeviceKey, shower: false);
1061 : return true;
1062 : }
1063 : }
1064 :
1065 : return false;
1066 : }
1067 :
1068 2 : Future<(String, String)?> getKeys(QRMode mode) async {
1069 4 : final keys = client.userDeviceKeys;
1070 :
1071 6 : final ownKeys = keys[client.userID];
1072 4 : final otherUserKeys = keys[userId];
1073 6 : final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
1074 2 : final ownMasterKey = ownKeys?.getCrossSigningKey('master');
1075 4 : final otherDeviceKey = otherUserKeys?.getKey(deviceId!);
1076 2 : final otherMasterKey = otherUserKeys?.getCrossSigningKey('master');
1077 :
1078 2 : if (mode == QRMode.verifyOtherUser &&
1079 : ownMasterKey != null &&
1080 : otherMasterKey != null) {
1081 : // we already have this check when sending `knownVerificationMethods`, but
1082 : // just to be safe anyway
1083 1 : if (ownMasterKey.verified) {
1084 2 : return (ownMasterKey.ed25519Key!, otherMasterKey.ed25519Key!);
1085 : }
1086 1 : } else if (mode == QRMode.verifySelfTrusted &&
1087 : ownMasterKey != null &&
1088 : otherDeviceKey != null) {
1089 1 : if (ownMasterKey.verified) {
1090 2 : return (ownMasterKey.ed25519Key!, otherDeviceKey.ed25519Key!);
1091 : }
1092 1 : } else if (mode == QRMode.verifySelfUntrusted &&
1093 : ownMasterKey != null &&
1094 : ownDeviceKey != null) {
1095 2 : return (ownDeviceKey.ed25519Key!, ownMasterKey.ed25519Key!);
1096 : }
1097 : return null;
1098 : }
1099 :
1100 2 : Future<QRCode?> generateQrCode() async {
1101 2 : final data = Uint8Buffer();
1102 : // why 11? https://github.com/matrix-org/matrix-js-sdk/commit/275ea6aacbfc6623e7559a7649ca5cab207903d9
1103 2 : randomSharedSecretForQRCode =
1104 4 : encodeBase64Unpadded(uc.secureRandomBytes(11));
1105 :
1106 2 : final mode = getOurQRMode();
1107 4 : data.addAll(ascii.encode(prefix));
1108 2 : data.add(version);
1109 4 : data.add(mode.code);
1110 4 : final encodedTxnId = utf8.encode(transactionId!);
1111 2 : final txnIdLen = encodedTxnId.length;
1112 2 : final tmpBuf = Uint8List(2);
1113 6 : ByteData.view(tmpBuf.buffer).setUint16(0, txnIdLen);
1114 2 : data.addAll(tmpBuf);
1115 2 : data.addAll(encodedTxnId);
1116 2 : final keys = await getKeys(mode);
1117 : if (keys != null) {
1118 4 : data.addAll(base64decodeUnpadded(keys.$1));
1119 4 : data.addAll(base64decodeUnpadded(keys.$2));
1120 : } else {
1121 : return null;
1122 : }
1123 :
1124 6 : data.addAll(base64decodeUnpadded(randomSharedSecretForQRCode!));
1125 4 : return QRCode(randomSharedSecretForQRCode!, data);
1126 : }
1127 : }
1128 :
1129 : abstract class _KeyVerificationMethod {
1130 : KeyVerification request;
1131 3 : Encryption get encryption => request.encryption;
1132 6 : Client get client => request.client;
1133 2 : _KeyVerificationMethod({required this.request});
1134 :
1135 : Future<void> handlePayload(String type, Map<String, dynamic> payload);
1136 0 : bool validateStart(Map<String, dynamic> payload) {
1137 : return false;
1138 : }
1139 :
1140 : late String _type;
1141 4 : String get type => _type;
1142 :
1143 : Future<void> sendStart();
1144 2 : void dispose() {}
1145 : }
1146 :
1147 : class _KeyVerificationMethodQRReciprocate extends _KeyVerificationMethod {
1148 2 : _KeyVerificationMethodQRReciprocate({required super.request});
1149 :
1150 : @override
1151 : // ignore: overridden_fields
1152 : final _type = EventTypes.Reciprocate;
1153 :
1154 2 : @override
1155 : bool validateStart(Map<String, dynamic> payload) {
1156 6 : if (payload['method'] != type) return false;
1157 8 : if (payload['secret'] != request.randomSharedSecretForQRCode) return false;
1158 : return true;
1159 : }
1160 :
1161 2 : @override
1162 : Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
1163 : try {
1164 : switch (type) {
1165 2 : case EventTypes.KeyVerificationStart:
1166 6 : if (!(await request.verifyLastStep([
1167 : EventTypes.KeyVerificationReady,
1168 : EventTypes.KeyVerificationRequest,
1169 : ]))) {
1170 : return; // abort
1171 : }
1172 2 : if (!validateStart(payload)) {
1173 2 : await request.cancel('m.invalid_message');
1174 : return;
1175 : }
1176 4 : request.setState(KeyVerificationState.confirmQRScan);
1177 : break;
1178 : }
1179 : } catch (e, s) {
1180 0 : Logs().e('[Key Verification Reciprocate] An error occured', e, s);
1181 0 : if (request.deviceId != null) {
1182 0 : await request.cancel('m.invalid_message');
1183 : }
1184 : }
1185 : }
1186 :
1187 2 : Future<void> acceptQRScanConfirmation() async {
1188 : // secret validation already done in validateStart
1189 :
1190 4 : final ourQRMode = request.getOurQRMode();
1191 : SignableKey? keyToVerify;
1192 :
1193 2 : if (ourQRMode == QRMode.verifyOtherUser) {
1194 6 : keyToVerify = client.userDeviceKeys[request.userId]?.masterKey;
1195 1 : } else if (ourQRMode == QRMode.verifySelfTrusted) {
1196 : keyToVerify =
1197 9 : client.userDeviceKeys[client.userID]?.deviceKeys[request.deviceId];
1198 1 : } else if (ourQRMode == QRMode.verifySelfUntrusted) {
1199 6 : keyToVerify = client.userDeviceKeys[client.userID]?.masterKey;
1200 : }
1201 : if (keyToVerify != null) {
1202 4 : await request.verifyKeysQR(keyToVerify, shower: true);
1203 : } else {
1204 0 : Logs().e('[KeyVerification], verifying keys failed');
1205 0 : await request.cancel('m.invalid_key');
1206 : }
1207 : }
1208 :
1209 2 : @override
1210 : Future<void> sendStart() async {
1211 2 : final payload = <String, dynamic>{
1212 2 : 'method': type,
1213 4 : 'secret': request.randomSharedSecretForQRCode,
1214 : };
1215 4 : request.makePayload(payload);
1216 4 : await request.send(EventTypes.KeyVerificationStart, payload);
1217 : }
1218 :
1219 2 : @override
1220 : void dispose() {}
1221 : }
1222 :
1223 : enum QRMode {
1224 : verifyOtherUser(0x00),
1225 : verifySelfTrusted(0x01),
1226 : verifySelfUntrusted(0x02);
1227 :
1228 : const QRMode(this.code);
1229 : final int code;
1230 : }
1231 :
1232 : class QRCode {
1233 : /// You actually never need this when implementing in a client, its just to
1234 : /// make tests easier. Just pass `qrDataRawBytes` in `continueVerifcation()`
1235 : final String randomSharedSecret;
1236 : final Uint8Buffer qrDataRawBytes;
1237 2 : QRCode(this.randomSharedSecret, this.qrDataRawBytes);
1238 : }
1239 :
1240 : const knownKeyAgreementProtocols = ['curve25519-hkdf-sha256', 'curve25519'];
1241 : const knownHashes = ['sha256'];
1242 : const knownHashesAuthentificationCodes = [
1243 : 'hkdf-hmac-sha256.v2',
1244 : 'hkdf-hmac-sha256',
1245 : ];
1246 :
1247 : class _KeyVerificationMethodSas extends _KeyVerificationMethod {
1248 2 : _KeyVerificationMethodSas({required super.request});
1249 :
1250 : @override
1251 : // ignore: overridden_fields
1252 : final _type = EventTypes.Sas;
1253 :
1254 : String? keyAgreementProtocol;
1255 : String? hash;
1256 : String? messageAuthenticationCode;
1257 : List<String>? authenticationTypes;
1258 : late String startCanonicalJson;
1259 : String? commitment;
1260 : late String theirPublicKey;
1261 : Map<String, dynamic>? macPayload;
1262 : vod.Sas? sas;
1263 : vod.EstablishedSas? establishedSas;
1264 :
1265 2 : List<String> get knownAuthentificationTypes {
1266 2 : final types = <String>[];
1267 6 : if (request.client.verificationMethods
1268 2 : .contains(KeyVerificationMethod.emoji)) {
1269 2 : types.add('emoji');
1270 : }
1271 6 : if (request.client.verificationMethods
1272 2 : .contains(KeyVerificationMethod.numbers)) {
1273 2 : types.add('decimal');
1274 : }
1275 : return types;
1276 : }
1277 :
1278 1 : @override
1279 : Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
1280 : try {
1281 : switch (type) {
1282 1 : case EventTypes.KeyVerificationStart:
1283 3 : if (!(await request.verifyLastStep([
1284 : EventTypes.KeyVerificationReady,
1285 : EventTypes.KeyVerificationRequest,
1286 : EventTypes.KeyVerificationStart,
1287 : ]))) {
1288 : return; // abort
1289 : }
1290 1 : if (!validateStart(payload)) {
1291 0 : await request.cancel('m.unknown_method');
1292 : return;
1293 : }
1294 1 : await _sendAccept();
1295 : break;
1296 1 : case EventTypes.KeyVerificationAccept:
1297 3 : if (!(await request.verifyLastStep([
1298 : EventTypes.KeyVerificationReady,
1299 : EventTypes.KeyVerificationRequest,
1300 : ]))) {
1301 : return;
1302 : }
1303 1 : if (!_handleAccept(payload)) {
1304 0 : await request.cancel('m.unknown_method');
1305 : return;
1306 : }
1307 1 : await _sendKey();
1308 : break;
1309 1 : case 'm.key.verification.key':
1310 3 : if (!(await request.verifyLastStep([
1311 : EventTypes.KeyVerificationAccept,
1312 : EventTypes.KeyVerificationStart,
1313 : ]))) {
1314 : return;
1315 : }
1316 1 : _handleKey(payload);
1317 3 : if (request.lastStep == EventTypes.KeyVerificationStart) {
1318 : // we need to send our key
1319 1 : await _sendKey();
1320 : } else {
1321 : // we already sent our key, time to verify the commitment being valid
1322 2 : if (await _validateCommitment() == false) {
1323 0 : await request.cancel('m.mismatched_commitment');
1324 : return;
1325 : }
1326 : }
1327 2 : request.setState(KeyVerificationState.askSas);
1328 : break;
1329 1 : case 'm.key.verification.mac':
1330 3 : if (!(await request.verifyLastStep(['m.key.verification.key']))) {
1331 : return;
1332 : }
1333 1 : macPayload = payload;
1334 3 : if (request.state == KeyVerificationState.waitingSas) {
1335 1 : await _processMac();
1336 : }
1337 : break;
1338 : }
1339 : } catch (err, stacktrace) {
1340 0 : Logs().e('[Key Verification SAS] An error occured', err, stacktrace);
1341 0 : if (request.deviceId != null) {
1342 0 : await request.cancel('m.invalid_message');
1343 : }
1344 : }
1345 : }
1346 :
1347 1 : Future<void> acceptSas() async {
1348 1 : await _sendMac();
1349 2 : request.setState(KeyVerificationState.waitingSas);
1350 1 : if (macPayload != null) {
1351 1 : await _processMac();
1352 : }
1353 : }
1354 :
1355 1 : Future<void> rejectSas() async {
1356 2 : await request.cancel('m.mismatched_sas');
1357 : }
1358 :
1359 2 : @override
1360 : Future<void> sendStart() async {
1361 2 : final payload = <String, dynamic>{
1362 2 : 'method': type,
1363 : 'key_agreement_protocols': knownKeyAgreementProtocols,
1364 : 'hashes': knownHashes,
1365 : 'message_authentication_codes': knownHashesAuthentificationCodes,
1366 2 : 'short_authentication_string': knownAuthentificationTypes,
1367 : };
1368 4 : request.makePayload(payload);
1369 : // We just store the canonical json in here for later verification
1370 6 : startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload));
1371 4 : await request.send(EventTypes.KeyVerificationStart, payload);
1372 : }
1373 :
1374 1 : @override
1375 : bool validateStart(Map<String, dynamic> payload) {
1376 3 : if (payload['method'] != type) {
1377 : return false;
1378 : }
1379 1 : final possibleKeyAgreementProtocols = _intersect(
1380 : knownKeyAgreementProtocols,
1381 1 : payload['key_agreement_protocols'],
1382 : );
1383 1 : if (possibleKeyAgreementProtocols.isEmpty) {
1384 : return false;
1385 : }
1386 2 : keyAgreementProtocol = possibleKeyAgreementProtocols.first;
1387 2 : final possibleHashes = _intersect(knownHashes, payload['hashes']);
1388 1 : if (possibleHashes.isEmpty) {
1389 : return false;
1390 : }
1391 2 : hash = possibleHashes.first;
1392 1 : final possibleMessageAuthenticationCodes = _intersect(
1393 : knownHashesAuthentificationCodes,
1394 1 : payload['message_authentication_codes'],
1395 : );
1396 1 : if (possibleMessageAuthenticationCodes.isEmpty) {
1397 : return false;
1398 : }
1399 :
1400 : // intersect should make sure we choose v2 over the dep'd one
1401 2 : messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
1402 1 : final possibleAuthenticationTypes = _intersect(
1403 1 : knownAuthentificationTypes,
1404 1 : payload['short_authentication_string'],
1405 : );
1406 1 : if (possibleAuthenticationTypes.isEmpty) {
1407 : return false;
1408 : }
1409 1 : authenticationTypes = possibleAuthenticationTypes;
1410 3 : startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload));
1411 : return true;
1412 : }
1413 :
1414 1 : Future<void> _sendAccept() async {
1415 2 : final sas = this.sas = vod.Sas();
1416 4 : commitment = await _makeCommitment(sas.publicKey, startCanonicalJson);
1417 3 : await request.send(EventTypes.KeyVerificationAccept, {
1418 1 : 'method': type,
1419 1 : 'key_agreement_protocol': keyAgreementProtocol,
1420 1 : 'hash': hash,
1421 1 : 'message_authentication_code': messageAuthenticationCode,
1422 1 : 'short_authentication_string': authenticationTypes,
1423 1 : 'commitment': commitment,
1424 : });
1425 : }
1426 :
1427 1 : bool _handleAccept(Map<String, dynamic> payload) {
1428 : if (!knownKeyAgreementProtocols
1429 2 : .contains(payload['key_agreement_protocol'])) {
1430 : return false;
1431 : }
1432 2 : keyAgreementProtocol = payload['key_agreement_protocol'];
1433 2 : if (!knownHashes.contains(payload['hash'])) {
1434 : return false;
1435 : }
1436 2 : hash = payload['hash'];
1437 : if (!knownHashesAuthentificationCodes
1438 2 : .contains(payload['message_authentication_code'])) {
1439 : return false;
1440 : }
1441 2 : messageAuthenticationCode = payload['message_authentication_code'];
1442 1 : final possibleAuthenticationTypes = _intersect(
1443 1 : knownAuthentificationTypes,
1444 1 : payload['short_authentication_string'],
1445 : );
1446 1 : if (possibleAuthenticationTypes.isEmpty) {
1447 : return false;
1448 : }
1449 1 : authenticationTypes = possibleAuthenticationTypes;
1450 2 : commitment = payload['commitment'];
1451 2 : sas = vod.Sas();
1452 : return true;
1453 : }
1454 :
1455 1 : Future<void> _sendKey() async {
1456 3 : await request.send('m.key.verification.key', {
1457 2 : 'key': sas!.publicKey,
1458 : });
1459 : }
1460 :
1461 1 : void _handleKey(Map<String, dynamic> payload) {
1462 2 : theirPublicKey = payload['key'];
1463 1 : final sas = this.sas;
1464 1 : if (sas == null || sas.disposed) {
1465 0 : throw Exception('SAS object is disposed');
1466 : }
1467 3 : establishedSas = sas.establishSasSecret(payload['key']);
1468 : }
1469 :
1470 1 : Future<bool> _validateCommitment() async {
1471 : final checkCommitment =
1472 3 : await _makeCommitment(theirPublicKey, startCanonicalJson);
1473 2 : return commitment == checkCommitment;
1474 : }
1475 :
1476 1 : Uint8List makeSas(int bytes) {
1477 : var sasInfo = '';
1478 2 : if (keyAgreementProtocol == 'curve25519-hkdf-sha256') {
1479 7 : final ourInfo = '${client.userID}|${client.deviceID}|${sas!.publicKey}|';
1480 : final theirInfo =
1481 6 : '${request.userId}|${request.deviceId}|$theirPublicKey|';
1482 : sasInfo =
1483 7 : 'MATRIX_KEY_VERIFICATION_SAS|${request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo}${request.transactionId!}';
1484 0 : } else if (keyAgreementProtocol == 'curve25519') {
1485 0 : final ourInfo = client.userID! + client.deviceID!;
1486 0 : final theirInfo = request.userId + request.deviceId!;
1487 : sasInfo =
1488 0 : 'MATRIX_KEY_VERIFICATION_SAS${request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo}${request.transactionId!}';
1489 : } else {
1490 0 : throw Exception('Unknown key agreement protocol');
1491 : }
1492 2 : return establishedSas!.generateBytes(sasInfo, bytes);
1493 : }
1494 :
1495 1 : Future<void> _sendMac() async {
1496 : final baseInfo =
1497 11 : 'MATRIX_KEY_VERIFICATION_MAC${client.userID!}${client.deviceID!}${request.userId}${request.deviceId!}${request.transactionId!}';
1498 1 : final mac = <String, String>{};
1499 1 : final keyList = <String>[];
1500 :
1501 : // now add all the keys we want the other to verify
1502 : // for now it is just our device key, once we have cross-signing
1503 : // we would also add the cross signing key here
1504 3 : final deviceKeyId = 'ed25519:${client.deviceID}';
1505 1 : mac[deviceKeyId] =
1506 4 : _calculateMac(encryption.fingerprintKey!, baseInfo + deviceKeyId);
1507 1 : keyList.add(deviceKeyId);
1508 :
1509 6 : final masterKey = client.userDeviceKeys[client.userID]?.masterKey;
1510 1 : if (masterKey != null && masterKey.verified) {
1511 : // we have our own master key verified, let's send it!
1512 2 : final masterKeyId = 'ed25519:${masterKey.publicKey}';
1513 1 : mac[masterKeyId] =
1514 3 : _calculateMac(masterKey.publicKey!, baseInfo + masterKeyId);
1515 1 : keyList.add(masterKeyId);
1516 : }
1517 :
1518 1 : keyList.sort();
1519 3 : final keys = _calculateMac(keyList.join(','), '${baseInfo}KEY_IDS');
1520 3 : await request.send('m.key.verification.mac', {
1521 : 'mac': mac,
1522 : 'keys': keys,
1523 : });
1524 : }
1525 :
1526 1 : Future<void> _processMac() async {
1527 1 : final payload = macPayload!;
1528 : final baseInfo =
1529 11 : 'MATRIX_KEY_VERIFICATION_MAC${request.userId}${request.deviceId!}${client.userID!}${client.deviceID!}${request.transactionId!}';
1530 :
1531 3 : final keyList = payload['mac'].keys.toList();
1532 1 : keyList.sort();
1533 2 : if (payload['keys'] !=
1534 3 : _calculateMac(keyList.join(','), '${baseInfo}KEY_IDS')) {
1535 0 : await request.cancel('m.key_mismatch');
1536 : return;
1537 : }
1538 :
1539 5 : if (!client.userDeviceKeys.containsKey(request.userId)) {
1540 0 : await request.cancel('m.key_mismatch');
1541 : return;
1542 : }
1543 1 : final mac = <String, String>{};
1544 3 : for (final entry in payload['mac'].entries) {
1545 2 : if (entry.value is String) {
1546 3 : mac[entry.key] = entry.value;
1547 : }
1548 : }
1549 3 : await request.verifyKeysSAS(mac, (String mac, SignableKey key) async {
1550 1 : return mac ==
1551 1 : _calculateMac(
1552 1 : key.ed25519Key!,
1553 2 : '${baseInfo}ed25519:${key.identifier!}',
1554 : );
1555 : });
1556 : }
1557 :
1558 1 : Future<String> _makeCommitment(String pubKey, String canonicalJson) async {
1559 2 : if (hash == 'sha256') {
1560 3 : final bytes = utf8.encoder.convert(pubKey + canonicalJson);
1561 1 : final digest = vod.CryptoUtils.sha256(input: bytes);
1562 1 : return encodeBase64Unpadded(digest);
1563 : }
1564 0 : throw Exception('Unknown hash method');
1565 : }
1566 :
1567 1 : String _calculateMac(String input, String info) {
1568 2 : if (messageAuthenticationCode == 'hkdf-hmac-sha256.v2') {
1569 2 : return establishedSas!.calculateMac(input, info);
1570 0 : } else if (messageAuthenticationCode == 'hkdf-hmac-sha256') {
1571 0 : return establishedSas!.calculateMacDeprecated(input, info);
1572 : } else {
1573 0 : throw Exception('Unknown message authentification code');
1574 : }
1575 : }
1576 : }
1577 :
1578 : const _emojiMap = [
1579 : {
1580 : 'emoji': '\u{1F436}',
1581 : 'name': 'Dog',
1582 : },
1583 : {
1584 : 'emoji': '\u{1F431}',
1585 : 'name': 'Cat',
1586 : },
1587 : {
1588 : 'emoji': '\u{1F981}',
1589 : 'name': 'Lion',
1590 : },
1591 : {
1592 : 'emoji': '\u{1F40E}',
1593 : 'name': 'Horse',
1594 : },
1595 : {
1596 : 'emoji': '\u{1F984}',
1597 : 'name': 'Unicorn',
1598 : },
1599 : {
1600 : 'emoji': '\u{1F437}',
1601 : 'name': 'Pig',
1602 : },
1603 : {
1604 : 'emoji': '\u{1F418}',
1605 : 'name': 'Elephant',
1606 : },
1607 : {
1608 : 'emoji': '\u{1F430}',
1609 : 'name': 'Rabbit',
1610 : },
1611 : {
1612 : 'emoji': '\u{1F43C}',
1613 : 'name': 'Panda',
1614 : },
1615 : {
1616 : 'emoji': '\u{1F413}',
1617 : 'name': 'Rooster',
1618 : },
1619 : {
1620 : 'emoji': '\u{1F427}',
1621 : 'name': 'Penguin',
1622 : },
1623 : {
1624 : 'emoji': '\u{1F422}',
1625 : 'name': 'Turtle',
1626 : },
1627 : {
1628 : 'emoji': '\u{1F41F}',
1629 : 'name': 'Fish',
1630 : },
1631 : {
1632 : 'emoji': '\u{1F419}',
1633 : 'name': 'Octopus',
1634 : },
1635 : {
1636 : 'emoji': '\u{1F98B}',
1637 : 'name': 'Butterfly',
1638 : },
1639 : {
1640 : 'emoji': '\u{1F337}',
1641 : 'name': 'Flower',
1642 : },
1643 : {
1644 : 'emoji': '\u{1F333}',
1645 : 'name': 'Tree',
1646 : },
1647 : {
1648 : 'emoji': '\u{1F335}',
1649 : 'name': 'Cactus',
1650 : },
1651 : {
1652 : 'emoji': '\u{1F344}',
1653 : 'name': 'Mushroom',
1654 : },
1655 : {
1656 : 'emoji': '\u{1F30F}',
1657 : 'name': 'Globe',
1658 : },
1659 : {
1660 : 'emoji': '\u{1F319}',
1661 : 'name': 'Moon',
1662 : },
1663 : {
1664 : 'emoji': '\u{2601}\u{FE0F}',
1665 : 'name': 'Cloud',
1666 : },
1667 : {
1668 : 'emoji': '\u{1F525}',
1669 : 'name': 'Fire',
1670 : },
1671 : {
1672 : 'emoji': '\u{1F34C}',
1673 : 'name': 'Banana',
1674 : },
1675 : {
1676 : 'emoji': '\u{1F34E}',
1677 : 'name': 'Apple',
1678 : },
1679 : {
1680 : 'emoji': '\u{1F353}',
1681 : 'name': 'Strawberry',
1682 : },
1683 : {
1684 : 'emoji': '\u{1F33D}',
1685 : 'name': 'Corn',
1686 : },
1687 : {
1688 : 'emoji': '\u{1F355}',
1689 : 'name': 'Pizza',
1690 : },
1691 : {
1692 : 'emoji': '\u{1F382}',
1693 : 'name': 'Cake',
1694 : },
1695 : {
1696 : 'emoji': '\u{2764}\u{FE0F}',
1697 : 'name': 'Heart',
1698 : },
1699 : {
1700 : 'emoji': '\u{1F600}',
1701 : 'name': 'Smiley',
1702 : },
1703 : {
1704 : 'emoji': '\u{1F916}',
1705 : 'name': 'Robot',
1706 : },
1707 : {
1708 : 'emoji': '\u{1F3A9}',
1709 : 'name': 'Hat',
1710 : },
1711 : {
1712 : 'emoji': '\u{1F453}',
1713 : 'name': 'Glasses',
1714 : },
1715 : {
1716 : 'emoji': '\u{1F527}',
1717 : 'name': 'Spanner',
1718 : },
1719 : {
1720 : 'emoji': '\u{1F385}',
1721 : 'name': 'Santa',
1722 : },
1723 : {
1724 : 'emoji': '\u{1F44D}',
1725 : 'name': 'Thumbs Up',
1726 : },
1727 : {
1728 : 'emoji': '\u{2602}\u{FE0F}',
1729 : 'name': 'Umbrella',
1730 : },
1731 : {
1732 : 'emoji': '\u{231B}',
1733 : 'name': 'Hourglass',
1734 : },
1735 : {
1736 : 'emoji': '\u{23F0}',
1737 : 'name': 'Clock',
1738 : },
1739 : {
1740 : 'emoji': '\u{1F381}',
1741 : 'name': 'Gift',
1742 : },
1743 : {
1744 : 'emoji': '\u{1F4A1}',
1745 : 'name': 'Light Bulb',
1746 : },
1747 : {
1748 : 'emoji': '\u{1F4D5}',
1749 : 'name': 'Book',
1750 : },
1751 : {
1752 : 'emoji': '\u{270F}\u{FE0F}',
1753 : 'name': 'Pencil',
1754 : },
1755 : {
1756 : 'emoji': '\u{1F4CE}',
1757 : 'name': 'Paperclip',
1758 : },
1759 : {
1760 : 'emoji': '\u{2702}\u{FE0F}',
1761 : 'name': 'Scissors',
1762 : },
1763 : {
1764 : 'emoji': '\u{1F512}',
1765 : 'name': 'Lock',
1766 : },
1767 : {
1768 : 'emoji': '\u{1F511}',
1769 : 'name': 'Key',
1770 : },
1771 : {
1772 : 'emoji': '\u{1F528}',
1773 : 'name': 'Hammer',
1774 : },
1775 : {
1776 : 'emoji': '\u{260E}\u{FE0F}',
1777 : 'name': 'Telephone',
1778 : },
1779 : {
1780 : 'emoji': '\u{1F3C1}',
1781 : 'name': 'Flag',
1782 : },
1783 : {
1784 : 'emoji': '\u{1F682}',
1785 : 'name': 'Train',
1786 : },
1787 : {
1788 : 'emoji': '\u{1F6B2}',
1789 : 'name': 'Bicycle',
1790 : },
1791 : {
1792 : 'emoji': '\u{2708}\u{FE0F}',
1793 : 'name': 'Aeroplane',
1794 : },
1795 : {
1796 : 'emoji': '\u{1F680}',
1797 : 'name': 'Rocket',
1798 : },
1799 : {
1800 : 'emoji': '\u{1F3C6}',
1801 : 'name': 'Trophy',
1802 : },
1803 : {
1804 : 'emoji': '\u{26BD}',
1805 : 'name': 'Ball',
1806 : },
1807 : {
1808 : 'emoji': '\u{1F3B8}',
1809 : 'name': 'Guitar',
1810 : },
1811 : {
1812 : 'emoji': '\u{1F3BA}',
1813 : 'name': 'Trumpet',
1814 : },
1815 : {
1816 : 'emoji': '\u{1F514}',
1817 : 'name': 'Bell',
1818 : },
1819 : {
1820 : 'emoji': '\u{2693}',
1821 : 'name': 'Anchor',
1822 : },
1823 : {
1824 : 'emoji': '\u{1F3A7}',
1825 : 'name': 'Headphones',
1826 : },
1827 : {
1828 : 'emoji': '\u{1F4C1}',
1829 : 'name': 'Folder',
1830 : },
1831 : {
1832 : 'emoji': '\u{1F4CC}',
1833 : 'name': 'Pin',
1834 : },
1835 : ];
1836 :
1837 : class KeyVerificationEmoji {
1838 : final int number;
1839 1 : KeyVerificationEmoji(this.number);
1840 :
1841 4 : String get emoji => _emojiMap[number]['emoji'] ?? '';
1842 4 : String get name => _emojiMap[number]['name'] ?? '';
1843 : }
|