@@ -18,12 +18,14 @@ import fetchMock from "fetch-mock-jest";
1818import { MockResponse } from "fetch-mock" ;
1919
2020import { createClient , MatrixClient } from "../../../src" ;
21- import { ShowSasCallbacks , VerifierEvent } from "../../../src/crypto-api/verification" ;
21+ import { ShowQrCodeCallbacks , ShowSasCallbacks , VerifierEvent } from "../../../src/crypto-api/verification" ;
2222import { escapeRegExp } from "../../../src/utils" ;
2323import { VerificationBase } from "../../../src/crypto/verification/Base" ;
2424import { CRYPTO_BACKENDS , InitCrypto } from "../../test-utils/test-utils" ;
2525import { SyncResponder } from "../../test-utils/SyncResponder" ;
2626import {
27+ MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 ,
28+ SIGNED_CROSS_SIGNING_KEYS_DATA ,
2729 SIGNED_TEST_DEVICE_DATA ,
2830 TEST_DEVICE_ID ,
2931 TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 ,
@@ -40,6 +42,34 @@ import {
4042// to ensure that we don't end up with dangling timeouts.
4143jest . useFakeTimers ( ) ;
4244
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+
4373/**
4474 * Integration tests for verification functionality.
4575 *
@@ -208,6 +238,107 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
208238 olmSAS . free ( ) ;
209239 } ) ;
210240
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+
211342 function returnToDeviceMessageFromSync ( ev : { type : string ; content : object ; sender ?: string } ) : void {
212343 ev . sender ??= TEST_USER_ID ;
213344 syncResponder . sendOrQueueSyncResponse ( { to_device : { events : [ ev ] } } ) ;
@@ -253,3 +384,7 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
253384 //console.info(`Test MAC: input:'${input}, info: '${info}' -> '${mac}`);
254385 return mac ;
255386}
387+
388+ function encodeUnpaddedBase64 ( uint8Array : ArrayBuffer | Uint8Array ) : string {
389+ return Buffer . from ( uint8Array ) . toString ( "base64" ) . replace ( / = + $ / g, "" ) ;
390+ }
0 commit comments