1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Element-R: support for displaying QR codes during verification (#3588)

* Support for showing QR codes

* Emit `VerificationRequestEvent.Change` events when the verifier changes

* Minor integ test tweaks

* Handle transitions from QR code display to SAS

* Fix naming

* Add a test for `ShowQrCodeCallbacks.cancel`
This commit is contained in:
Richard van der Hoff
2023-07-13 12:11:13 +01:00
committed by GitHub
parent f005984df3
commit d92936fba5
3 changed files with 279 additions and 42 deletions

View File

@ -81,12 +81,8 @@ const TEST_HOMESERVER_URL = "https://alice-server.com";
*/ */
// we test with both crypto stacks... // we test with both crypto stacks...
describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: string, initCrypto: InitCrypto) => { describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: string, initCrypto: InitCrypto) => {
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// 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 // newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy
// backend. // backend. Once we drop support for legacy crypto, it will go away.
const newBackendOnly = backend === "rust-sdk" ? test : test.skip; const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
/** the client under test */ /** the client under test */
@ -391,12 +387,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(toDeviceMessage.transaction_id).toEqual(transactionId); expect(toDeviceMessage.transaction_id).toEqual(transactionId);
}); });
oldBackendOnly("can verify another via QR code with an untrusted cross-signing key", async () => { it("can verify another via QR code with an untrusted cross-signing key", async () => {
aliceClient = await startTestClient(); aliceClient = await startTestClient();
// QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now. // QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA); e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
await waitForDeviceList(); await waitForDeviceList();
expect(aliceClient.getStoredCrossSigningForUser(TEST_USER_ID)).toBeTruthy();
// have alice initiate a verification. She should send a m.key.verification.request // have alice initiate a verification. She should send a m.key.verification.request
const [requestBody, request] = await Promise.all([ const [requestBody, request] = await Promise.all([
@ -434,16 +429,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
); );
const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen); const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen);
// we should still be "Ready" and have no verifier
expect(request.phase).toEqual(VerificationPhase.Ready);
expect(request.verifier).toBeUndefined();
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start" // the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
returnToDeviceMessageFromSync({ returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret));
type: "m.key.verification.start",
content: {
from_device: TEST_DEVICE_ID,
method: "m.reciprocate.v1",
transaction_id: transactionId,
secret: encodeUnpaddedBase64(sharedSecret),
},
});
await waitForVerificationRequestChanged(request); await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Started); expect(request.phase).toEqual(VerificationPhase.Started);
expect(request.chosenMethod).toEqual("m.reciprocate.v1"); expect(request.chosenMethod).toEqual("m.reciprocate.v1");
@ -451,23 +442,25 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// there should now be a verifier // there should now be a verifier
const verifier: Verifier = request.verifier!; const verifier: Verifier = request.verifier!;
expect(verifier).toBeDefined(); expect(verifier).toBeDefined();
expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull();
// ... which we call .verify on, which emits a ShowReciprocateQr event // ... which we call .verify on, which emits a ShowReciprocateQr event
const verificationPromise = verifier.verify(); const reciprocatePromise = new Promise<ShowQrCodeCallbacks>((resolve) => {
const reciprocateQRCodeCallbacks = await new Promise<ShowQrCodeCallbacks>((resolve) => {
verifier.once(VerifierEvent.ShowReciprocateQr, resolve); verifier.once(VerifierEvent.ShowReciprocateQr, resolve);
}); });
const verificationPromise = verifier.verify();
const reciprocateQRCodeCallbacks = await reciprocatePromise;
// getReciprocateQrCodeCallbacks() is an alternative way to get the callbacks // getReciprocateQrCodeCallbacks() is an alternative way to get the callbacks
expect(verifier.getReciprocateQrCodeCallbacks()).toBe(reciprocateQRCodeCallbacks); expect(verifier.getReciprocateQrCodeCallbacks()).toBe(reciprocateQRCodeCallbacks);
expect(verifier.getShowSasCallbacks()).toBeNull(); expect(verifier.getShowSasCallbacks()).toBeNull();
// Alice confirms she is happy // Alice confirms she is happy, which makes her reply with a 'done'
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.done");
reciprocateQRCodeCallbacks.confirm(); reciprocateQRCodeCallbacks.confirm();
await sendToDevicePromise;
// that should satisfy Alice, who should reply with a 'done' // the dummy device replies with its own 'done'
await expectSendToDeviceMessage("m.key.verification.done"); returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
// ... and the whole thing should be done! // ... and the whole thing should be done!
await verificationPromise; await verificationPromise;
@ -531,12 +524,102 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
await verificationPromise; await verificationPromise;
expect(request.phase).toEqual(VerificationPhase.Done); expect(request.phase).toEqual(VerificationPhase.Done);
}); });
it("can send an SAS start after QR code display", async () => {
aliceClient = await startTestClient();
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, with an indication it can scan a QR code
// or do the emoji dance
returnToDeviceMessageFromSync(
buildReadyMessage(transactionId, ["m.qr_code.scan.v1", "m.sas.v1", "m.reciprocate.v1"]),
);
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Ready);
// Alice displays the QR code
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
expect(request.phase).toEqual(VerificationPhase.Ready);
expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
// ... but Alice wants to do an SAS verification
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
await request.startVerification("m.sas.v1");
await sendToDevicePromise;
// There should now be a `verifier`
const verifier: Verifier = request.verifier!;
expect(verifier).toBeDefined();
expect(request.chosenMethod).toEqual("m.sas.v1");
// clean up the test
expectSendToDeviceMessage("m.key.verification.cancel");
request.cancel();
await expect(verifier.verify()).rejects.toBeTruthy();
});
it("can receive an SAS start after QR code display", async () => {
aliceClient = await startTestClient();
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, with an indication it can scan a QR code
// or do the emoji dance
returnToDeviceMessageFromSync(
buildReadyMessage(transactionId, ["m.qr_code.scan.v1", "m.sas.v1", "m.reciprocate.v1"]),
);
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Ready);
// Alice displays the QR code
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
expect(request.phase).toEqual(VerificationPhase.Ready);
expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
// ... but the dummy device wants to do an SAS verification
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
await emitPromise(request, VerificationRequestEvent.Change);
// Alice should now have a `verifier`
const verifier: Verifier = request.verifier!;
expect(verifier).toBeDefined();
expect(request.chosenMethod).toEqual("m.sas.v1");
// clean up the test
expectSendToDeviceMessage("m.key.verification.cancel");
request.cancel();
await expect(verifier.verify()).rejects.toBeTruthy();
});
}); });
describe("cancellation", () => { describe("cancellation", () => {
beforeEach(async () => { beforeEach(async () => {
// pretend that we have another device, which we will start verifying // pretend that we have another device, which we will start verifying
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA); e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
aliceClient = await startTestClient(); aliceClient = await startTestClient();
await waitForDeviceList(); await waitForDeviceList();
@ -604,6 +687,50 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(request.phase).toEqual(VerificationPhase.Cancelled); expect(request.phase).toEqual(VerificationPhase.Cancelled);
expect(verifier.hasBeenCancelled).toBe(true); expect(verifier.hasBeenCancelled).toBe(true);
}); });
it("can cancel in the ShowQrCodeCallbacks", async () => {
// have alice initiate a verification. She should send 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, with an indication it can scan the QR code
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.qr_code.scan.v1"]));
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Ready);
// we should now have QR data we can display
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
const sharedSecret = qrCodeBuffer.subarray(74 + transactionId.length);
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret));
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Started);
expect(request.chosenMethod).toEqual("m.reciprocate.v1");
// there should now be a verifier
const verifier: Verifier = request.verifier!;
expect(verifier).toBeDefined();
// ... which we call .verify on, which emits a ShowReciprocateQr event
const reciprocatePromise = emitPromise(verifier, VerifierEvent.ShowReciprocateQr);
const verificationPromise = verifier.verify();
const reciprocateQRCodeCallbacks: ShowQrCodeCallbacks = await reciprocatePromise;
// Alice complains that she didn't see the dummy device scan her code
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.cancel");
reciprocateQRCodeCallbacks.cancel();
await sendToDevicePromise;
// ... which should cancel the verifier
await expect(verificationPromise).rejects.toBeTruthy();
expect(request.phase).toEqual(VerificationPhase.Cancelled);
expect(verifier.hasBeenCancelled).toBe(true);
});
}); });
describe("Incoming verification from another device", () => { describe("Incoming verification from another device", () => {
@ -779,6 +906,19 @@ function buildReadyMessage(transactionId: string, methods: string[]): { type: st
}; };
} }
/** build an m.key.verification.start to-device message suitable for the m.reciprocate.v1 flow, originating from the dummy device */
function buildReciprocateStartMessage(transactionId: string, sharedSecret: Uint8Array) {
return {
type: "m.key.verification.start",
content: {
from_device: TEST_DEVICE_ID,
method: "m.reciprocate.v1",
transaction_id: transactionId,
secret: encodeUnpaddedBase64(sharedSecret),
},
};
}
/** build an m.key.verification.start to-device message suitable for the SAS flow, originating from the dummy device */ /** build an m.key.verification.start to-device message suitable for the SAS flow, originating from the dummy device */
function buildSasStartMessage(transactionId: string): { type: string; content: object } { function buildSasStartMessage(transactionId: string): { type: string; content: object } {
return { return {

View File

@ -310,7 +310,7 @@ export enum VerifierEvent {
ShowSas = "show_sas", ShowSas = "show_sas",
/** /**
* QR code data should be displayed to the user. * The user should confirm if the other side has scanned our QR code.
* *
* The payload is the {@link ShowQrCodeCallbacks} object. * The payload is the {@link ShowQrCodeCallbacks} object.
*/ */
@ -325,7 +325,7 @@ export type VerifierEventHandlerMap = {
}; };
/** /**
* Callbacks for user actions while a QR code is displayed. * Callbacks for user actions to confirm that the other side has scanned our QR code.
* *
* This is exposed as the payload of a `VerifierEvent.ShowReciprocateQr` event, or can be retrieved directly from the * This is exposed as the payload of a `VerifierEvent.ShowReciprocateQr` event, or can be retrieved directly from the
* verifier as `reciprocateQREvent`. * verifier as `reciprocateQREvent`.

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import { Emoji } from "@matrix-org/matrix-sdk-crypto-js"; import { Emoji, QrState } from "@matrix-org/matrix-sdk-crypto-js";
import { import {
ShowQrCodeCallbacks, ShowQrCodeCallbacks,
@ -30,6 +30,7 @@ import {
} from "../crypto-api/verification"; } from "../crypto-api/verification";
import { TypedEventEmitter } from "../models/typed-event-emitter"; import { TypedEventEmitter } from "../models/typed-event-emitter";
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
import { TypedReEmitter } from "../ReEmitter";
/** /**
* An incoming, or outgoing, request to verify a user or a device via cross-signing. * An incoming, or outgoing, request to verify a user or a device via cross-signing.
@ -40,13 +41,16 @@ export class RustVerificationRequest
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap> extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap>
implements VerificationRequest implements VerificationRequest
{ {
/** a reëmitter which relays VerificationRequestEvent.Changed events emitted by the verifier */
private readonly reEmitter: TypedReEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap>;
/** Are we in the process of sending an `m.key.verification.ready` event? */ /** Are we in the process of sending an `m.key.verification.ready` event? */
private _accepting = false; private _accepting = false;
/** Are we in the process of sending an `m.key.verification.cancellation` event? */ /** Are we in the process of sending an `m.key.verification.cancellation` event? */
private _cancelling = false; private _cancelling = false;
private _verifier: Verifier | undefined; private _verifier: undefined | RustSASVerifier | RustQrCodeVerifier;
/** /**
* Construct a new RustVerificationRequest to wrap the rust-level `VerificationRequest`. * Construct a new RustVerificationRequest to wrap the rust-level `VerificationRequest`.
@ -62,15 +66,19 @@ export class RustVerificationRequest
) { ) {
super(); super();
this.reEmitter = new TypedReEmitter(this);
const onChange = async (): Promise<void> => { const onChange = async (): Promise<void> => {
// if we now have a `Verification` where we lacked one before, wrap it. const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
if (this._verifier === undefined) {
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification(); // If we now have a `Verification` where we lacked one before, or we have transitioned from QR to SAS,
if (verification instanceof RustSdkCryptoJs.Sas) { // wrap the new rust Verification as a js-sdk Verifier.
if (verification instanceof RustSdkCryptoJs.Sas) {
if (this._verifier === undefined || this._verifier instanceof RustQrCodeVerifier) {
this.setVerifier(new RustSASVerifier(verification, this, outgoingRequestProcessor)); this.setVerifier(new RustSASVerifier(verification, this, outgoingRequestProcessor));
} else if (verification instanceof RustSdkCryptoJs.Qr) {
this.setVerifier(new RustQrCodeVerifier(verification, outgoingRequestProcessor));
} }
} else if (verification instanceof RustSdkCryptoJs.Qr && this._verifier === undefined) {
this.setVerifier(new RustQrCodeVerifier(verification, outgoingRequestProcessor));
} }
this.emit(VerificationRequestEvent.Change); this.emit(VerificationRequestEvent.Change);
@ -79,7 +87,12 @@ export class RustVerificationRequest
} }
private setVerifier(verifier: RustSASVerifier | RustQrCodeVerifier): void { private setVerifier(verifier: RustSASVerifier | RustQrCodeVerifier): void {
// if we already have a verifier, unsubscribe from its events
if (this._verifier) {
this.reEmitter.stopReEmitting(this._verifier, [VerificationRequestEvent.Change]);
}
this._verifier = verifier; this._verifier = verifier;
this.reEmitter.reEmit(this._verifier, [VerificationRequestEvent.Change]);
} }
/** /**
@ -138,7 +151,11 @@ export class RustVerificationRequest
// parlance. // parlance.
return this._accepting ? VerificationPhase.Requested : VerificationPhase.Ready; return this._accepting ? VerificationPhase.Requested : VerificationPhase.Ready;
case RustSdkCryptoJs.VerificationRequestPhase.Transitioned: case RustSdkCryptoJs.VerificationRequestPhase.Transitioned:
return VerificationPhase.Started; if (!this._verifier) {
// this shouldn't happen, because the onChange handler should have created a _verifier.
throw new Error("VerificationRequest: inner phase == Transitioned but no verifier!");
}
return this._verifier.verificationPhase;
case RustSdkCryptoJs.VerificationRequestPhase.Done: case RustSdkCryptoJs.VerificationRequestPhase.Done:
return VerificationPhase.Done; return VerificationPhase.Done;
case RustSdkCryptoJs.VerificationRequestPhase.Cancelled: case RustSdkCryptoJs.VerificationRequestPhase.Cancelled:
@ -189,8 +206,9 @@ export class RustVerificationRequest
/** the method picked in the .start event */ /** the method picked in the .start event */
public get chosenMethod(): string | null { public get chosenMethod(): string | null {
if (this.phase !== VerificationPhase.Started) return null;
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification(); const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
// TODO: this isn't quite right. The existence of a Verification doesn't prove that we have .started.
if (verification instanceof RustSdkCryptoJs.Sas) { if (verification instanceof RustSdkCryptoJs.Sas) {
return "m.sas.v1"; return "m.sas.v1";
} else if (verification instanceof RustSdkCryptoJs.Qr) { } else if (verification instanceof RustSdkCryptoJs.Qr) {
@ -350,15 +368,20 @@ export class RustVerificationRequest
* Only defined when the `phase` is Started. * Only defined when the `phase` is Started.
*/ */
public get verifier(): Verifier | undefined { public get verifier(): Verifier | undefined {
return this._verifier; // It's possible for us to have a Verifier before a method has been chosen (in particular,
// if we are showing a QR code which the other device has not yet scanned. At that point, we could
// still switch to SAS).
//
// In that case, we should not return it to the application yet, since the application will not expect the
// Verifier to be replaced during the lifetime of the VerificationRequest.
return this.phase === VerificationPhase.Started ? this._verifier : undefined;
} }
/** /**
* Stub implementation of {@link Crypto.VerificationRequest#getQRCodeBytes}. * Stub implementation of {@link Crypto.VerificationRequest#getQRCodeBytes}.
*/ */
public getQRCodeBytes(): Buffer | undefined { public getQRCodeBytes(): Buffer | undefined {
// TODO throw new Error("getQRCodeBytes() unsupported in Rust Crypto; use generateQRCode() instead.");
return undefined;
} }
/** /**
@ -367,8 +390,8 @@ export class RustVerificationRequest
* Implementation of {@link Crypto.VerificationRequest#generateQRCode}. * Implementation of {@link Crypto.VerificationRequest#generateQRCode}.
*/ */
public async generateQRCode(): Promise<Buffer | undefined> { public async generateQRCode(): Promise<Buffer | undefined> {
// TODO const innerVerifier: RustSdkCryptoJs.Qr = await this.inner.generateQrCode();
return undefined; return Buffer.from(innerVerifier.toBytes());
} }
/** /**
@ -396,8 +419,8 @@ export class RustVerificationRequest
* @internal * @internal
*/ */
abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas> extends TypedEventEmitter< abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas> extends TypedEventEmitter<
VerifierEvent, VerifierEvent | VerificationRequestEvent,
VerifierEventHandlerMap VerifierEventHandlerMap & VerificationRequestEventHandlerMap
> { > {
/** A promise which completes when the verification completes (or rejects when it is cancelled/fails) */ /** A promise which completes when the verification completes (or rejects when it is cancelled/fails) */
protected readonly completionPromise: Promise<void>; protected readonly completionPromise: Promise<void>;
@ -424,6 +447,8 @@ abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCry
), ),
); );
} }
this.emit(VerificationRequestEvent.Change);
}; };
inner.registerChangesCallback(onChange); inner.registerChangesCallback(onChange);
}); });
@ -460,7 +485,7 @@ abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCry
* *
* @param e - the reason for the cancellation. * @param e - the reason for the cancellation.
*/ */
public cancel(e: Error): void { public cancel(e?: Error): void {
// TODO: something with `e` // TODO: something with `e`
const req: undefined | OutgoingRequest = this.inner.cancel(); const req: undefined | OutgoingRequest = this.inner.cancel();
if (req) { if (req) {
@ -491,10 +516,23 @@ abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCry
/** A Verifier instance which is used to show and/or scan a QR code. */ /** A Verifier instance which is used to show and/or scan a QR code. */
export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> implements Verifier { export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> implements Verifier {
private callbacks: ShowQrCodeCallbacks | null = null;
public constructor(inner: RustSdkCryptoJs.Qr, outgoingRequestProcessor: OutgoingRequestProcessor) { public constructor(inner: RustSdkCryptoJs.Qr, outgoingRequestProcessor: OutgoingRequestProcessor) {
super(inner, outgoingRequestProcessor); super(inner, outgoingRequestProcessor);
} }
protected onChange(): void {
// if the other side has scanned our QR code and sent us a "reciprocate" message, it is now time for the
// application to prompt the user to confirm their side.
if (this.callbacks === null && this.inner.hasBeenScanned()) {
this.callbacks = {
confirm: () => this.confirmScanning(),
cancel: () => this.cancel(),
};
}
}
/** /**
* Start the key verification, if it has not already been started. * Start the key verification, if it has not already been started.
* *
@ -502,9 +540,61 @@ export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> impl
* or times out. * or times out.
*/ */
public async verify(): Promise<void> { public async verify(): Promise<void> {
// Some applications (hello, matrix-react-sdk) may not check if there is a `ShowQrCodeCallbacks` and instead
// register a `ShowReciprocateQr` listener which they expect to be called once `.verify` is called.
if (this.callbacks !== null) {
this.emit(VerifierEvent.ShowReciprocateQr, this.callbacks);
}
// Nothing to do here but wait. // Nothing to do here but wait.
await this.completionPromise; await this.completionPromise;
} }
/**
* Calculate an appropriate VerificationPhase for a VerificationRequest where this is the verifier.
*
* This is abnormally complicated because a rust-side QR Code verifier can span several verification phases.
*/
public get verificationPhase(): VerificationPhase {
switch (this.inner.state()) {
case QrState.Created:
// we have created a QR for display; neither side has yet sent an `m.key.verification.start`.
return VerificationPhase.Ready;
case QrState.Scanned:
// other side has scanned our QR and sent an `m.key.verification.start` with `m.reciprocate.v1`
return VerificationPhase.Started;
case QrState.Confirmed:
// we have confirmed the other side's scan and sent an `m.key.verification.done`.
return VerificationPhase.Done;
case QrState.Reciprocated:
// although the rust SDK doesn't immediately send the `m.key.verification.start` on transition into this
// state, `RustVerificationRequest.scanQrCode` immediately calls `reciprocate()` and does so, so in practice
// we can treat the two the same.
return VerificationPhase.Started;
case QrState.Done:
return VerificationPhase.Done;
case QrState.Cancelled:
return VerificationPhase.Cancelled;
default:
throw new Error(`Unknown qr code state ${this.inner.state()}`);
}
}
/**
* 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 this.callbacks;
}
private async confirmScanning(): Promise<void> {
const req: undefined | OutgoingRequest = this.inner.confirmScanning();
if (req) {
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
}
}
} }
/** A Verifier instance which is used if we are exchanging emojis */ /** A Verifier instance which is used if we are exchanging emojis */
@ -568,6 +658,13 @@ export class RustSASVerifier extends BaseRustVerifer<RustSdkCryptoJs.Sas> implem
} }
} }
/**
* Calculate an appropriate VerificationPhase for a VerificationRequest where this is the verifier.
*/
public get verificationPhase(): VerificationPhase {
return VerificationPhase.Started;
}
/** /**
* Get the details for an SAS verification, if one is in progress * Get the details for an SAS verification, if one is in progress
* *