1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-07 23:02:56 +03:00

Add support for scanning QR codes during verification, with Rust crypto (#3565)

* Offer `m.qr_code.scan.v1` verification method by default

Normally, the application specifies the supported verification methods when
creating the MatrixClient (and matrix-react-sdk does so). If the application
leaves it unset, then the idea is that the js-sdk offers all known verification
methods.

However, by default, the rust-sdk doesn't specify `m.qr_code.scan.v1`. So
basically, we need to set our own list of supported methods, rather than
relying on the rust-sdk's defaults.

* Factor out base class from `RustSASVerifier`

* Implement QR code scanning

* Update src/rust-crypto/verification.ts
This commit is contained in:
Richard van der Hoff
2023-07-11 17:00:59 +01:00
committed by GitHub
parent d5b22e1deb
commit 9db6ce107a
7 changed files with 278 additions and 85 deletions

View File

@@ -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);
}

View File

@@ -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 */

View File

@@ -2241,7 +2241,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.cryptoCallbacks,
useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
);
rustCrypto.supportedVerificationMethods = this.verificationMethods;
rustCrypto.setSupportedVerificationMethods(this.verificationMethods);
this.cryptoBackend = rustCrypto;

View File

@@ -136,12 +136,27 @@ export interface VerificationRequest
/**
* Send an `m.key.verification.start` event to start verification via a particular method.
*
* This is normally used when starting a verification via emojis (ie, `method` is set to `m.sas.v1`).
*
* @param method - the name of the verification method to use.
*
* @returns The verifier which will do the actual verification.
*/
startVerification(method: string): Promise<Verifier>;
/**
* 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<Verifier>;
/**
* The verifier which is doing the actual verification, once the method has been established.
* Only defined when the `phase` is Started.

View File

@@ -478,6 +478,10 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
return verifier;
}
public scanQRCode(qrCodeData: Uint8Array): Promise<Verifier> {
throw new Error("QR code scanning not supported by legacy crypto");
}
/**
* sends the initial .request event.
* @returns resolves when the event has been sent.

View File

@@ -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<RustCryptoEvents, RustCryptoEv
new RustVerificationRequest(
request,
this.outgoingRequestProcessor,
this.supportedVerificationMethods,
this._supportedVerificationMethods,
),
);
}
@@ -582,10 +584,18 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
/**
* The verification methods we offer to the other side during an interactive verification.
*/
private _supportedVerificationMethods: string[] = ALL_VERIFICATION_METHODS;
/**
* Set the verification methods we offer to the other side during an interactive verification.
*
* If `undefined`, we will offer all the methods supported by the Rust SDK.
*/
public supportedVerificationMethods: string[] | undefined;
public setSupportedVerificationMethods(methods: string[] | undefined): void {
// by default, the Rust SDK does not offer `m.qr_code.scan.v1`, but we do want to offer that.
this._supportedVerificationMethods = methods ?? ALL_VERIFICATION_METHODS;
}
/**
* Send a verification request to our other devices.
@@ -606,10 +616,10 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
await userIdentity.requestVerification(
this.supportedVerificationMethods?.map(verificationMethodIdentifierToMethod),
this._supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
);
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods);
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
}
/**
@@ -636,10 +646,10 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
await device.requestVerification(
this.supportedVerificationMethods?.map(verificationMethodIdentifierToMethod),
this._supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
);
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods);
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -793,7 +803,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
if (request) {
this.emit(
CryptoEvent.VerificationRequestReceived,
new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods),
new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods),
);
}
}

View File

@@ -58,17 +58,18 @@ export class RustVerificationRequest
public constructor(
private readonly inner: RustSdkCryptoJs.VerificationRequest,
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
private readonly supportedVerificationMethods: string[] | undefined,
private readonly supportedVerificationMethods: string[],
) {
super();
const onChange = async (): Promise<void> => {
// 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,10 +232,7 @@ export class RustVerificationRequest
this._accepting = true;
try {
const req: undefined | OutgoingRequest =
this.supportedVerificationMethods === undefined
? this.inner.accept()
: this.inner.acceptWithMethods(
const req: undefined | OutgoingRequest = this.inner.acceptWithMethods(
this.supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
);
if (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<Verifier> {
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<VerifierEvent, VerifierEventHandlerMap> 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<InnerType extends RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas> extends TypedEventEmitter<
VerifierEvent,
VerifierEventHandlerMap
> {
/** A promise which completes when the verification completes (or rejects when it is cancelled/fails) */
private readonly completionPromise: Promise<void>;
private callbacks: ShowSasCallbacks | null = null;
protected readonly completionPromise: Promise<void>;
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<void>((resolve, reject) => {
const onChange = async (): Promise<void> => {
this.updateCallbacks();
this.onChange();
if (this.inner.isDone()) {
resolve(undefined);
@@ -396,8 +431,113 @@ export class RustSASVerifier extends TypedEventEmitter<VerifierEvent, VerifierEv
this.completionPromise.catch(() => 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<RustSdkCryptoJs.Qr> 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<void> {
// 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<RustSdkCryptoJs.Sas> 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<void> {
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<Emoji> | undefined = this.inner.emoji();
const decimal = this.inner.decimals() as [number, number, number] | undefined;
@@ -428,53 +568,6 @@ export class RustSASVerifier extends TypedEventEmitter<VerifierEvent, VerifierEv
}
}
/**
* 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();
}
/**
* 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<void> {
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<VerifierEvent, VerifierEv
public getShowSasCallbacks(): ShowSasCallbacks | null {
return this.callbacks;
}
/**
* 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;
}
}
/** For each specced verification method, the rust-side `VerificationMethod` corresponding to it */