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...
|
// 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 {
|
||||||
|
@ -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`.
|
||||||
|
@ -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
|
||||||
*
|
*
|
||||||
|
Reference in New Issue
Block a user