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