@@ -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,17 @@ import {
4042// to ensure that we don't end up with dangling timeouts.
4143jest . useFakeTimers ( ) ;
4244
45+ // Stub out global.crypto
46+ //
47+ // This shouldn't leak into other files, because jest gives each file a new environment.
48+ global [ "crypto" ] = {
49+ // @ts -ignore this doesn't match the type in typescript, but that doesn't really matter
50+ getRandomValues : function < T extends Uint8Array > ( array : T ) : T {
51+ array . fill ( 0x12 ) ;
52+ return array ;
53+ } ,
54+ } ;
55+
4356/**
4457 * Integration tests for verification functionality.
4558 *
@@ -208,6 +221,110 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
208221 olmSAS . free ( ) ;
209222 } ) ;
210223
224+ oldBackendOnly (
225+ "Outgoing verification: can verify another device via QR code with an untrusted cross-signing key" ,
226+ async ( ) => {
227+ // we need to have bootstrapped cross-signing for this
228+ //await bootstrapCrossSigning(aliceClient);
229+ // console.warn("Bootstrapped");
230+
231+ // expect requests to download our own keys
232+ fetchMock . post ( new RegExp ( "/_matrix/client/(r0|v3)/keys/query" ) , {
233+ device_keys : {
234+ [ TEST_USER_ID ] : {
235+ [ TEST_DEVICE_ID ] : SIGNED_TEST_DEVICE_DATA ,
236+ } ,
237+ } ,
238+ ...SIGNED_CROSS_SIGNING_KEYS_DATA ,
239+ } ) ;
240+
241+ // QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
242+ //
243+ // Completing the initial sync will make the device list download outdated device lists (of which our own
244+ // user will be one).
245+ syncResponder . sendOrQueueSyncResponse ( { } ) ;
246+ // DeviceList has a sleep(5) which we need to make happen
247+ await jest . advanceTimersByTimeAsync ( 10 ) ;
248+ expect ( aliceClient . getStoredCrossSigningForUser ( TEST_USER_ID ) ) . toBeTruthy ( ) ;
249+
250+ // have alice initiate a verification. She should send a m.key.verification.request
251+ const [ requestBody , request ] = await Promise . all ( [
252+ expectSendToDeviceMessage ( "m.key.verification.request" ) ,
253+ aliceClient . requestVerification ( TEST_USER_ID , [ TEST_DEVICE_ID ] ) ,
254+ ] ) ;
255+ const transactionId = request . channel . transactionId ;
256+
257+ const toDeviceMessage = requestBody . messages [ TEST_USER_ID ] [ TEST_DEVICE_ID ] ;
258+ expect ( toDeviceMessage . methods ) . toContain ( "m.qr_code.show.v1" ) ;
259+ expect ( toDeviceMessage . methods ) . toContain ( "m.qr_code.scan.v1" ) ;
260+ expect ( toDeviceMessage . methods ) . toContain ( "m.reciprocate.v1" ) ;
261+ expect ( toDeviceMessage . from_device ) . toEqual ( aliceClient . deviceId ) ;
262+ expect ( toDeviceMessage . transaction_id ) . toEqual ( transactionId ) ;
263+
264+ // The dummy device replies with an m.key.verification.ready, with an indication we can scan the QR code
265+ returnToDeviceMessageFromSync ( {
266+ type : "m.key.verification.ready" ,
267+ content : {
268+ from_device : TEST_DEVICE_ID ,
269+ methods : [ "m.qr_code.scan.v1" ] ,
270+ transaction_id : transactionId ,
271+ } ,
272+ } ) ;
273+ await waitForVerificationRequestChanged ( request ) ;
274+ expect ( request . phase ) . toEqual ( Phase . Ready ) ;
275+
276+ // we should now have QR data we can display
277+ const qrCodeData = request . qrCodeData ! ;
278+ expect ( qrCodeData ) . toBeTruthy ( ) ;
279+ const qrCodeBuffer = qrCodeData . getBuffer ( ) ;
280+ // https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
281+ expect ( qrCodeBuffer . subarray ( 0 , 6 ) . toString ( "latin1" ) ) . toEqual ( "MATRIX" ) ;
282+ expect ( qrCodeBuffer . readUint8 ( 6 ) ) . toEqual ( 0x02 ) ; // version
283+ expect ( qrCodeBuffer . readUint8 ( 7 ) ) . toEqual ( 0x02 ) ; // mode
284+ const txnIdLen = qrCodeBuffer . readUint16BE ( 8 ) ;
285+ expect ( qrCodeBuffer . subarray ( 10 , 10 + txnIdLen ) . toString ( "utf-8" ) ) . toEqual ( transactionId ) ;
286+ // const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen);
287+ expect ( qrCodeBuffer . subarray ( 42 + txnIdLen , 32 + 42 + txnIdLen ) ) . toEqual (
288+ Buffer . from ( MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 , "base64" ) ,
289+ ) ;
290+ const sharedSecret = qrCodeBuffer . subarray ( 74 + txnIdLen ) ;
291+
292+ // the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
293+ returnToDeviceMessageFromSync ( {
294+ type : "m.key.verification.start" ,
295+ content : {
296+ from_device : TEST_DEVICE_ID ,
297+ method : "m.reciprocate.v1" ,
298+ transaction_id : transactionId ,
299+ secret : encodeUnpaddedBase64 ( sharedSecret ) ,
300+ } ,
301+ } ) ;
302+ await waitForVerificationRequestChanged ( request ) ;
303+ expect ( request . phase ) . toEqual ( Phase . Started ) ;
304+ expect ( request . chosenMethod ) . toEqual ( "m.reciprocate.v1" ) ;
305+
306+ // there should now be a verifier
307+ const verifier : VerificationBase = request . verifier ! ;
308+ expect ( verifier ) . toBeDefined ( ) ;
309+
310+ // ... which we call .verify on, which emits a ShowReciprocateQr event
311+ const verificationPromise = verifier . verify ( ) ;
312+ const reciprocateQRCodeCallbacks = await new Promise < ShowQrCodeCallbacks > ( ( resolve ) => {
313+ verifier . once ( VerifierEvent . ShowReciprocateQr , resolve ) ;
314+ } ) ;
315+
316+ // Alice confirms she is happy
317+ reciprocateQRCodeCallbacks . confirm ( ) ;
318+
319+ // that should satisfy Alice, who should reply with a 'done'
320+ await expectSendToDeviceMessage ( "m.key.verification.done" ) ;
321+
322+ // ... and the whole thing should be done!
323+ await verificationPromise ;
324+ expect ( request . phase ) . toEqual ( Phase . Done ) ;
325+ } ,
326+ ) ;
327+
211328 function returnToDeviceMessageFromSync ( ev : { type : string ; content : object ; sender ?: string } ) : void {
212329 ev . sender ??= TEST_USER_ID ;
213330 syncResponder . sendOrQueueSyncResponse ( { to_device : { events : [ ev ] } } ) ;
@@ -253,3 +370,7 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
253370 //console.info(`Test MAC: input:'${input}, info: '${info}' -> '${mac}`);
254371 return mac ;
255372}
373+
374+ function encodeUnpaddedBase64 ( uint8Array : ArrayBuffer | Uint8Array ) : string {
375+ return Buffer . from ( uint8Array ) . toString ( "base64" ) . replace ( / = + $ / g, "" ) ;
376+ }
0 commit comments