You've already forked matrix-js-sdk
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:
committed by
GitHub
parent
f005984df3
commit
d92936fba5
@ -81,12 +81,8 @@ const TEST_HOMESERVER_URL = "https://alice-server.com";
|
||||
*/
|
||||
// we test with both crypto stacks...
|
||||
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
|
||||
// backend.
|
||||
// backend. Once we drop support for legacy crypto, it will go away.
|
||||
const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
|
||||
|
||||
/** the client under test */
|
||||
@ -391,12 +387,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
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();
|
||||
// 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);
|
||||
await waitForDeviceList();
|
||||
expect(aliceClient.getStoredCrossSigningForUser(TEST_USER_ID)).toBeTruthy();
|
||||
|
||||
// have alice initiate a verification. She should send a m.key.verification.request
|
||||
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);
|
||||
|
||||
// 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"
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.start",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
method: "m.reciprocate.v1",
|
||||
transaction_id: transactionId,
|
||||
secret: encodeUnpaddedBase64(sharedSecret),
|
||||
},
|
||||
});
|
||||
returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret));
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(VerificationPhase.Started);
|
||||
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
|
||||
const verifier: Verifier = request.verifier!;
|
||||
expect(verifier).toBeDefined();
|
||||
expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull();
|
||||
|
||||
// ... which we call .verify on, which emits a ShowReciprocateQr event
|
||||
const verificationPromise = verifier.verify();
|
||||
const reciprocateQRCodeCallbacks = await new Promise<ShowQrCodeCallbacks>((resolve) => {
|
||||
const reciprocatePromise = new Promise<ShowQrCodeCallbacks>((resolve) => {
|
||||
verifier.once(VerifierEvent.ShowReciprocateQr, resolve);
|
||||
});
|
||||
const verificationPromise = verifier.verify();
|
||||
const reciprocateQRCodeCallbacks = await reciprocatePromise;
|
||||
|
||||
// getReciprocateQrCodeCallbacks() is an alternative way to get the callbacks
|
||||
expect(verifier.getReciprocateQrCodeCallbacks()).toBe(reciprocateQRCodeCallbacks);
|
||||
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();
|
||||
await sendToDevicePromise;
|
||||
|
||||
// that should satisfy Alice, who should reply with a 'done'
|
||||
await expectSendToDeviceMessage("m.key.verification.done");
|
||||
// the dummy device replies with its own 'done'
|
||||
returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
|
||||
|
||||
// ... and the whole thing should be done!
|
||||
await verificationPromise;
|
||||
@ -531,12 +524,102 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
await verificationPromise;
|
||||
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", () => {
|
||||
beforeEach(async () => {
|
||||
// pretend that we have another device, which we will start verifying
|
||||
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
|
||||
aliceClient = await startTestClient();
|
||||
await waitForDeviceList();
|
||||
@ -604,6 +687,50 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(request.phase).toEqual(VerificationPhase.Cancelled);
|
||||
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", () => {
|
||||
@ -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 */
|
||||
function buildSasStartMessage(transactionId: string): { type: string; content: object } {
|
||||
return {
|
||||
|
@ -310,7 +310,7 @@ export enum VerifierEvent {
|
||||
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.
|
||||
*/
|
||||
@ -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
|
||||
* verifier as `reciprocateQREvent`.
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
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 {
|
||||
ShowQrCodeCallbacks,
|
||||
@ -30,6 +30,7 @@ import {
|
||||
} from "../crypto-api/verification";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
|
||||
/**
|
||||
* 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>
|
||||
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? */
|
||||
private _accepting = false;
|
||||
|
||||
/** Are we in the process of sending an `m.key.verification.cancellation` event? */
|
||||
private _cancelling = false;
|
||||
|
||||
private _verifier: Verifier | undefined;
|
||||
private _verifier: undefined | RustSASVerifier | RustQrCodeVerifier;
|
||||
|
||||
/**
|
||||
* Construct a new RustVerificationRequest to wrap the rust-level `VerificationRequest`.
|
||||
@ -62,15 +66,19 @@ export class RustVerificationRequest
|
||||
) {
|
||||
super();
|
||||
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
|
||||
const onChange = async (): Promise<void> => {
|
||||
// if we now have a `Verification` where we lacked one before, wrap it.
|
||||
if (this._verifier === undefined) {
|
||||
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
|
||||
if (verification instanceof RustSdkCryptoJs.Sas) {
|
||||
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,
|
||||
// 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));
|
||||
} 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);
|
||||
@ -79,7 +87,12 @@ export class RustVerificationRequest
|
||||
}
|
||||
|
||||
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.reEmitter.reEmit(this._verifier, [VerificationRequestEvent.Change]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,7 +151,11 @@ export class RustVerificationRequest
|
||||
// parlance.
|
||||
return this._accepting ? VerificationPhase.Requested : VerificationPhase.Ready;
|
||||
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:
|
||||
return VerificationPhase.Done;
|
||||
case RustSdkCryptoJs.VerificationRequestPhase.Cancelled:
|
||||
@ -189,8 +206,9 @@ export class RustVerificationRequest
|
||||
|
||||
/** the method picked in the .start event */
|
||||
public get chosenMethod(): string | null {
|
||||
if (this.phase !== VerificationPhase.Started) return null;
|
||||
|
||||
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) {
|
||||
return "m.sas.v1";
|
||||
} else if (verification instanceof RustSdkCryptoJs.Qr) {
|
||||
@ -350,15 +368,20 @@ export class RustVerificationRequest
|
||||
* Only defined when the `phase` is Started.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
public getQRCodeBytes(): Buffer | undefined {
|
||||
// TODO
|
||||
return undefined;
|
||||
throw new Error("getQRCodeBytes() unsupported in Rust Crypto; use generateQRCode() instead.");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -367,8 +390,8 @@ export class RustVerificationRequest
|
||||
* Implementation of {@link Crypto.VerificationRequest#generateQRCode}.
|
||||
*/
|
||||
public async generateQRCode(): Promise<Buffer | undefined> {
|
||||
// TODO
|
||||
return undefined;
|
||||
const innerVerifier: RustSdkCryptoJs.Qr = await this.inner.generateQrCode();
|
||||
return Buffer.from(innerVerifier.toBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -396,8 +419,8 @@ export class RustVerificationRequest
|
||||
* @internal
|
||||
*/
|
||||
abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas> extends TypedEventEmitter<
|
||||
VerifierEvent,
|
||||
VerifierEventHandlerMap
|
||||
VerifierEvent | VerificationRequestEvent,
|
||||
VerifierEventHandlerMap & VerificationRequestEventHandlerMap
|
||||
> {
|
||||
/** A promise which completes when the verification completes (or rejects when it is cancelled/fails) */
|
||||
protected readonly completionPromise: Promise<void>;
|
||||
@ -424,6 +447,8 @@ abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCry
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
};
|
||||
inner.registerChangesCallback(onChange);
|
||||
});
|
||||
@ -460,7 +485,7 @@ abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCry
|
||||
*
|
||||
* @param e - the reason for the cancellation.
|
||||
*/
|
||||
public cancel(e: Error): void {
|
||||
public cancel(e?: Error): void {
|
||||
// TODO: something with `e`
|
||||
const req: undefined | OutgoingRequest = this.inner.cancel();
|
||||
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. */
|
||||
export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> implements Verifier {
|
||||
private callbacks: ShowQrCodeCallbacks | null = null;
|
||||
|
||||
public constructor(inner: RustSdkCryptoJs.Qr, outgoingRequestProcessor: 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.
|
||||
*
|
||||
@ -502,9 +540,61 @@ export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> impl
|
||||
* or times out.
|
||||
*/
|
||||
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.
|
||||
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 */
|
||||
@ -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
|
||||
*
|
||||
|
Reference in New Issue
Block a user