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,
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 : // ignore: unawaited_futures
751 2 : encryption.ssss
752 4 : .maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList());
753 2 : if (requestInterval.length <= i) {
754 : return;
755 : }
756 1 : Timer(
757 2 : Duration(seconds: requestInterval[i]),
758 0 : () => maybeRequestSSSSSecrets(i + 1),
759 : );
760 : }
761 :
762 1 : Future<void> verifyKeysSAS(
763 : Map<String, String> keys,
764 : Future<bool> Function(String, SignableKey) verifier,
765 : ) async {
766 2 : _verifiedDevices = <SignableKey>[];
767 :
768 4 : final userDeviceKey = client.userDeviceKeys[userId];
769 : if (userDeviceKey == null) {
770 0 : await cancel('m.key_mismatch');
771 : return;
772 : }
773 2 : for (final entry in keys.entries) {
774 1 : final keyId = entry.key;
775 2 : final verifyDeviceId = keyId.substring('ed25519:'.length);
776 1 : final keyInfo = entry.value;
777 1 : final key = userDeviceKey.getKey(verifyDeviceId);
778 : if (key != null) {
779 1 : if (!(await verifier(keyInfo, key))) {
780 0 : await cancel('m.key_mismatch');
781 : return;
782 : }
783 2 : _verifiedDevices.add(key);
784 : }
785 : }
786 : // okay, we reached this far, so all the devices are verified!
787 : var verifiedMasterKey = false;
788 2 : final wasUnknownSession = client.isUnknownSession;
789 2 : for (final key in _verifiedDevices) {
790 1 : await key.setVerified(
791 : true,
792 : false,
793 : ); // we don't want to sign the keys juuuust yet
794 3 : if (key is CrossSigningKey && key.usage.contains('master')) {
795 : verifiedMasterKey = true;
796 : }
797 : }
798 4 : if (verifiedMasterKey && userId == client.userID) {
799 : // it was our own master key, let's request the cross signing keys
800 : // we do it in the background, thus no await needed here
801 : // ignore: unawaited_futures
802 0 : maybeRequestSSSSSecrets();
803 : }
804 2 : await send(EventTypes.KeyVerificationDone, {});
805 :
806 : var askingSSSS = false;
807 3 : if (encryption.crossSigning.enabled &&
808 4 : encryption.crossSigning.signable(_verifiedDevices)) {
809 : // these keys can be signed! Let's do so
810 3 : if (await encryption.crossSigning.isCached()) {
811 : // we want to make sure the verification state is correct for the other party after this event is handled.
812 : // Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
813 0 : await encryption.crossSigning.sign(_verifiedDevices);
814 : } else if (!wasUnknownSession) {
815 : askingSSSS = true;
816 : }
817 : }
818 : if (askingSSSS) {
819 1 : setState(KeyVerificationState.askSSSS);
820 1 : _nextAction = 'done';
821 : } else {
822 1 : setState(KeyVerificationState.done);
823 : }
824 : }
825 :
826 : /// shower is true only for reciprocated verifications (shower side)
827 2 : Future<void> verifyKeysQR(SignableKey key, {bool shower = true}) async {
828 : var verifiedMasterKey = false;
829 4 : final wasUnknownSession = client.isUnknownSession;
830 :
831 2 : key.setDirectVerified(true);
832 6 : if (key is CrossSigningKey && key.usage.contains('master')) {
833 : verifiedMasterKey = true;
834 : }
835 :
836 8 : if (verifiedMasterKey && userId == client.userID) {
837 : // it was our own master key, let's request the cross signing keys
838 : // we do it in the background, thus no await needed here
839 : // ignore: unawaited_futures
840 1 : maybeRequestSSSSSecrets();
841 : }
842 : if (shower) {
843 4 : await send(EventTypes.KeyVerificationDone, {});
844 : }
845 4 : final keyList = List<SignableKey>.from([key]);
846 : var askingSSSS = false;
847 6 : if (encryption.crossSigning.enabled &&
848 6 : encryption.crossSigning.signable(keyList)) {
849 : // these keys can be signed! Let's do so
850 6 : if (await encryption.crossSigning.isCached()) {
851 : // we want to make sure the verification state is correct for the other party after this event is handled.
852 : // Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
853 6 : await encryption.crossSigning.sign(keyList);
854 : } else if (!wasUnknownSession) {
855 : askingSSSS = true;
856 : }
857 : }
858 : if (askingSSSS) {
859 : // no need to worry about shower/scanner here because if scanner was
860 : // verified, ssss is already
861 1 : setState(KeyVerificationState.askSSSS);
862 : if (shower) {
863 1 : _nextAction = 'done';
864 : } else {
865 0 : _nextAction = 'showQRSuccess';
866 : }
867 : } else {
868 : if (shower) {
869 2 : setState(KeyVerificationState.done);
870 : } else {
871 2 : setState(KeyVerificationState.showQRSuccess);
872 : }
873 : }
874 : }
875 :
876 2 : Future<bool> verifyActivity() async {
877 10 : if (lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
878 4 : lastActivity = DateTime.now();
879 : return true;
880 : }
881 0 : await cancel('m.timeout');
882 : return false;
883 : }
884 :
885 2 : Future<bool> verifyLastStep(List<String?> checkLastStep) async {
886 2 : if (!(await verifyActivity())) {
887 : return false;
888 : }
889 4 : if (checkLastStep.contains(lastStep)) {
890 : return true;
891 : }
892 0 : Logs().e(
893 0 : '[KeyVerificaton] lastStep mismatch cancelling, expected from ${checkLastStep.toString()} was ${lastStep.toString()}',
894 : );
895 0 : await cancel('m.unexpected_message');
896 : return false;
897 : }
898 :
899 2 : Future<void> cancel([String code = 'm.unknown', bool quiet = false]) async {
900 3 : if (!quiet && (deviceId != null || room != null)) {
901 4 : await send(EventTypes.KeyVerificationCancel, {
902 : 'reason': code,
903 : 'code': code,
904 : });
905 : }
906 2 : canceled = true;
907 2 : canceledCode = code;
908 2 : setState(KeyVerificationState.error);
909 : }
910 :
911 3 : void makePayload(Map<String, dynamic> payload) {
912 9 : payload['from_device'] = client.deviceID;
913 3 : if (transactionId != null) {
914 3 : if (room != null) {
915 2 : payload['m.relates_to'] = {
916 : 'rel_type': 'm.reference',
917 1 : 'event_id': transactionId,
918 : };
919 : } else {
920 4 : payload['transaction_id'] = transactionId;
921 : }
922 : }
923 : }
924 :
925 3 : Future<void> send(
926 : String type,
927 : Map<String, dynamic> payload,
928 : ) async {
929 3 : makePayload(payload);
930 9 : Logs().i('[Key Verification] Sending type $type: $payload');
931 3 : if (room != null) {
932 12 : Logs().i('[Key Verification] Sending to $userId in room ${room!.id}...');
933 4 : if ({EventTypes.KeyVerificationRequest}.contains(type)) {
934 2 : payload['msgtype'] = type;
935 4 : payload['to'] = userId;
936 2 : payload['body'] =
937 2 : 'Attempting verification request. ($type) Apparently your client doesn\'t support this';
938 : type = EventTypes.Message;
939 : }
940 4 : final newTransactionId = await room!.sendEvent(payload, type: type);
941 2 : if (transactionId == null) {
942 2 : transactionId = newTransactionId;
943 6 : encryption.keyVerificationManager.addRequest(this);
944 : }
945 : } else {
946 10 : Logs().i('[Key Verification] Sending to $userId device $deviceId...');
947 4 : if (deviceId == '*') {
948 : if ({
949 1 : EventTypes.KeyVerificationRequest,
950 1 : EventTypes.KeyVerificationCancel,
951 1 : }.contains(type)) {
952 : final deviceKeys =
953 7 : client.userDeviceKeys[userId]?.deviceKeys.values.where(
954 2 : (deviceKey) => deviceKey.hasValidSignatureChain(
955 : verifiedByTheirMasterKey: true,
956 : ),
957 : );
958 :
959 : if (deviceKeys != null) {
960 2 : await client.sendToDeviceEncrypted(
961 1 : deviceKeys.toList(),
962 : type,
963 : payload,
964 : );
965 : }
966 : } else {
967 0 : Logs().e(
968 0 : '[Key Verification] Tried to broadcast and un-broadcastable type: $type',
969 : );
970 : }
971 : } else {
972 14 : if (client.userDeviceKeys[userId]?.deviceKeys[deviceId] != null) {
973 4 : await client.sendToDeviceEncrypted(
974 16 : [client.userDeviceKeys[userId]!.deviceKeys[deviceId]!],
975 : type,
976 : payload,
977 : );
978 : } else {
979 0 : Logs().e('[Key Verification] Unknown device');
980 : }
981 : }
982 : }
983 : }
984 :
985 3 : void setState(KeyVerificationState newState) {
986 6 : if (state != KeyVerificationState.error) {
987 3 : state = newState;
988 : }
989 :
990 3 : onUpdate?.call();
991 : }
992 :
993 : static const String prefix = 'MATRIX';
994 : static const int version = 0x02;
995 :
996 2 : Future<bool> verifyQrData(Uint8List qrDataRawBytes) async {
997 : final data = qrDataRawBytes;
998 : // hardcoded stuff + 2 keys + secret
999 18 : if (data.length < 10 + 32 + 32 + 8 + utf8.encode(transactionId!).length) {
1000 : return false;
1001 : }
1002 4 : if (data[6] != version) return false;
1003 : final remoteQrMode =
1004 10 : QRMode.values.singleWhere((mode) => mode.code == data[7]);
1005 6 : if (ascii.decode(data.sublist(0, 6)) != prefix) return false;
1006 4 : if (data[6] != version) return false;
1007 8 : final tmpBuf = Uint8List.fromList([data[8], data[9]]);
1008 6 : final encodedTxnLen = ByteData.view(tmpBuf.buffer).getUint16(0);
1009 10 : if (utf8.decode(data.sublist(10, 10 + encodedTxnLen)) != transactionId) {
1010 : return false;
1011 : }
1012 4 : final keys = client.userDeviceKeys;
1013 :
1014 6 : final ownKeys = keys[client.userID];
1015 4 : final otherUserKeys = keys[userId];
1016 2 : final ownMasterKey = ownKeys?.getCrossSigningKey('master');
1017 6 : final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
1018 4 : final ownOtherDeviceKey = ownKeys?.getKey(deviceId!);
1019 2 : final otherUserMasterKey = otherUserKeys?.masterKey;
1020 :
1021 2 : final secondKey = encodeBase64Unpadded(
1022 12 : data.sublist(10 + encodedTxnLen + 32, 10 + encodedTxnLen + 32 + 32),
1023 : );
1024 : final randomSharedSecret =
1025 10 : encodeBase64Unpadded(data.sublist(10 + encodedTxnLen + 32 + 32));
1026 :
1027 : /// `request.randomSharedSecretForQRCode` is overwritten below to send with `sendStart`
1028 4 : if ({QRMode.verifyOtherUser, QRMode.verifySelfUntrusted}
1029 2 : .contains(remoteQrMode)) {
1030 2 : if (!(ownMasterKey?.verified ?? false)) {
1031 0 : Logs().e(
1032 : '[KeyVerification] verifyQrData because you were in mode 0/2 and had untrusted msk',
1033 : );
1034 : return false;
1035 : }
1036 : }
1037 :
1038 2 : if (remoteQrMode == QRMode.verifyOtherUser &&
1039 : otherUserMasterKey != null &&
1040 : ownMasterKey != null) {
1041 2 : if (secondKey == ownMasterKey.ed25519Key) {
1042 1 : randomSharedSecretForQRCode = randomSharedSecret;
1043 1 : await verifyKeysQR(otherUserMasterKey, shower: false);
1044 : return true;
1045 : }
1046 1 : } else if (remoteQrMode == QRMode.verifySelfTrusted &&
1047 : ownMasterKey != null &&
1048 : ownDeviceKey != null) {
1049 2 : if (secondKey == ownDeviceKey.ed25519Key) {
1050 1 : randomSharedSecretForQRCode = randomSharedSecret;
1051 1 : await verifyKeysQR(ownMasterKey, shower: false);
1052 : return true;
1053 : }
1054 1 : } else if (remoteQrMode == QRMode.verifySelfUntrusted &&
1055 : ownOtherDeviceKey != null &&
1056 : ownMasterKey != null) {
1057 2 : if (secondKey == ownMasterKey.ed25519Key) {
1058 1 : randomSharedSecretForQRCode = randomSharedSecret;
1059 1 : await verifyKeysQR(ownOtherDeviceKey, shower: false);
1060 : return true;
1061 : }
1062 : }
1063 :
1064 : return false;
1065 : }
1066 :
1067 2 : Future<(String, String)?> getKeys(QRMode mode) async {
1068 4 : final keys = client.userDeviceKeys;
1069 :
1070 6 : final ownKeys = keys[client.userID];
1071 4 : final otherUserKeys = keys[userId];
1072 6 : final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
1073 2 : final ownMasterKey = ownKeys?.getCrossSigningKey('master');
1074 4 : final otherDeviceKey = otherUserKeys?.getKey(deviceId!);
1075 2 : final otherMasterKey = otherUserKeys?.getCrossSigningKey('master');
1076 :
1077 2 : if (mode == QRMode.verifyOtherUser &&
1078 : ownMasterKey != null &&
1079 : otherMasterKey != null) {
1080 : // we already have this check when sending `knownVerificationMethods`, but
1081 : // just to be safe anyway
1082 1 : if (ownMasterKey.verified) {
1083 2 : return (ownMasterKey.ed25519Key!, otherMasterKey.ed25519Key!);
1084 : }
1085 1 : } else if (mode == QRMode.verifySelfTrusted &&
1086 : ownMasterKey != null &&
1087 : otherDeviceKey != null) {
1088 1 : if (ownMasterKey.verified) {
1089 2 : return (ownMasterKey.ed25519Key!, otherDeviceKey.ed25519Key!);
1090 : }
1091 1 : } else if (mode == QRMode.verifySelfUntrusted &&
1092 : ownMasterKey != null &&
1093 : ownDeviceKey != null) {
1094 2 : return (ownDeviceKey.ed25519Key!, ownMasterKey.ed25519Key!);
1095 : }
1096 : return null;
1097 : }
1098 :
1099 2 : Future<QRCode?> generateQrCode() async {
1100 2 : final data = Uint8Buffer();
1101 : // why 11? https://github.com/matrix-org/matrix-js-sdk/commit/275ea6aacbfc6623e7559a7649ca5cab207903d9
1102 2 : randomSharedSecretForQRCode =
1103 4 : encodeBase64Unpadded(uc.secureRandomBytes(11));
1104 :
1105 2 : final mode = getOurQRMode();
1106 4 : data.addAll(ascii.encode(prefix));
1107 2 : data.add(version);
1108 4 : data.add(mode.code);
1109 4 : final encodedTxnId = utf8.encode(transactionId!);
1110 2 : final txnIdLen = encodedTxnId.length;
1111 2 : final tmpBuf = Uint8List(2);
1112 6 : ByteData.view(tmpBuf.buffer).setUint16(0, txnIdLen);
1113 2 : data.addAll(tmpBuf);
1114 2 : data.addAll(encodedTxnId);
1115 2 : final keys = await getKeys(mode);
1116 : if (keys != null) {
1117 4 : data.addAll(base64decodeUnpadded(keys.$1));
1118 4 : data.addAll(base64decodeUnpadded(keys.$2));
1119 : } else {
1120 : return null;
1121 : }
1122 :
1123 6 : data.addAll(base64decodeUnpadded(randomSharedSecretForQRCode!));
1124 4 : return QRCode(randomSharedSecretForQRCode!, data);
1125 : }
1126 : }
1127 :
1128 : abstract class _KeyVerificationMethod {
1129 : KeyVerification request;
1130 3 : Encryption get encryption => request.encryption;
1131 6 : Client get client => request.client;
1132 2 : _KeyVerificationMethod({required this.request});
1133 :
1134 : Future<void> handlePayload(String type, Map<String, dynamic> payload);
1135 0 : bool validateStart(Map<String, dynamic> payload) {
1136 : return false;
1137 : }
1138 :
1139 : late String _type;
1140 4 : String get type => _type;
1141 :
1142 : Future<void> sendStart();
1143 0 : void dispose() {}
1144 : }
1145 :
1146 : class _KeyVerificationMethodQRReciprocate extends _KeyVerificationMethod {
1147 2 : _KeyVerificationMethodQRReciprocate({required super.request});
1148 :
1149 : @override
1150 : // ignore: overridden_fields
1151 : final _type = EventTypes.Reciprocate;
1152 :
1153 2 : @override
1154 : bool validateStart(Map<String, dynamic> payload) {
1155 6 : if (payload['method'] != type) return false;
1156 8 : if (payload['secret'] != request.randomSharedSecretForQRCode) return false;
1157 : return true;
1158 : }
1159 :
1160 2 : @override
1161 : Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
1162 : try {
1163 : switch (type) {
1164 2 : case EventTypes.KeyVerificationStart:
1165 6 : if (!(await request.verifyLastStep([
1166 : EventTypes.KeyVerificationReady,
1167 : EventTypes.KeyVerificationRequest,
1168 : ]))) {
1169 : return; // abort
1170 : }
1171 2 : if (!validateStart(payload)) {
1172 2 : await request.cancel('m.invalid_message');
1173 : return;
1174 : }
1175 4 : request.setState(KeyVerificationState.confirmQRScan);
1176 : break;
1177 : }
1178 : } catch (e, s) {
1179 0 : Logs().e('[Key Verification Reciprocate] An error occured', e, s);
1180 0 : if (request.deviceId != null) {
1181 0 : await request.cancel('m.invalid_message');
1182 : }
1183 : }
1184 : }
1185 :
1186 2 : Future<void> acceptQRScanConfirmation() async {
1187 : // secret validation already done in validateStart
1188 :
1189 4 : final ourQRMode = request.getOurQRMode();
1190 : SignableKey? keyToVerify;
1191 :
1192 2 : if (ourQRMode == QRMode.verifyOtherUser) {
1193 6 : keyToVerify = client.userDeviceKeys[request.userId]?.masterKey;
1194 1 : } else if (ourQRMode == QRMode.verifySelfTrusted) {
1195 : keyToVerify =
1196 9 : client.userDeviceKeys[client.userID]?.deviceKeys[request.deviceId];
1197 1 : } else if (ourQRMode == QRMode.verifySelfUntrusted) {
1198 6 : keyToVerify = client.userDeviceKeys[client.userID]?.masterKey;
1199 : }
1200 : if (keyToVerify != null) {
1201 4 : await request.verifyKeysQR(keyToVerify, shower: true);
1202 : } else {
1203 0 : Logs().e('[KeyVerification], verifying keys failed');
1204 0 : await request.cancel('m.invalid_key');
1205 : }
1206 : }
1207 :
1208 2 : @override
1209 : Future<void> sendStart() async {
1210 2 : final payload = <String, dynamic>{
1211 2 : 'method': type,
1212 4 : 'secret': request.randomSharedSecretForQRCode,
1213 : };
1214 4 : request.makePayload(payload);
1215 4 : await request.send(EventTypes.KeyVerificationStart, payload);
1216 : }
1217 :
1218 2 : @override
1219 : void dispose() {}
1220 : }
1221 :
1222 : enum QRMode {
1223 : verifyOtherUser(0x00),
1224 : verifySelfTrusted(0x01),
1225 : verifySelfUntrusted(0x02);
1226 :
1227 : const QRMode(this.code);
1228 : final int code;
1229 : }
1230 :
1231 : class QRCode {
1232 : /// You actually never need this when implementing in a client, its just to
1233 : /// make tests easier. Just pass `qrDataRawBytes` in `continueVerifcation()`
1234 : final String randomSharedSecret;
1235 : final Uint8Buffer qrDataRawBytes;
1236 2 : QRCode(this.randomSharedSecret, this.qrDataRawBytes);
1237 : }
1238 :
1239 : const knownKeyAgreementProtocols = ['curve25519-hkdf-sha256', 'curve25519'];
1240 : const knownHashes = ['sha256'];
1241 : const knownHashesAuthentificationCodes = ['hkdf-hmac-sha256'];
1242 :
1243 : class _KeyVerificationMethodSas extends _KeyVerificationMethod {
1244 2 : _KeyVerificationMethodSas({required super.request});
1245 :
1246 : @override
1247 : // ignore: overridden_fields
1248 : final _type = EventTypes.Sas;
1249 :
1250 : String? keyAgreementProtocol;
1251 : String? hash;
1252 : String? messageAuthenticationCode;
1253 : List<String>? authenticationTypes;
1254 : late String startCanonicalJson;
1255 : String? commitment;
1256 : late String theirPublicKey;
1257 : Map<String, dynamic>? macPayload;
1258 : olm.SAS? sas;
1259 :
1260 2 : @override
1261 : void dispose() {
1262 3 : sas?.free();
1263 : }
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 1 : if (!_validateCommitment()) {
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 2 : messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
1400 1 : final possibleAuthenticationTypes = _intersect(
1401 1 : knownAuthentificationTypes,
1402 1 : payload['short_authentication_string'],
1403 : );
1404 1 : if (possibleAuthenticationTypes.isEmpty) {
1405 : return false;
1406 : }
1407 1 : authenticationTypes = possibleAuthenticationTypes;
1408 3 : startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload));
1409 : return true;
1410 : }
1411 :
1412 1 : Future<void> _sendAccept() async {
1413 2 : final sas = this.sas = olm.SAS();
1414 4 : commitment = _makeCommitment(sas.get_pubkey(), startCanonicalJson);
1415 3 : await request.send(EventTypes.KeyVerificationAccept, {
1416 1 : 'method': type,
1417 1 : 'key_agreement_protocol': keyAgreementProtocol,
1418 1 : 'hash': hash,
1419 1 : 'message_authentication_code': messageAuthenticationCode,
1420 1 : 'short_authentication_string': authenticationTypes,
1421 1 : 'commitment': commitment,
1422 : });
1423 : }
1424 :
1425 1 : bool _handleAccept(Map<String, dynamic> payload) {
1426 : if (!knownKeyAgreementProtocols
1427 2 : .contains(payload['key_agreement_protocol'])) {
1428 : return false;
1429 : }
1430 2 : keyAgreementProtocol = payload['key_agreement_protocol'];
1431 2 : if (!knownHashes.contains(payload['hash'])) {
1432 : return false;
1433 : }
1434 2 : hash = payload['hash'];
1435 : if (!knownHashesAuthentificationCodes
1436 2 : .contains(payload['message_authentication_code'])) {
1437 : return false;
1438 : }
1439 2 : messageAuthenticationCode = payload['message_authentication_code'];
1440 1 : final possibleAuthenticationTypes = _intersect(
1441 1 : knownAuthentificationTypes,
1442 1 : payload['short_authentication_string'],
1443 : );
1444 1 : if (possibleAuthenticationTypes.isEmpty) {
1445 : return false;
1446 : }
1447 1 : authenticationTypes = possibleAuthenticationTypes;
1448 2 : commitment = payload['commitment'];
1449 2 : sas = olm.SAS();
1450 : return true;
1451 : }
1452 :
1453 1 : Future<void> _sendKey() async {
1454 3 : await request.send('m.key.verification.key', {
1455 2 : 'key': sas!.get_pubkey(),
1456 : });
1457 : }
1458 :
1459 1 : void _handleKey(Map<String, dynamic> payload) {
1460 2 : theirPublicKey = payload['key'];
1461 3 : sas!.set_their_key(payload['key']);
1462 : }
1463 :
1464 1 : bool _validateCommitment() {
1465 3 : final checkCommitment = _makeCommitment(theirPublicKey, startCanonicalJson);
1466 2 : return commitment == checkCommitment;
1467 : }
1468 :
1469 1 : Uint8List makeSas(int bytes) {
1470 : var sasInfo = '';
1471 2 : if (keyAgreementProtocol == 'curve25519-hkdf-sha256') {
1472 : final ourInfo =
1473 7 : '${client.userID}|${client.deviceID}|${sas!.get_pubkey()}|';
1474 : final theirInfo =
1475 6 : '${request.userId}|${request.deviceId}|$theirPublicKey|';
1476 : sasInfo =
1477 7 : 'MATRIX_KEY_VERIFICATION_SAS|${request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo}${request.transactionId!}';
1478 0 : } else if (keyAgreementProtocol == 'curve25519') {
1479 0 : final ourInfo = client.userID! + client.deviceID!;
1480 0 : final theirInfo = request.userId + request.deviceId!;
1481 : sasInfo =
1482 0 : 'MATRIX_KEY_VERIFICATION_SAS${request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo}${request.transactionId!}';
1483 : } else {
1484 0 : throw Exception('Unknown key agreement protocol');
1485 : }
1486 2 : return sas!.generate_bytes(sasInfo, bytes);
1487 : }
1488 :
1489 1 : Future<void> _sendMac() async {
1490 : final baseInfo =
1491 11 : 'MATRIX_KEY_VERIFICATION_MAC${client.userID!}${client.deviceID!}${request.userId}${request.deviceId!}${request.transactionId!}';
1492 1 : final mac = <String, String>{};
1493 1 : final keyList = <String>[];
1494 :
1495 : // now add all the keys we want the other to verify
1496 : // for now it is just our device key, once we have cross-signing
1497 : // we would also add the cross signing key here
1498 3 : final deviceKeyId = 'ed25519:${client.deviceID}';
1499 1 : mac[deviceKeyId] =
1500 4 : _calculateMac(encryption.fingerprintKey!, baseInfo + deviceKeyId);
1501 1 : keyList.add(deviceKeyId);
1502 :
1503 6 : final masterKey = client.userDeviceKeys[client.userID]?.masterKey;
1504 1 : if (masterKey != null && masterKey.verified) {
1505 : // we have our own master key verified, let's send it!
1506 2 : final masterKeyId = 'ed25519:${masterKey.publicKey}';
1507 1 : mac[masterKeyId] =
1508 3 : _calculateMac(masterKey.publicKey!, baseInfo + masterKeyId);
1509 1 : keyList.add(masterKeyId);
1510 : }
1511 :
1512 1 : keyList.sort();
1513 3 : final keys = _calculateMac(keyList.join(','), '${baseInfo}KEY_IDS');
1514 3 : await request.send('m.key.verification.mac', {
1515 : 'mac': mac,
1516 : 'keys': keys,
1517 : });
1518 : }
1519 :
1520 1 : Future<void> _processMac() async {
1521 1 : final payload = macPayload!;
1522 : final baseInfo =
1523 11 : 'MATRIX_KEY_VERIFICATION_MAC${request.userId}${request.deviceId!}${client.userID!}${client.deviceID!}${request.transactionId!}';
1524 :
1525 3 : final keyList = payload['mac'].keys.toList();
1526 1 : keyList.sort();
1527 2 : if (payload['keys'] !=
1528 3 : _calculateMac(keyList.join(','), '${baseInfo}KEY_IDS')) {
1529 0 : await request.cancel('m.key_mismatch');
1530 : return;
1531 : }
1532 :
1533 5 : if (!client.userDeviceKeys.containsKey(request.userId)) {
1534 0 : await request.cancel('m.key_mismatch');
1535 : return;
1536 : }
1537 1 : final mac = <String, String>{};
1538 3 : for (final entry in payload['mac'].entries) {
1539 2 : if (entry.value is String) {
1540 3 : mac[entry.key] = entry.value;
1541 : }
1542 : }
1543 3 : await request.verifyKeysSAS(mac, (String mac, SignableKey key) async {
1544 1 : return mac ==
1545 1 : _calculateMac(
1546 1 : key.ed25519Key!,
1547 2 : '${baseInfo}ed25519:${key.identifier!}',
1548 : );
1549 : });
1550 : }
1551 :
1552 1 : String _makeCommitment(String pubKey, String canonicalJson) {
1553 2 : if (hash == 'sha256') {
1554 1 : final olmutil = olm.Utility();
1555 2 : final ret = olmutil.sha256(pubKey + canonicalJson);
1556 1 : olmutil.free();
1557 : return ret;
1558 : }
1559 0 : throw Exception('Unknown hash method');
1560 : }
1561 :
1562 1 : String _calculateMac(String input, String info) {
1563 2 : if (messageAuthenticationCode == 'hkdf-hmac-sha256') {
1564 2 : return sas!.calculate_mac(input, info);
1565 : } else {
1566 0 : throw Exception('Unknown message authentification code');
1567 : }
1568 : }
1569 : }
1570 :
1571 : const _emojiMap = [
1572 : {
1573 : 'emoji': '\u{1F436}',
1574 : 'name': 'Dog',
1575 : },
1576 : {
1577 : 'emoji': '\u{1F431}',
1578 : 'name': 'Cat',
1579 : },
1580 : {
1581 : 'emoji': '\u{1F981}',
1582 : 'name': 'Lion',
1583 : },
1584 : {
1585 : 'emoji': '\u{1F40E}',
1586 : 'name': 'Horse',
1587 : },
1588 : {
1589 : 'emoji': '\u{1F984}',
1590 : 'name': 'Unicorn',
1591 : },
1592 : {
1593 : 'emoji': '\u{1F437}',
1594 : 'name': 'Pig',
1595 : },
1596 : {
1597 : 'emoji': '\u{1F418}',
1598 : 'name': 'Elephant',
1599 : },
1600 : {
1601 : 'emoji': '\u{1F430}',
1602 : 'name': 'Rabbit',
1603 : },
1604 : {
1605 : 'emoji': '\u{1F43C}',
1606 : 'name': 'Panda',
1607 : },
1608 : {
1609 : 'emoji': '\u{1F413}',
1610 : 'name': 'Rooster',
1611 : },
1612 : {
1613 : 'emoji': '\u{1F427}',
1614 : 'name': 'Penguin',
1615 : },
1616 : {
1617 : 'emoji': '\u{1F422}',
1618 : 'name': 'Turtle',
1619 : },
1620 : {
1621 : 'emoji': '\u{1F41F}',
1622 : 'name': 'Fish',
1623 : },
1624 : {
1625 : 'emoji': '\u{1F419}',
1626 : 'name': 'Octopus',
1627 : },
1628 : {
1629 : 'emoji': '\u{1F98B}',
1630 : 'name': 'Butterfly',
1631 : },
1632 : {
1633 : 'emoji': '\u{1F337}',
1634 : 'name': 'Flower',
1635 : },
1636 : {
1637 : 'emoji': '\u{1F333}',
1638 : 'name': 'Tree',
1639 : },
1640 : {
1641 : 'emoji': '\u{1F335}',
1642 : 'name': 'Cactus',
1643 : },
1644 : {
1645 : 'emoji': '\u{1F344}',
1646 : 'name': 'Mushroom',
1647 : },
1648 : {
1649 : 'emoji': '\u{1F30F}',
1650 : 'name': 'Globe',
1651 : },
1652 : {
1653 : 'emoji': '\u{1F319}',
1654 : 'name': 'Moon',
1655 : },
1656 : {
1657 : 'emoji': '\u{2601}\u{FE0F}',
1658 : 'name': 'Cloud',
1659 : },
1660 : {
1661 : 'emoji': '\u{1F525}',
1662 : 'name': 'Fire',
1663 : },
1664 : {
1665 : 'emoji': '\u{1F34C}',
1666 : 'name': 'Banana',
1667 : },
1668 : {
1669 : 'emoji': '\u{1F34E}',
1670 : 'name': 'Apple',
1671 : },
1672 : {
1673 : 'emoji': '\u{1F353}',
1674 : 'name': 'Strawberry',
1675 : },
1676 : {
1677 : 'emoji': '\u{1F33D}',
1678 : 'name': 'Corn',
1679 : },
1680 : {
1681 : 'emoji': '\u{1F355}',
1682 : 'name': 'Pizza',
1683 : },
1684 : {
1685 : 'emoji': '\u{1F382}',
1686 : 'name': 'Cake',
1687 : },
1688 : {
1689 : 'emoji': '\u{2764}\u{FE0F}',
1690 : 'name': 'Heart',
1691 : },
1692 : {
1693 : 'emoji': '\u{1F600}',
1694 : 'name': 'Smiley',
1695 : },
1696 : {
1697 : 'emoji': '\u{1F916}',
1698 : 'name': 'Robot',
1699 : },
1700 : {
1701 : 'emoji': '\u{1F3A9}',
1702 : 'name': 'Hat',
1703 : },
1704 : {
1705 : 'emoji': '\u{1F453}',
1706 : 'name': 'Glasses',
1707 : },
1708 : {
1709 : 'emoji': '\u{1F527}',
1710 : 'name': 'Spanner',
1711 : },
1712 : {
1713 : 'emoji': '\u{1F385}',
1714 : 'name': 'Santa',
1715 : },
1716 : {
1717 : 'emoji': '\u{1F44D}',
1718 : 'name': 'Thumbs Up',
1719 : },
1720 : {
1721 : 'emoji': '\u{2602}\u{FE0F}',
1722 : 'name': 'Umbrella',
1723 : },
1724 : {
1725 : 'emoji': '\u{231B}',
1726 : 'name': 'Hourglass',
1727 : },
1728 : {
1729 : 'emoji': '\u{23F0}',
1730 : 'name': 'Clock',
1731 : },
1732 : {
1733 : 'emoji': '\u{1F381}',
1734 : 'name': 'Gift',
1735 : },
1736 : {
1737 : 'emoji': '\u{1F4A1}',
1738 : 'name': 'Light Bulb',
1739 : },
1740 : {
1741 : 'emoji': '\u{1F4D5}',
1742 : 'name': 'Book',
1743 : },
1744 : {
1745 : 'emoji': '\u{270F}\u{FE0F}',
1746 : 'name': 'Pencil',
1747 : },
1748 : {
1749 : 'emoji': '\u{1F4CE}',
1750 : 'name': 'Paperclip',
1751 : },
1752 : {
1753 : 'emoji': '\u{2702}\u{FE0F}',
1754 : 'name': 'Scissors',
1755 : },
1756 : {
1757 : 'emoji': '\u{1F512}',
1758 : 'name': 'Lock',
1759 : },
1760 : {
1761 : 'emoji': '\u{1F511}',
1762 : 'name': 'Key',
1763 : },
1764 : {
1765 : 'emoji': '\u{1F528}',
1766 : 'name': 'Hammer',
1767 : },
1768 : {
1769 : 'emoji': '\u{260E}\u{FE0F}',
1770 : 'name': 'Telephone',
1771 : },
1772 : {
1773 : 'emoji': '\u{1F3C1}',
1774 : 'name': 'Flag',
1775 : },
1776 : {
1777 : 'emoji': '\u{1F682}',
1778 : 'name': 'Train',
1779 : },
1780 : {
1781 : 'emoji': '\u{1F6B2}',
1782 : 'name': 'Bicycle',
1783 : },
1784 : {
1785 : 'emoji': '\u{2708}\u{FE0F}',
1786 : 'name': 'Aeroplane',
1787 : },
1788 : {
1789 : 'emoji': '\u{1F680}',
1790 : 'name': 'Rocket',
1791 : },
1792 : {
1793 : 'emoji': '\u{1F3C6}',
1794 : 'name': 'Trophy',
1795 : },
1796 : {
1797 : 'emoji': '\u{26BD}',
1798 : 'name': 'Ball',
1799 : },
1800 : {
1801 : 'emoji': '\u{1F3B8}',
1802 : 'name': 'Guitar',
1803 : },
1804 : {
1805 : 'emoji': '\u{1F3BA}',
1806 : 'name': 'Trumpet',
1807 : },
1808 : {
1809 : 'emoji': '\u{1F514}',
1810 : 'name': 'Bell',
1811 : },
1812 : {
1813 : 'emoji': '\u{2693}',
1814 : 'name': 'Anchor',
1815 : },
1816 : {
1817 : 'emoji': '\u{1F3A7}',
1818 : 'name': 'Headphones',
1819 : },
1820 : {
1821 : 'emoji': '\u{1F4C1}',
1822 : 'name': 'Folder',
1823 : },
1824 : {
1825 : 'emoji': '\u{1F4CC}',
1826 : 'name': 'Pin',
1827 : },
1828 : ];
1829 :
1830 : class KeyVerificationEmoji {
1831 : final int number;
1832 1 : KeyVerificationEmoji(this.number);
1833 :
1834 4 : String get emoji => _emojiMap[number]['emoji'] ?? '';
1835 4 : String get name => _emojiMap[number]['name'] ?? '';
1836 : }
|