1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

ElementR: fix emoji verification stalling when both ends hit start at the same time (#4004)

* Rust crypto: handle the SAS verifier being replaced

* lint

* make changes from review

* apply changes from code review

* remove useless assertions

* wrap acceptance inside a try-catch, and factor out acceptance into a function

* fix bugs

* we don't actually need the .accept variable

* move setInner to inside SAS class, and rename to replaceInner

* use defer to avoid using a closure

* lint

* prettier

* use the right name

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* combine onChangeCallback with onChange

* apply changes from review

* add test for QR code verification, and try changing order in onChange

* lint

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Hubert Chathi
2024-01-31 15:07:34 -05:00
committed by GitHub
parent d178fbf9cd
commit 8007bc5fe8
2 changed files with 571 additions and 32 deletions

View File

@@ -17,8 +17,19 @@ limitations under the License.
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm"; import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { Mocked } from "jest-mock"; import { Mocked } from "jest-mock";
import { isVerificationEvent, RustVerificationRequest } from "../../../src/rust-crypto/verification"; import {
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; isVerificationEvent,
RustVerificationRequest,
verificationMethodIdentifierToMethod,
} from "../../../src/rust-crypto/verification";
import {
ShowSasCallbacks,
VerificationRequestEvent,
Verifier,
VerifierEvent,
} from "../../../src/crypto-api/verification";
import { OutgoingRequest, OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { IDeviceKeys } from "../../../src/@types/crypto";
import { EventType, MatrixEvent, MsgType } from "../../../src"; import { EventType, MatrixEvent, MsgType } from "../../../src";
describe("VerificationRequest", () => { describe("VerificationRequest", () => {
@@ -91,6 +102,354 @@ describe("VerificationRequest", () => {
); );
}); });
}); });
it("can verify with SAS", async () => {
const aliceUserId = "@alice:example.org";
const aliceDeviceId = "ABCDEFG";
const bobUserId = "@bob:example.org";
const bobDeviceId = "HIJKLMN";
const [aliceOlmMachine, aliceDeviceKeys, aliceCrossSigningKeys] = await initOlmMachineAndKeys(
aliceUserId,
aliceDeviceId,
);
const [bobOlmMachine, bobDeviceKeys, bobCrossSigningKeys] = await initOlmMachineAndKeys(bobUserId, bobDeviceId);
const aliceRequestLoop = makeRequestLoop(
aliceOlmMachine,
aliceDeviceKeys,
aliceCrossSigningKeys,
bobOlmMachine,
bobDeviceKeys,
bobCrossSigningKeys,
);
const bobRequestLoop = makeRequestLoop(
bobOlmMachine,
bobDeviceKeys,
bobCrossSigningKeys,
aliceOlmMachine,
aliceDeviceKeys,
aliceCrossSigningKeys,
);
try {
await aliceOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(bobUserId)]);
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
// Alice requests verification
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
const innerVerificationRequest = await bobUserIdentity.requestVerification(
roomId,
new RustSdkCryptoJs.EventId("$m.key.verification.request"),
methods,
);
const aliceVerificationRequest = new RustVerificationRequest(
aliceOlmMachine,
innerVerificationRequest,
aliceRequestLoop as unknown as OutgoingRequestProcessor,
["m.sas.v1"],
);
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
await bobOlmMachine.receiveVerificationEvent(
JSON.stringify({
type: "m.room.message",
sender: aliceUserId,
event_id: "$m.key.verification.request",
content: verificationRequestContent,
origin_server_ts: Date.now(),
unsigned: {
age: 0,
},
}),
roomId,
);
// Bob accepts
const bobInnerVerificationRequest = bobOlmMachine.getVerificationRequest(
new RustSdkCryptoJs.UserId(aliceUserId),
"$m.key.verification.request",
)!;
const bobVerificationRequest = new RustVerificationRequest(
bobOlmMachine,
bobInnerVerificationRequest,
bobRequestLoop as unknown as OutgoingRequestProcessor,
["m.sas.v1"],
);
await bobVerificationRequest.accept();
// Alice starts the verification
const bobVerifierPromise: Promise<Verifier> = new Promise((resolve, reject) => {
bobVerificationRequest.on(VerificationRequestEvent.Change, () => {
const verifier = bobVerificationRequest.verifier;
if (verifier) {
resolve(verifier);
}
});
});
const aliceVerifier = await aliceVerificationRequest.startVerification("m.sas.v1");
const bobVerifier = await bobVerifierPromise;
// create a function to compare the SAS, and then let the verification run
let otherCallbacks: ShowSasCallbacks | undefined;
const compareSas = (callbacks: ShowSasCallbacks): void => {
if (otherCallbacks) {
const ourDecimal = callbacks.sas.decimal!;
const theirDecimal = otherCallbacks.sas.decimal!;
if (ourDecimal.every((el, idx) => el == theirDecimal[idx])) {
otherCallbacks.confirm();
callbacks.confirm();
} else {
otherCallbacks.mismatch();
callbacks.mismatch();
}
} else {
otherCallbacks = callbacks;
}
};
aliceVerifier.on(VerifierEvent.ShowSas, compareSas);
bobVerifier.on(VerifierEvent.ShowSas, compareSas);
await Promise.all([aliceVerifier.verify(), await bobVerifier.verify()]);
} finally {
await aliceRequestLoop.stop();
await bobRequestLoop.stop();
}
});
it("can handle simultaneous starts in SAS", async () => {
const aliceUserId = "@alice:example.org";
const aliceDeviceId = "ABCDEFG";
const bobUserId = "@bob:example.org";
const bobDeviceId = "HIJKLMN";
const [aliceOlmMachine, aliceDeviceKeys, aliceCrossSigningKeys] = await initOlmMachineAndKeys(
aliceUserId,
aliceDeviceId,
);
const [bobOlmMachine, bobDeviceKeys, bobCrossSigningKeys] = await initOlmMachineAndKeys(bobUserId, bobDeviceId);
let aliceStartRequest: RustSdkCryptoJs.RoomMessageRequest | undefined;
const aliceRequestLoop = makeRequestLoop(
aliceOlmMachine,
aliceDeviceKeys,
aliceCrossSigningKeys,
bobOlmMachine,
bobDeviceKeys,
bobCrossSigningKeys,
async (request): Promise<any> => {
// If the request is sending the m.key.verification.start
// event, we delay sending it until after Bob has also started
// a verification
if (
!aliceStartRequest &&
request instanceof RustSdkCryptoJs.RoomMessageRequest &&
request.event_type == "m.key.verification.start"
) {
aliceStartRequest = request;
return { event_id: "$m.key.verification.start" };
}
},
);
const bobRequestLoop = makeRequestLoop(
bobOlmMachine,
bobDeviceKeys,
bobCrossSigningKeys,
aliceOlmMachine,
aliceDeviceKeys,
aliceCrossSigningKeys,
);
try {
await aliceOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(bobUserId)]);
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
// Alice requests verification
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
const innerVerificationRequest = await bobUserIdentity.requestVerification(
roomId,
new RustSdkCryptoJs.EventId("$m.key.verification.request"),
methods,
);
const aliceVerificationRequest = new RustVerificationRequest(
aliceOlmMachine,
innerVerificationRequest,
aliceRequestLoop as unknown as OutgoingRequestProcessor,
["m.sas.v1"],
);
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
await bobOlmMachine.receiveVerificationEvent(
JSON.stringify({
type: "m.room.message",
sender: aliceUserId,
event_id: "$m.key.verification.request",
content: verificationRequestContent,
origin_server_ts: Date.now(),
unsigned: {
age: 0,
},
}),
roomId,
);
// Bob accepts
const bobInnerVerificationRequest = bobOlmMachine.getVerificationRequest(
new RustSdkCryptoJs.UserId(aliceUserId),
"$m.key.verification.request",
)!;
const bobVerificationRequest = new RustVerificationRequest(
bobOlmMachine,
bobInnerVerificationRequest,
bobRequestLoop as unknown as OutgoingRequestProcessor,
["m.sas.v1"],
);
await bobVerificationRequest.accept();
// Alice and Bob both start the verification
const aliceVerifier = await aliceVerificationRequest.startVerification("m.sas.v1");
const bobVerifier = await bobVerificationRequest.startVerification("m.sas.v1");
// We can now send Alice's start message to Bob
await aliceRequestLoop.makeOutgoingRequest(aliceStartRequest!);
// create a function to compare the SAS, and then let the verification run
let otherCallbacks: ShowSasCallbacks | undefined;
const compareSas = (callbacks: ShowSasCallbacks) => {
if (otherCallbacks) {
const ourDecimal = callbacks.sas.decimal!;
const theirDecimal = otherCallbacks.sas.decimal!;
if (ourDecimal.every((el, idx) => el == theirDecimal[idx])) {
otherCallbacks.confirm();
callbacks.confirm();
} else {
otherCallbacks.mismatch();
callbacks.mismatch();
}
} else {
otherCallbacks = callbacks;
}
};
aliceVerifier.on(VerifierEvent.ShowSas, compareSas);
bobVerifier.on(VerifierEvent.ShowSas, compareSas);
await Promise.all([aliceVerifier.verify(), await bobVerifier.verify()]);
} finally {
await aliceRequestLoop.stop();
await bobRequestLoop.stop();
}
});
it("can verify by QR code", async () => {
const aliceUserId = "@alice:example.org";
const aliceDeviceId = "ABCDEFG";
const bobUserId = "@bob:example.org";
const bobDeviceId = "HIJKLMN";
const [aliceOlmMachine, aliceDeviceKeys, aliceCrossSigningKeys] = await initOlmMachineAndKeys(
aliceUserId,
aliceDeviceId,
);
const [bobOlmMachine, bobDeviceKeys, bobCrossSigningKeys] = await initOlmMachineAndKeys(bobUserId, bobDeviceId);
const aliceRequestLoop = makeRequestLoop(
aliceOlmMachine,
aliceDeviceKeys,
aliceCrossSigningKeys,
bobOlmMachine,
bobDeviceKeys,
bobCrossSigningKeys,
);
const bobRequestLoop = makeRequestLoop(
bobOlmMachine,
bobDeviceKeys,
bobCrossSigningKeys,
aliceOlmMachine,
aliceDeviceKeys,
aliceCrossSigningKeys,
);
try {
await aliceOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(bobUserId)]);
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
// Alice requests verification
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
const methods = [
verificationMethodIdentifierToMethod("m.reciprocate.v1"),
verificationMethodIdentifierToMethod("m.qr_code.show.v1"),
];
const innerVerificationRequest = await bobUserIdentity.requestVerification(
roomId,
new RustSdkCryptoJs.EventId("$m.key.verification.request"),
methods,
);
const aliceVerificationRequest = new RustVerificationRequest(
aliceOlmMachine,
innerVerificationRequest,
aliceRequestLoop as unknown as OutgoingRequestProcessor,
["m.reciprocate.v1", "m.qr_code.show.v1"],
);
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
await bobOlmMachine.receiveVerificationEvent(
JSON.stringify({
type: "m.room.message",
sender: aliceUserId,
event_id: "$m.key.verification.request",
content: verificationRequestContent,
origin_server_ts: Date.now(),
unsigned: {
age: 0,
},
}),
roomId,
);
// Bob accepts
const bobInnerVerificationRequest = bobOlmMachine.getVerificationRequest(
new RustSdkCryptoJs.UserId(aliceUserId),
"$m.key.verification.request",
)!;
const bobVerificationRequest = new RustVerificationRequest(
bobOlmMachine,
bobInnerVerificationRequest,
bobRequestLoop as unknown as OutgoingRequestProcessor,
["m.reciprocate.v1", "m.qr_code.show.v1", "m.qr_code.scan.v1"],
);
await bobVerificationRequest.accept();
// Bob scans
const qrCode = await aliceVerificationRequest.generateQRCode();
const aliceVerifierPromise: Promise<Verifier> = new Promise((resolve, reject) => {
aliceVerificationRequest.on(VerificationRequestEvent.Change, () => {
const verifier = aliceVerificationRequest.verifier;
if (verifier) {
resolve(verifier);
}
});
});
const bobVerifier = await bobVerificationRequest.scanQRCode(qrCode!);
const aliceVerifier = await aliceVerifierPromise;
aliceVerifier.on(VerifierEvent.ShowReciprocateQr, (showQrCodeCallbacks) => {
showQrCodeCallbacks.confirm();
});
await Promise.all([aliceVerifier.verify(), await bobVerifier.verify()]);
} finally {
await aliceRequestLoop.stop();
await bobRequestLoop.stop();
}
});
}); });
describe("isVerificationEvent", () => { describe("isVerificationEvent", () => {
@@ -152,3 +511,148 @@ function makeMockedInner(): Mocked<RustSdkCryptoJs.VerificationRequest> {
}, },
} as unknown as Mocked<RustSdkCryptoJs.VerificationRequest>; } as unknown as Mocked<RustSdkCryptoJs.VerificationRequest>;
} }
interface CrossSigningKeys {
master_key: any;
self_signing_key: any;
user_signing_key: any;
}
/** create an Olm machine and device/cross-signing keys for a user */
async function initOlmMachineAndKeys(
userId: string,
deviceId: string,
): Promise<[RustSdkCryptoJs.OlmMachine, IDeviceKeys, CrossSigningKeys]> {
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(
new RustSdkCryptoJs.UserId(userId),
new RustSdkCryptoJs.DeviceId(deviceId),
undefined,
undefined,
);
const { uploadKeysRequest, uploadSignaturesRequest, uploadSigningKeysRequest } =
await olmMachine.bootstrapCrossSigning(true);
const deviceKeys = JSON.parse(uploadKeysRequest.body).device_keys;
await olmMachine.markRequestAsSent(
uploadKeysRequest.id,
uploadKeysRequest.type,
'{"one_time_key_counts":{"signed_curve25519":100}}',
);
const crossSigningSignatures = JSON.parse(uploadSignaturesRequest.body);
for (const [keyId, signature] of Object.entries(crossSigningSignatures[userId][deviceId]["signatures"][userId])) {
deviceKeys["signatures"][userId][keyId] = signature;
}
const crossSigningKeys = JSON.parse(uploadSigningKeysRequest.body);
// note: the upload signatures request and upload signing keys requests
// don't need to be marked as sent in the Olm machine
return [olmMachine, deviceKeys, crossSigningKeys];
}
type CustomRequestHandler = (request: OutgoingRequest | RustSdkCryptoJs.UploadSigningKeysRequest) => Promise<any>;
/** Loop for handling outgoing requests from an Olm machine.
*
* Simulates a server with two users: "us" and "them". Handles key query
* requests, querying either our keys or the other user's keys. Room messages
* are sent as incoming verification events to the other user. A custom
* handler can be added to override default request processing (the handler
* should return a response body to inhibit default processing).
*
* Can also be used as an OutgoingRequestProcessor. */
function makeRequestLoop(
ourOlmMachine: RustSdkCryptoJs.OlmMachine,
ourDeviceKeys: IDeviceKeys,
ourCrossSigningKeys: CrossSigningKeys,
theirOlmMachine: RustSdkCryptoJs.OlmMachine,
theirDeviceKeys: IDeviceKeys,
theirCrossSigningKeys: CrossSigningKeys,
customHandler?: CustomRequestHandler,
) {
let stopRequestLoop = false;
const ourUserId = ourOlmMachine.userId.toString();
const ourDeviceId = ourOlmMachine.deviceId.toString();
const theirUserId = theirOlmMachine.userId.toString();
const theirDeviceId = theirOlmMachine.deviceId.toString();
function defaultHandler(request: OutgoingRequest | RustSdkCryptoJs.UploadSigningKeysRequest): any {
if (request instanceof RustSdkCryptoJs.KeysQueryRequest) {
const resp: Record<string, any> = {
device_keys: {},
};
const body = JSON.parse(request.body);
const query = body.device_keys;
const masterKeys: Record<string, any> = {};
const selfSigningKeys: Record<string, any> = {};
if (ourUserId in query) {
resp.device_keys[ourUserId] = { [ourDeviceId]: ourDeviceKeys };
masterKeys[ourUserId] = ourCrossSigningKeys.master_key;
selfSigningKeys[ourUserId] = ourCrossSigningKeys.self_signing_key;
resp.user_signing_keys = {
[ourUserId]: ourCrossSigningKeys.user_signing_key,
};
}
if (theirUserId in query) {
resp.device_keys[theirUserId] = {
[theirDeviceId]: theirDeviceKeys,
};
masterKeys[theirUserId] = theirCrossSigningKeys.master_key;
selfSigningKeys[theirUserId] = theirCrossSigningKeys.self_signing_key;
}
if (Object.keys(masterKeys).length) {
resp.master_keys = masterKeys;
}
if (Object.keys(selfSigningKeys).length) {
resp.self_signing_keys = selfSigningKeys;
}
return resp;
} else if (request instanceof RustSdkCryptoJs.RoomMessageRequest) {
theirOlmMachine.receiveVerificationEvent(
JSON.stringify({
type: request.event_type,
sender: ourUserId,
event_id: "$" + request.event_type,
content: JSON.parse(request.body),
origin_server_ts: Date.now(),
unsigned: {
age: 0,
},
}),
new RustSdkCryptoJs.RoomId(request.room_id),
);
return { event_id: "$" + request.event_type };
} else if (request instanceof RustSdkCryptoJs.SignatureUploadRequest) {
// this only gets called at the end after the verification
// succeeds, so we don't actually have to do anything.
return { failures: {} };
}
return {};
}
async function makeOutgoingRequest(
request: OutgoingRequest | RustSdkCryptoJs.UploadSigningKeysRequest,
): Promise<any> {
const resp = (await customHandler?.(request)) ?? defaultHandler(request);
if (!(request instanceof RustSdkCryptoJs.UploadSigningKeysRequest) && request.id) {
await ourOlmMachine.markRequestAsSent(request.id!, request.type, JSON.stringify(resp));
}
}
async function runLoop() {
while (!stopRequestLoop) {
const requests = await ourOlmMachine.outgoingRequests();
for (const request of requests) {
await makeOutgoingRequest(request);
}
}
}
const loopCompletedPromise = runLoop();
return {
makeOutgoingRequest,
stop: async () => {
stopRequestLoop = true;
await loopCompletedPromise;
},
};
}

View File

@@ -34,6 +34,7 @@ import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProc
import { TypedReEmitter } from "../ReEmitter"; import { TypedReEmitter } from "../ReEmitter";
import { MatrixEvent } from "../models/event"; import { MatrixEvent } from "../models/event";
import { EventType, MsgType } from "../@types/event"; import { EventType, MsgType } from "../@types/event";
import { defer, IDeferred } from "../utils";
/** /**
* 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.
@@ -76,11 +77,16 @@ export class RustVerificationRequest
const onChange = async (): Promise<void> => { const onChange = async (): Promise<void> => {
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification(); 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, // Set the _verifier object (wrapping the rust `Verification` as a js-sdk Verifier) if:
// wrap the new rust Verification as a js-sdk Verifier. // - we now have a `Verification` where we lacked one before
// - we have transitioned from QR to SAS
// - we are verifying with SAS, but we need to replace our verifier with a new one because both parties
// tried to start verification at the same time, and we lost the tie breaking
if (verification instanceof RustSdkCryptoJs.Sas) { if (verification instanceof RustSdkCryptoJs.Sas) {
if (this._verifier === undefined || this._verifier instanceof RustQrCodeVerifier) { if (this._verifier === undefined || this._verifier instanceof RustQrCodeVerifier) {
this.setVerifier(new RustSASVerifier(verification, this, outgoingRequestProcessor)); this.setVerifier(new RustSASVerifier(verification, this, outgoingRequestProcessor));
} else if (this._verifier instanceof RustSASVerifier) {
this._verifier.replaceInner(verification);
} }
} else if (verification instanceof RustSdkCryptoJs.Qr && this._verifier === undefined) { } else if (verification instanceof RustSdkCryptoJs.Qr && this._verifier === undefined) {
this.setVerifier(new RustQrCodeVerifier(verification, outgoingRequestProcessor)); this.setVerifier(new RustQrCodeVerifier(verification, outgoingRequestProcessor));
@@ -456,46 +462,45 @@ abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCry
VerifierEvent | VerificationRequestEvent, VerifierEvent | VerificationRequestEvent,
VerifierEventHandlerMap & VerificationRequestEventHandlerMap VerifierEventHandlerMap & VerificationRequestEventHandlerMap
> { > {
/** A promise which completes when the verification completes (or rejects when it is cancelled/fails) */ /** A deferred which completes when the verification completes (or rejects when it is cancelled/fails) */
protected readonly completionPromise: Promise<void>; protected readonly completionDeferred: IDeferred<void>;
public constructor( public constructor(
protected readonly inner: InnerType, protected inner: InnerType,
protected readonly outgoingRequestProcessor: OutgoingRequestProcessor, protected readonly outgoingRequestProcessor: OutgoingRequestProcessor,
) { ) {
super(); super();
this.completionPromise = new Promise<void>((resolve, reject) => { this.completionDeferred = defer();
const onChange = async (): Promise<void> => { inner.registerChangesCallback(async () => {
this.onChange(); this.onChange();
if (this.inner.isDone()) {
resolve(undefined);
} else if (this.inner.isCancelled()) {
const cancelInfo = this.inner.cancelInfo()!;
reject(
new Error(
`Verification cancelled by ${
cancelInfo.cancelledbyUs() ? "us" : "them"
} with code ${cancelInfo.cancelCode()}: ${cancelInfo.reason()}`,
),
);
}
this.emit(VerificationRequestEvent.Change);
};
inner.registerChangesCallback(onChange);
}); });
// stop the runtime complaining if nobody catches a failure // stop the runtime complaining if nobody catches a failure
this.completionPromise.catch(() => null); this.completionDeferred.promise.catch(() => null);
} }
/** /**
* Hook which is called when the underlying rust class notifies us that there has been a change. * 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. * Can be overridden by subclasses to see if we can notify the application about an update. The overriding method
* must call `super.onChange()`.
*/ */
protected onChange(): void {} protected onChange(): void {
if (this.inner.isDone()) {
this.completionDeferred.resolve(undefined);
} else if (this.inner.isCancelled()) {
const cancelInfo = this.inner.cancelInfo()!;
this.completionDeferred.reject(
new Error(
`Verification cancelled by ${
cancelInfo.cancelledbyUs() ? "us" : "them"
} with code ${cancelInfo.cancelCode()}: ${cancelInfo.reason()}`,
),
);
}
this.emit(VerificationRequestEvent.Change);
}
/** /**
* Returns true if the verification has been cancelled, either by us or the other side. * Returns true if the verification has been cancelled, either by us or the other side.
@@ -565,6 +570,8 @@ export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> impl
cancel: () => this.cancel(), cancel: () => this.cancel(),
}; };
} }
super.onChange();
} }
/** /**
@@ -580,7 +587,7 @@ export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> impl
this.emit(VerifierEvent.ShowReciprocateQr, this.callbacks); this.emit(VerifierEvent.ShowReciprocateQr, this.callbacks);
} }
// Nothing to do here but wait. // Nothing to do here but wait.
await this.completionPromise; await this.completionDeferred.promise;
} }
/** /**
@@ -657,15 +664,24 @@ export class RustSASVerifier extends BaseRustVerifer<RustSdkCryptoJs.Sas> implem
* or times out. * or times out.
*/ */
public async verify(): Promise<void> { public async verify(): Promise<void> {
await this.sendAccept();
await this.completionDeferred.promise;
}
/**
* Send the accept or start event, if it hasn't already been sent
*/
private async sendAccept(): Promise<void> {
const req: undefined | OutgoingRequest = this.inner.accept(); const req: undefined | OutgoingRequest = this.inner.accept();
if (req) { if (req) {
await this.outgoingRequestProcessor.makeOutgoingRequest(req); await this.outgoingRequestProcessor.makeOutgoingRequest(req);
} }
await this.completionPromise;
} }
/** if we can now show the callbacks, do so */ /** if we can now show the callbacks, do so */
protected onChange(): void { protected onChange(): void {
super.onChange();
if (this.callbacks === null) { if (this.callbacks === null) {
const emoji = this.inner.emoji(); const emoji = this.inner.emoji();
const decimal = this.inner.decimals(); const decimal = this.inner.decimals();
@@ -717,6 +733,25 @@ export class RustSASVerifier extends BaseRustVerifer<RustSdkCryptoJs.Sas> implem
public getShowSasCallbacks(): ShowSasCallbacks | null { public getShowSasCallbacks(): ShowSasCallbacks | null {
return this.callbacks; return this.callbacks;
} }
/**
* Replace the inner Rust verifier with a different one.
*
* @param inner - the new Rust verifier
* @internal
*/
public replaceInner(inner: RustSdkCryptoJs.Sas): void {
if (this.inner != inner) {
this.inner = inner;
inner.registerChangesCallback(async () => {
this.onChange();
});
// replaceInner will only get called if we started the verification at the same time as the other side, and we lost
// the tie breaker. So we need to re-accept their verification.
this.sendAccept();
this.onChange();
}
}
} }
/** For each specced verification method, the rust-side `VerificationMethod` corresponding to it */ /** For each specced verification method, the rust-side `VerificationMethod` corresponding to it */