diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index 5238871d2..1a8ce32f1 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -85,6 +85,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st // Rust backend. Once we have full support in the rust sdk, it will go away. const oldBackendOnly = backend === "rust-sdk" ? test.skip : test; + // newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy + // backend. + const newBackendOnly = backend === "rust-sdk" ? test : test.skip; + /** the client under test */ let aliceClient: MatrixClient; @@ -469,6 +473,64 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st await verificationPromise; expect(request.phase).toEqual(VerificationPhase.Done); }); + + newBackendOnly("can verify another by scanning their QR code", async () => { + aliceClient = await startTestClient(); + // we need cross-signing keys for a QR code verification + e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA); + await waitForDeviceList(); + + // Alice sends a m.key.verification.request + const [, request] = await Promise.all([ + expectSendToDeviceMessage("m.key.verification.request"), + aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID), + ]); + const transactionId = request.transactionId!; + + // The dummy device replies with an m.key.verification.ready, indicating it can show a QR code + returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.qr_code.show.v1", "m.reciprocate.v1"])); + await waitForVerificationRequestChanged(request); + expect(request.phase).toEqual(VerificationPhase.Ready); + expect(request.otherPartySupportsMethod("m.qr_code.show.v1")).toBe(true); + + // the dummy device shows a QR code + const sharedSecret = "SUPERSEKRET"; + const qrCodeBuffer = buildQRCode( + transactionId, + TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64, + MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, + sharedSecret, + ); + + // Alice scans the QR code + const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start"); + const verifier = await request.scanQRCode(qrCodeBuffer); + + const requestBody = await sendToDevicePromise; + const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID]; + expect(toDeviceMessage).toEqual({ + from_device: aliceClient.deviceId, + method: "m.reciprocate.v1", + transaction_id: transactionId, + secret: encodeUnpaddedBase64(Buffer.from(sharedSecret)), + }); + + expect(request.phase).toEqual(VerificationPhase.Started); + expect(request.chosenMethod).toEqual("m.reciprocate.v1"); + expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull(); + + const verificationPromise = verifier.verify(); + + // the dummy device confirms that Alice scanned the QR code, by replying with a done + returnToDeviceMessageFromSync(buildDoneMessage(transactionId)); + + // Alice also replies with a 'done' + await expectSendToDeviceMessage("m.key.verification.done"); + + // ... and the whole thing should be done! + await verificationPromise; + expect(request.phase).toEqual(VerificationPhase.Done); + }); }); describe("cancellation", () => { @@ -794,3 +856,22 @@ function buildDoneMessage(transactionId: string) { }, }; } + +function buildQRCode(transactionId: string, key1Base64: string, key2Base64: string, sharedSecret: string): Uint8Array { + // https://spec.matrix.org/v1.7/client-server-api/#qr-code-format + + const qrCodeBuffer = Buffer.alloc(150); // oversize + let idx = 0; + idx += qrCodeBuffer.write("MATRIX", idx, "ascii"); + idx = qrCodeBuffer.writeUInt8(0x02, idx); // version + idx = qrCodeBuffer.writeUInt8(0x02, idx); // mode + idx = qrCodeBuffer.writeInt16BE(transactionId.length, idx); + idx += qrCodeBuffer.write(transactionId, idx, "ascii"); + + idx += Buffer.from(key1Base64, "base64").copy(qrCodeBuffer, idx); + idx += Buffer.from(key2Base64, "base64").copy(qrCodeBuffer, idx); + idx += qrCodeBuffer.write(sharedSecret, idx); + + // truncate to the right length + return qrCodeBuffer.subarray(0, idx); +} diff --git a/spec/unit/rust-crypto/verification.spec.ts b/spec/unit/rust-crypto/verification.spec.ts index d74e8de1a..a2f671fdb 100644 --- a/spec/unit/rust-crypto/verification.spec.ts +++ b/spec/unit/rust-crypto/verification.spec.ts @@ -87,7 +87,7 @@ function makeTestRequest( ): RustVerificationRequest { inner ??= makeMockedInner(); outgoingRequestProcessor ??= {} as OutgoingRequestProcessor; - return new RustVerificationRequest(inner, outgoingRequestProcessor, undefined); + return new RustVerificationRequest(inner, outgoingRequestProcessor, []); } /** Mock up a rust-side VerificationRequest */ diff --git a/src/client.ts b/src/client.ts index a5343258e..e84a19d0e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2241,7 +2241,7 @@ export class MatrixClient extends TypedEventEmitter; + /** + * Start a QR code verification by providing a scanned QR code for this verification flow. + * + * Validates the QR code, and if it is ok, sends an `m.key.verification.start` event with `method` set to + * `m.reciprocate.v1`, to tell the other side the scan was successful. + * + * See also {@link VerificationRequest#startVerification} which can be used to start other verification methods. + * + * @param qrCodeData - the decoded QR code. + * @returns A verifier; call `.verify()` on it to wait for the other side to complete the verification flow. + */ + scanQRCode(qrCodeData: Uint8Array): Promise; + /** * The verifier which is doing the actual verification, once the method has been established. * Only defined when the `phase` is Started. diff --git a/src/crypto/verification/request/VerificationRequest.ts b/src/crypto/verification/request/VerificationRequest.ts index 7bcb948b4..ffcefc4db 100644 --- a/src/crypto/verification/request/VerificationRequest.ts +++ b/src/crypto/verification/request/VerificationRequest.ts @@ -478,6 +478,10 @@ export class VerificationRequest { + throw new Error("QR code scanning not supported by legacy crypto"); + } + /** * sends the initial .request event. * @returns resolves when the event has been sent. diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 0da71c619..5ca8b7f65 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -56,6 +56,8 @@ import { EventType } from "../@types/event"; import { CryptoEvent } from "../crypto"; import { TypedEventEmitter } from "../models/typed-event-emitter"; +const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"]; + /** * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. * @@ -560,7 +562,7 @@ export class RustCrypto extends TypedEventEmitter => { // if we now have a `Verification` where we lacked one before, wrap it. - // TODO: QR support if (this._verifier === undefined) { const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification(); if (verification instanceof RustSdkCryptoJs.Sas) { - this._verifier = new RustSASVerifier(verification, this, outgoingRequestProcessor); + this.setVerifier(new RustSASVerifier(verification, this, outgoingRequestProcessor)); + } else if (verification instanceof RustSdkCryptoJs.Qr) { + this.setVerifier(new RustQrCodeVerifier(verification, outgoingRequestProcessor)); } } @@ -77,6 +78,10 @@ export class RustVerificationRequest inner.registerChangesCallback(onChange); } + private setVerifier(verifier: RustSASVerifier | RustQrCodeVerifier): void { + this._verifier = verifier; + } + /** * Unique ID for this verification request. * @@ -188,6 +193,8 @@ export class RustVerificationRequest // TODO: this isn't quite right. The existence of a Verification doesn't prove that we have .started. if (verification instanceof RustSdkCryptoJs.Sas) { return "m.sas.v1"; + } else if (verification instanceof RustSdkCryptoJs.Qr) { + return "m.reciprocate.v1"; } else { return null; } @@ -225,12 +232,9 @@ export class RustVerificationRequest this._accepting = true; try { - const req: undefined | OutgoingRequest = - this.supportedVerificationMethods === undefined - ? this.inner.accept() - : this.inner.acceptWithMethods( - this.supportedVerificationMethods.map(verificationMethodIdentifierToMethod), - ); + const req: undefined | OutgoingRequest = this.inner.acceptWithMethods( + this.supportedVerificationMethods.map(verificationMethodIdentifierToMethod), + ); if (req) { await this.outgoingRequestProcessor.makeOutgoingRequest(req); } @@ -315,6 +319,32 @@ export class RustVerificationRequest return this._verifier; } + /** + * Start a QR code verification by providing a scanned QR code for this verification flow. + * + * Implementation of {@link Crypto.VerificationRequest#scanQRCode}. + * + * @param qrCodeData - the decoded QR code. + * @returns A verifier; call `.verify()` on it to wait for the other side to complete the verification flow. + */ + public async scanQRCode(uint8Array: Uint8Array): Promise { + const scan = RustSdkCryptoJs.QrCodeScan.fromBytes(new Uint8ClampedArray(uint8Array)); + const verifier: RustSdkCryptoJs.Qr = await this.inner.scanQrCode(scan); + + // this should have triggered the onChange callback, and we should now have a verifier + if (!this._verifier) { + throw new Error("Still no verifier after scanQrCode() call"); + } + + // we can immediately trigger the reciprocate request + const req: undefined | OutgoingRequest = verifier.reciprocate(); + if (req) { + await this.outgoingRequestProcessor.makeOutgoingRequest(req); + } + + return this._verifier; + } + /** * The verifier which is doing the actual verification, once the method has been established. * Only defined when the `phase` is Started. @@ -359,23 +389,28 @@ export class RustVerificationRequest } } -/** @internal */ -export class RustSASVerifier extends TypedEventEmitter implements Verifier { +/** Common base class for `Verifier` implementations which wrap rust classes. + * + * The generic parameter `InnerType` is the type of the rust Verification class which we wrap. + * + * @internal + */ +abstract class BaseRustVerifer extends TypedEventEmitter< + VerifierEvent, + VerifierEventHandlerMap +> { /** A promise which completes when the verification completes (or rejects when it is cancelled/fails) */ - private readonly completionPromise: Promise; - - private callbacks: ShowSasCallbacks | null = null; + protected readonly completionPromise: Promise; public constructor( - private readonly inner: RustSdkCryptoJs.Sas, - _verificationRequest: RustVerificationRequest, - private readonly outgoingRequestProcessor: OutgoingRequestProcessor, + protected readonly inner: InnerType, + protected readonly outgoingRequestProcessor: OutgoingRequestProcessor, ) { super(); this.completionPromise = new Promise((resolve, reject) => { const onChange = async (): Promise => { - this.updateCallbacks(); + this.onChange(); if (this.inner.isDone()) { resolve(undefined); @@ -396,8 +431,113 @@ export class RustSASVerifier extends TypedEventEmitter null); } + /** + * Hook which is called when the underlying rust class notifies us that there has been a change. + * + * Can be overridden by subclasses to see if we can notify the application about an update. + */ + protected onChange(): void {} + + /** + * Returns true if the verification has been cancelled, either by us or the other side. + */ + public get hasBeenCancelled(): boolean { + return this.inner.isCancelled(); + } + + /** + * The ID of the other user in the verification process. + */ + public get userId(): string { + return this.inner.otherUserId.toString(); + } + + /** + * Cancel a verification. + * + * We will send an `m.key.verification.cancel` if the verification is still in flight. The verification promise + * will reject, and a {@link Crypto.VerifierEvent#Cancel} will be emitted. + * + * @param e - the reason for the cancellation. + */ + public cancel(e: Error): void { + // TODO: something with `e` + const req: undefined | OutgoingRequest = this.inner.cancel(); + if (req) { + this.outgoingRequestProcessor.makeOutgoingRequest(req); + } + } + + /** + * Get the details for an SAS verification, if one is in progress + * + * Returns `null`, unless this verifier is for a SAS-based verification and we are waiting for the user to confirm + * the SAS matches. + */ + public getShowSasCallbacks(): ShowSasCallbacks | null { + return null; + } + + /** + * Get the details for reciprocating QR code verification, if one is in progress + * + * Returns `null`, unless this verifier is for reciprocating a QR-code-based verification (ie, the other user has + * already scanned our QR code), and we are waiting for the user to confirm. + */ + public getReciprocateQrCodeCallbacks(): ShowQrCodeCallbacks | null { + return null; + } +} + +/** A Verifier instance which is used to show and/or scan a QR code. */ +export class RustQrCodeVerifier extends BaseRustVerifer implements Verifier { + public constructor(inner: RustSdkCryptoJs.Qr, outgoingRequestProcessor: OutgoingRequestProcessor) { + super(inner, outgoingRequestProcessor); + } + + /** + * Start the key verification, if it has not already been started. + * + * @returns Promise which resolves when the verification has completed, or rejects if the verification is cancelled + * or times out. + */ + public async verify(): Promise { + // Nothing to do here but wait. + await this.completionPromise; + } +} + +/** A Verifier instance which is used if we are exchanging emojis */ +export class RustSASVerifier extends BaseRustVerifer implements Verifier { + private callbacks: ShowSasCallbacks | null = null; + + public constructor( + inner: RustSdkCryptoJs.Sas, + _verificationRequest: RustVerificationRequest, + outgoingRequestProcessor: OutgoingRequestProcessor, + ) { + super(inner, outgoingRequestProcessor); + } + + /** + * Start the key verification, if it has not already been started. + * + * This means sending a `m.key.verification.start` if we are the first responder, or a `m.key.verification.accept` + * if the other side has already sent a start event. + * + * @returns Promise which resolves when the verification has completed, or rejects if the verification is cancelled + * or times out. + */ + public async verify(): Promise { + const req: undefined | OutgoingRequest = this.inner.accept(); + if (req) { + await this.outgoingRequestProcessor.makeOutgoingRequest(req); + } + await this.completionPromise; + } + /** if we can now show the callbacks, do so */ - private updateCallbacks(): void { + protected onChange(): void { if (this.callbacks === null) { const emoji: Array | undefined = this.inner.emoji(); const decimal = this.inner.decimals() as [number, number, number] | undefined; @@ -428,53 +568,6 @@ export class RustSASVerifier extends TypedEventEmitter { - const req: undefined | OutgoingRequest = this.inner.accept(); - if (req) { - await this.outgoingRequestProcessor.makeOutgoingRequest(req); - } - await this.completionPromise; - } - - /** - * Cancel a verification. - * - * We will send an `m.key.verification.cancel` if the verification is still in flight. The verification promise - * will reject, and a {@link Crypto.VerifierEvent#Cancel} will be emitted. - * - * @param e - the reason for the cancellation. - */ - public cancel(e: Error): void { - // TODO: something with `e` - const req: undefined | OutgoingRequest = this.inner.cancel(); - if (req) { - this.outgoingRequestProcessor.makeOutgoingRequest(req); - } - } - /** * Get the details for an SAS verification, if one is in progress * @@ -484,16 +577,6 @@ export class RustSASVerifier extends TypedEventEmitter