From 8007bc5fe87c907803245b91c657eed586ae847c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 31 Jan 2024 15:07:34 -0500 Subject: [PATCH] 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> --- spec/unit/rust-crypto/verification.spec.ts | 508 ++++++++++++++++++++- src/rust-crypto/verification.ts | 95 ++-- 2 files changed, 571 insertions(+), 32 deletions(-) diff --git a/spec/unit/rust-crypto/verification.spec.ts b/spec/unit/rust-crypto/verification.spec.ts index 6bf3df1a026..1579ee6a876 100644 --- a/spec/unit/rust-crypto/verification.spec.ts +++ b/spec/unit/rust-crypto/verification.spec.ts @@ -17,8 +17,19 @@ limitations under the License. import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm"; import { Mocked } from "jest-mock"; -import { isVerificationEvent, RustVerificationRequest } from "../../../src/rust-crypto/verification"; -import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; +import { + 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"; 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 = 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 => { + // 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 = 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", () => { @@ -152,3 +511,148 @@ function makeMockedInner(): Mocked { }, } as unknown as Mocked; } + +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; + +/** 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 = { + device_keys: {}, + }; + const body = JSON.parse(request.body); + const query = body.device_keys; + const masterKeys: Record = {}; + const selfSigningKeys: Record = {}; + 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 { + 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; + }, + }; +} diff --git a/src/rust-crypto/verification.ts b/src/rust-crypto/verification.ts index a8b733c5fe0..dde54fd58f6 100644 --- a/src/rust-crypto/verification.ts +++ b/src/rust-crypto/verification.ts @@ -34,6 +34,7 @@ import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProc import { TypedReEmitter } from "../ReEmitter"; import { MatrixEvent } from "../models/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. @@ -76,11 +77,16 @@ export class RustVerificationRequest const onChange = async (): Promise => { 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. + // Set the _verifier object (wrapping the rust `Verification` as a js-sdk Verifier) if: + // - 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 (this._verifier === undefined || this._verifier instanceof RustQrCodeVerifier) { 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) { this.setVerifier(new RustQrCodeVerifier(verification, outgoingRequestProcessor)); @@ -456,46 +462,45 @@ abstract class BaseRustVerifer { - /** A promise which completes when the verification completes (or rejects when it is cancelled/fails) */ - protected readonly completionPromise: Promise; + /** A deferred which completes when the verification completes (or rejects when it is cancelled/fails) */ + protected readonly completionDeferred: IDeferred; public constructor( - protected readonly inner: InnerType, + protected inner: InnerType, protected readonly outgoingRequestProcessor: OutgoingRequestProcessor, ) { super(); - this.completionPromise = new Promise((resolve, reject) => { - const onChange = async (): Promise => { - 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); + this.completionDeferred = defer(); + inner.registerChangesCallback(async () => { + this.onChange(); }); // 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. * - * 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. @@ -565,6 +570,8 @@ export class RustQrCodeVerifier extends BaseRustVerifer impl cancel: () => this.cancel(), }; } + + super.onChange(); } /** @@ -580,7 +587,7 @@ export class RustQrCodeVerifier extends BaseRustVerifer impl this.emit(VerifierEvent.ShowReciprocateQr, this.callbacks); } // Nothing to do here but wait. - await this.completionPromise; + await this.completionDeferred.promise; } /** @@ -657,15 +664,24 @@ export class RustSASVerifier extends BaseRustVerifer implem * or times out. */ public async verify(): Promise { + await this.sendAccept(); + await this.completionDeferred.promise; + } + + /** + * Send the accept or start event, if it hasn't already been sent + */ + private async sendAccept(): Promise { 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 */ protected onChange(): void { + super.onChange(); + if (this.callbacks === null) { const emoji = this.inner.emoji(); const decimal = this.inner.decimals(); @@ -717,6 +733,25 @@ export class RustSASVerifier extends BaseRustVerifer implem public getShowSasCallbacks(): ShowSasCallbacks | null { 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 */