Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull out a new VerificationRequest interface #3449

Merged
merged 5 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions spec/integ/crypto/verification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ limitations under the License.
import fetchMock from "fetch-mock-jest";
import { MockResponse } from "fetch-mock";

import { createClient, MatrixClient } from "../../../src";
import { createClient, CryptoEvent, MatrixClient } from "../../../src";
import { ShowQrCodeCallbacks, ShowSasCallbacks, Verifier, VerifierEvent } from "../../../src/crypto-api/verification";
import { escapeRegExp } from "../../../src/utils";
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
import { CRYPTO_BACKENDS, emitPromise, InitCrypto } from "../../test-utils/test-utils";
import { SyncResponder } from "../../test-utils/SyncResponder";
import {
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
Expand Down Expand Up @@ -350,6 +350,49 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
},
);

oldBackendOnly("Incoming verification: can accept", async () => {
// expect requests to download our own keys
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
device_keys: {
[TEST_USER_ID]: {
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
},
},
});

const TRANSACTION_ID = "abcd";

// Initiate the request by sending a to-device message
returnToDeviceMessageFromSync({
type: "m.key.verification.request",
content: {
from_device: TEST_DEVICE_ID,
methods: ["m.sas.v1"],
transaction_id: TRANSACTION_ID,
timestamp: Date.now() - 1000,
},
});
const request: VerificationRequest = await emitPromise(aliceClient, CryptoEvent.VerificationRequest);
expect(request.transactionId).toEqual(TRANSACTION_ID);
expect(request.phase).toEqual(Phase.Requested);
expect(request.roomId).toBeUndefined();
expect(request.canAccept).toBe(true);

// Alice accepts, by sending a to-device message
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.ready");
const acceptPromise = request.accept();
expect(request.canAccept).toBe(false);
expect(request.phase).toEqual(Phase.Requested);
await acceptPromise;
const requestBody = await sendToDevicePromise;
expect(request.phase).toEqual(Phase.Ready);

const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.methods).toContain("m.sas.v1");
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(TRANSACTION_ID);
});

function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
ev.sender ??= TEST_USER_ID;
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
Expand Down
189 changes: 189 additions & 0 deletions src/crypto-api/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,187 @@ limitations under the License.
import { MatrixEvent } from "../models/event";
import { TypedEventEmitter } from "../models/typed-event-emitter";

/**
* An incoming, or outgoing, request to verify a user or a device via cross-signing.
*/
export interface VerificationRequest
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap> {
/**
* Unique ID for this verification request.
*
* An ID isn't assigned until the first message is sent, so this may be `undefined` in the early phases.
*/
get transactionId(): string | undefined;

/**
* For an in-room verification, the ID of the room.
*
* For to-device verifictions, `undefined`.
*/
get roomId(): string | undefined;

/**
* True if this request was initiated by the local client.
*
* For in-room verifications, the initiator is who sent the `m.key.verification.request` event.
* For to-device verifications, the initiator is who sent the `m.key.verification.start` event.
*/
get initiatedByMe(): boolean;

/** The user id of the other party in this request */
get otherUserId(): string;

/** For verifications via to-device messages: the ID of the other device. Otherwise, undefined. */
get otherDeviceId(): string | undefined;

/** True if the other party in this request is one of this user's own devices. */
get isSelfVerification(): boolean;

/** current phase of the request. */
get phase(): VerificationPhase;

/** True if the request has sent its initial event and needs more events to complete
* (ie it is in phase `Requested`, `Ready` or `Started`).
*/
get pending(): boolean;

/**
* True if we have started the process of sending an `m.key.verification.ready` (but have not necessarily received
* the remote echo which causes a transition to {@link VerificationPhase.Ready}.
*/
get accepting(): boolean;

/**
* True if we have started the process of sending an `m.key.verification.cancel` (but have not necessarily received
* the remote echo which causes a transition to {@link VerificationPhase.Cancelled}).
*/
get declining(): boolean;

/**
* The remaining number of ms before the request will be automatically cancelled.
*
* `null` indicates that there is no timeout
*/
get timeout(): number | null;

/** once the phase is Started (and !initiatedByMe) or Ready: common methods supported by both sides */
get methods(): string[];

/** the method picked in the .start event */
get chosenMethod(): string | null;

/**
* Checks whether the other party supports a given verification method.
* This is useful when setting up the QR code UI, as it is somewhat asymmetrical:
* if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa.
* For methods that need to be supported by both ends, use the `methods` property.
*
* @param method - the method to check
* @returns true if the other party said they supported the method
*/
otherPartySupportsMethod(method: string): boolean;

/**
* Accepts the request, sending a .ready event to the other party
*
* @returns Promise which resolves when the event has been sent.
*/
accept(): Promise<void>;

/**
* Cancels the request, sending a cancellation to the other party
*
* @param params - Details for the cancellation, including `reason` (defaults to "User declined"), and `code`
* (defaults to `m.user`).
*
* @returns Promise which resolves when the event has been sent.
*/
cancel(params?: { reason?: string; code?: string }): Promise<void>;

/**
* Create a {@link Verifier} to do this verification via a particular method.
*
* If a verifier has already been created for this request, returns that verifier.
*
* This does *not* send the `m.key.verification.start` event - to do so, call {@link Crypto.Verifier#verify} on the
* returned verifier.
*
* If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
*
* @param method - the name of the verification method to use.
* @param targetDevice - details of where to send the request to.
*
* @returns The verifier which will do the actual verification.
*/
beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier;

/**
* The verifier which is doing the actual verification, once the method has been established.
* Only defined when the `phase` is Started.
*/
get verifier(): Verifier | undefined;

/**
* Get the data for a QR code allowing the other device to verify this one, if it supports it.
*
* Only set after a .ready if the other party can scan a QR code, otherwise undefined.
*/
getQRCodeBytes(): Buffer | undefined;

/**
* If this request has been cancelled, the cancellation code (e.g `m.user`) which is responsible for cancelling
* this verification.
*/
get cancellationCode(): string | null;

/**
* The id of the user that cancelled the request.
*
* Only defined when phase is Cancelled
*/
get cancellingUserId(): string | undefined;
}

/** Events emitted by {@link VerificationRequest}. */
export enum VerificationRequestEvent {
/**
* Fires whenever the state of the request object has changed.
*
* There is no payload to the event.
*/
Change = "change",
}

/**
* Listener type map for {@link VerificationRequestEvent}s.
*
* @internal
*/
export type VerificationRequestEventHandlerMap = {
[VerificationRequestEvent.Change]: () => void;
};

/** The current phase of a verification request. */
export enum VerificationPhase {
/** Initial state: no event yet exchanged */
Unsent = 1,

/** An `m.key.verification.request` event has been sent or received */
Requested,

/** An `m.key.verification.ready` event has been sent or received, indicating the verification request is accepted. */
Ready,

/** An `m.key.verification.start` event has been sent or received, choosing a verification method */
Started,

/** An `m.key.verification.cancel` event has been sent or received at any time before the `done` event, cancelling the verification request */
Cancelled,

/** An `m.key.verification.done` event has been **sent**, completing the verification request. */
Done,
}

/**
* A `Verifier` is responsible for performing the verification using a particular method, such as via QR code or SAS
* (emojis).
Expand Down Expand Up @@ -169,3 +350,11 @@ export interface GeneratedSas {
* English name.
*/
export type EmojiMapping = [emoji: string, name: string];

/**
* True if the request is in a state where it can be accepted (ie, that we're in phases {@link VerificationPhase.Unsent}
* or {@link VerificationPhase.Requested}, and that we're not in the process of sending a `ready` or `cancel`).
*/
export function canAcceptVerificationRequest(req: VerificationRequest): boolean {
return req.phase < VerificationPhase.Ready && !req.accepting && !req.declining;
}
42 changes: 17 additions & 25 deletions src/crypto/verification/request/VerificationRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ import { EventType } from "../../../@types/event";
import { VerificationBase } from "../Base";
import { VerificationMethod } from "../../index";
import { TypedEventEmitter } from "../../../models/typed-event-emitter";
import {
canAcceptVerificationRequest,
VerificationPhase as Phase,
VerificationRequest as IVerificationRequest,
VerificationRequestEvent,
VerificationRequestEventHandlerMap,
} from "../../../crypto-api/verification";

// backwards-compatibility exports
export { VerificationPhase as Phase, VerificationRequestEvent } from "../../../crypto-api/verification";

// How long after the event's timestamp that the request times out
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
Expand All @@ -44,15 +54,6 @@ export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
export const DONE_TYPE = EVENT_PREFIX + "done";
export const READY_TYPE = EVENT_PREFIX + "ready";

export enum Phase {
Unsent = 1,
Requested,
Ready,
Started,
Cancelled,
Done,
}

// Legacy export fields
export const PHASE_UNSENT = Phase.Unsent;
export const PHASE_REQUESTED = Phase.Requested;
Expand All @@ -71,26 +72,17 @@ interface ITransition {
event?: MatrixEvent;
}

export enum VerificationRequestEvent {
Change = "change",
}

type EventHandlerMap = {
/**
* Fires whenever the state of the request object has changed.
*/
[VerificationRequestEvent.Change]: () => void;
};

/**
* State machine for verification requests.
* Things that differ based on what channel is used to
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
*
* @deprecated Avoid direct references: instead prefer {@link Crypto.VerificationRequest}.
*/
export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends TypedEventEmitter<
VerificationRequestEvent,
EventHandlerMap
> {
export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel>
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap>
implements IVerificationRequest
{
private eventsByUs = new Map<string, MatrixEvent>();
private eventsByThem = new Map<string, MatrixEvent>();
private _observeOnly = false;
Expand Down Expand Up @@ -257,7 +249,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
}

public get canAccept(): boolean {
return this.phase < PHASE_READY && !this._accepting && !this._declining;
return canAcceptVerificationRequest(this);
}

public get accepting(): boolean {
Expand Down