@@ -18,12 +18,14 @@ import fetchMock from "fetch-mock-jest";
18
18
import { MockResponse } from "fetch-mock" ;
19
19
20
20
import { createClient , MatrixClient } from "../../../src" ;
21
- import { ShowSasCallbacks , VerifierEvent } from "../../../src/crypto-api/verification" ;
21
+ import { ShowQrCodeCallbacks , ShowSasCallbacks , VerifierEvent } from "../../../src/crypto-api/verification" ;
22
22
import { escapeRegExp } from "../../../src/utils" ;
23
23
import { VerificationBase } from "../../../src/crypto/verification/Base" ;
24
24
import { CRYPTO_BACKENDS , InitCrypto } from "../../test-utils/test-utils" ;
25
25
import { SyncResponder } from "../../test-utils/SyncResponder" ;
26
26
import {
27
+ MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 ,
28
+ SIGNED_CROSS_SIGNING_KEYS_DATA ,
27
29
SIGNED_TEST_DEVICE_DATA ,
28
30
TEST_DEVICE_ID ,
29
31
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 ,
@@ -40,6 +42,34 @@ import {
40
42
// to ensure that we don't end up with dangling timeouts.
41
43
jest . useFakeTimers ( ) ;
42
44
45
+ let previousCrypto : Crypto | undefined ;
46
+
47
+ beforeAll ( ( ) => {
48
+ // Stub out global.crypto
49
+ previousCrypto = global [ "crypto" ] ;
50
+
51
+ Object . defineProperty ( global , "crypto" , {
52
+ value : {
53
+ getRandomValues : function < T extends Uint8Array > ( array : T ) : T {
54
+ array . fill ( 0x12 ) ;
55
+ return array ;
56
+ } ,
57
+ } ,
58
+ } ) ;
59
+ } ) ;
60
+
61
+ // restore the original global.crypto
62
+ afterAll ( ( ) => {
63
+ if ( previousCrypto === undefined ) {
64
+ // @ts -ignore deleting a non-optional property. It *is* optional really.
65
+ delete global . crypto ;
66
+ } else {
67
+ Object . defineProperty ( global , "crypto" , {
68
+ value : previousCrypto ,
69
+ } ) ;
70
+ }
71
+ } ) ;
72
+
43
73
/**
44
74
* Integration tests for verification functionality.
45
75
*
@@ -208,6 +238,107 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
208
238
olmSAS . free ( ) ;
209
239
} ) ;
210
240
241
+ oldBackendOnly (
242
+ "Outgoing verification: can verify another device via QR code with an untrusted cross-signing key" ,
243
+ async ( ) => {
244
+ // expect requests to download our own keys
245
+ fetchMock . post ( new RegExp ( "/_matrix/client/(r0|v3)/keys/query" ) , {
246
+ device_keys : {
247
+ [ TEST_USER_ID ] : {
248
+ [ TEST_DEVICE_ID ] : SIGNED_TEST_DEVICE_DATA ,
249
+ } ,
250
+ } ,
251
+ ...SIGNED_CROSS_SIGNING_KEYS_DATA ,
252
+ } ) ;
253
+
254
+ // QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
255
+ //
256
+ // Completing the initial sync will make the device list download outdated device lists (of which our own
257
+ // user will be one).
258
+ syncResponder . sendOrQueueSyncResponse ( { } ) ;
259
+ // DeviceList has a sleep(5) which we need to make happen
260
+ await jest . advanceTimersByTimeAsync ( 10 ) ;
261
+ expect ( aliceClient . getStoredCrossSigningForUser ( TEST_USER_ID ) ) . toBeTruthy ( ) ;
262
+
263
+ // have alice initiate a verification. She should send a m.key.verification.request
264
+ const [ requestBody , request ] = await Promise . all ( [
265
+ expectSendToDeviceMessage ( "m.key.verification.request" ) ,
266
+ aliceClient . requestVerification ( TEST_USER_ID , [ TEST_DEVICE_ID ] ) ,
267
+ ] ) ;
268
+ const transactionId = request . channel . transactionId ;
269
+
270
+ const toDeviceMessage = requestBody . messages [ TEST_USER_ID ] [ TEST_DEVICE_ID ] ;
271
+ expect ( toDeviceMessage . methods ) . toContain ( "m.qr_code.show.v1" ) ;
272
+ expect ( toDeviceMessage . methods ) . toContain ( "m.qr_code.scan.v1" ) ;
273
+ expect ( toDeviceMessage . methods ) . toContain ( "m.reciprocate.v1" ) ;
274
+ expect ( toDeviceMessage . from_device ) . toEqual ( aliceClient . deviceId ) ;
275
+ expect ( toDeviceMessage . transaction_id ) . toEqual ( transactionId ) ;
276
+
277
+ // The dummy device replies with an m.key.verification.ready, with an indication we can scan the QR code
278
+ returnToDeviceMessageFromSync ( {
279
+ type : "m.key.verification.ready" ,
280
+ content : {
281
+ from_device : TEST_DEVICE_ID ,
282
+ methods : [ "m.qr_code.scan.v1" ] ,
283
+ transaction_id : transactionId ,
284
+ } ,
285
+ } ) ;
286
+ await waitForVerificationRequestChanged ( request ) ;
287
+ expect ( request . phase ) . toEqual ( Phase . Ready ) ;
288
+
289
+ // we should now have QR data we can display
290
+ const qrCodeData = request . qrCodeData ! ;
291
+ expect ( qrCodeData ) . toBeTruthy ( ) ;
292
+ const qrCodeBuffer = qrCodeData . getBuffer ( ) ;
293
+ // https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
294
+ expect ( qrCodeBuffer . subarray ( 0 , 6 ) . toString ( "latin1" ) ) . toEqual ( "MATRIX" ) ;
295
+ expect ( qrCodeBuffer . readUint8 ( 6 ) ) . toEqual ( 0x02 ) ; // version
296
+ expect ( qrCodeBuffer . readUint8 ( 7 ) ) . toEqual ( 0x02 ) ; // mode
297
+ const txnIdLen = qrCodeBuffer . readUint16BE ( 8 ) ;
298
+ expect ( qrCodeBuffer . subarray ( 10 , 10 + txnIdLen ) . toString ( "utf-8" ) ) . toEqual ( transactionId ) ;
299
+ // Alice's device's public key comes next, but we have nothing to do with it here.
300
+ // const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen);
301
+ expect ( qrCodeBuffer . subarray ( 42 + txnIdLen , 32 + 42 + txnIdLen ) ) . toEqual (
302
+ Buffer . from ( MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 , "base64" ) ,
303
+ ) ;
304
+ const sharedSecret = qrCodeBuffer . subarray ( 74 + txnIdLen ) ;
305
+
306
+ // the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
307
+ returnToDeviceMessageFromSync ( {
308
+ type : "m.key.verification.start" ,
309
+ content : {
310
+ from_device : TEST_DEVICE_ID ,
311
+ method : "m.reciprocate.v1" ,
312
+ transaction_id : transactionId ,
313
+ secret : encodeUnpaddedBase64 ( sharedSecret ) ,
314
+ } ,
315
+ } ) ;
316
+ await waitForVerificationRequestChanged ( request ) ;
317
+ expect ( request . phase ) . toEqual ( Phase . Started ) ;
318
+ expect ( request . chosenMethod ) . toEqual ( "m.reciprocate.v1" ) ;
319
+
320
+ // there should now be a verifier
321
+ const verifier : VerificationBase = request . verifier ! ;
322
+ expect ( verifier ) . toBeDefined ( ) ;
323
+
324
+ // ... which we call .verify on, which emits a ShowReciprocateQr event
325
+ const verificationPromise = verifier . verify ( ) ;
326
+ const reciprocateQRCodeCallbacks = await new Promise < ShowQrCodeCallbacks > ( ( resolve ) => {
327
+ verifier . once ( VerifierEvent . ShowReciprocateQr , resolve ) ;
328
+ } ) ;
329
+
330
+ // Alice confirms she is happy
331
+ reciprocateQRCodeCallbacks . confirm ( ) ;
332
+
333
+ // that should satisfy Alice, who should reply with a 'done'
334
+ await expectSendToDeviceMessage ( "m.key.verification.done" ) ;
335
+
336
+ // ... and the whole thing should be done!
337
+ await verificationPromise ;
338
+ expect ( request . phase ) . toEqual ( Phase . Done ) ;
339
+ } ,
340
+ ) ;
341
+
211
342
function returnToDeviceMessageFromSync ( ev : { type : string ; content : object ; sender ?: string } ) : void {
212
343
ev . sender ??= TEST_USER_ID ;
213
344
syncResponder . sendOrQueueSyncResponse ( { to_device : { events : [ ev ] } } ) ;
@@ -253,3 +384,7 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
253
384
//console.info(`Test MAC: input:'${input}, info: '${info}' -> '${mac}`);
254
385
return mac ;
255
386
}
387
+
388
+ function encodeUnpaddedBase64 ( uint8Array : ArrayBuffer | Uint8Array ) : string {
389
+ return Buffer . from ( uint8Array ) . toString ( "base64" ) . replace ( / = + $ / g, "" ) ;
390
+ }
0 commit comments