You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Pull out a new VerificationRequest interface (#3449)
* add a test for incoming verification requests * Move `VerificationRequestEvent` to crypto-api * Move `VerificationPhase` to `crypto-api` * Define `VerificationRequest` interface * Implement `canAcceptVerificationRequest`
This commit is contained in:
committed by
GitHub
parent
9f6073478f
commit
826ea5bc58
@@ -17,10 +17,10 @@ limitations under the License.
|
|||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
import { MockResponse } from "fetch-mock";
|
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 { ShowQrCodeCallbacks, ShowSasCallbacks, Verifier, VerifierEvent } from "../../../src/crypto-api/verification";
|
||||||
import { escapeRegExp } from "../../../src/utils";
|
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 { SyncResponder } from "../../test-utils/SyncResponder";
|
||||||
import {
|
import {
|
||||||
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
|
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
|
||||||
@@ -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 {
|
function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
|
||||||
ev.sender ??= TEST_USER_ID;
|
ev.sender ??= TEST_USER_ID;
|
||||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
|
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
|
||||||
|
|||||||
@@ -17,6 +17,187 @@ limitations under the License.
|
|||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
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
|
* A `Verifier` is responsible for performing the verification using a particular method, such as via QR code or SAS
|
||||||
* (emojis).
|
* (emojis).
|
||||||
@@ -169,3 +350,11 @@ export interface GeneratedSas {
|
|||||||
* English name.
|
* English name.
|
||||||
*/
|
*/
|
||||||
export type EmojiMapping = [emoji: string, name: string];
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ import { EventType } from "../../../@types/event";
|
|||||||
import { VerificationBase } from "../Base";
|
import { VerificationBase } from "../Base";
|
||||||
import { VerificationMethod } from "../../index";
|
import { VerificationMethod } from "../../index";
|
||||||
import { TypedEventEmitter } from "../../../models/typed-event-emitter";
|
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
|
// How long after the event's timestamp that the request times out
|
||||||
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
|
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
|
||||||
@@ -44,15 +54,6 @@ export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
|
|||||||
export const DONE_TYPE = EVENT_PREFIX + "done";
|
export const DONE_TYPE = EVENT_PREFIX + "done";
|
||||||
export const READY_TYPE = EVENT_PREFIX + "ready";
|
export const READY_TYPE = EVENT_PREFIX + "ready";
|
||||||
|
|
||||||
export enum Phase {
|
|
||||||
Unsent = 1,
|
|
||||||
Requested,
|
|
||||||
Ready,
|
|
||||||
Started,
|
|
||||||
Cancelled,
|
|
||||||
Done,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy export fields
|
// Legacy export fields
|
||||||
export const PHASE_UNSENT = Phase.Unsent;
|
export const PHASE_UNSENT = Phase.Unsent;
|
||||||
export const PHASE_REQUESTED = Phase.Requested;
|
export const PHASE_REQUESTED = Phase.Requested;
|
||||||
@@ -71,26 +72,17 @@ interface ITransition {
|
|||||||
event?: MatrixEvent;
|
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.
|
* State machine for verification requests.
|
||||||
* Things that differ based on what channel is used to
|
* Things that differ based on what channel is used to
|
||||||
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
|
* 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<
|
export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel>
|
||||||
VerificationRequestEvent,
|
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap>
|
||||||
EventHandlerMap
|
implements IVerificationRequest
|
||||||
> {
|
{
|
||||||
private eventsByUs = new Map<string, MatrixEvent>();
|
private eventsByUs = new Map<string, MatrixEvent>();
|
||||||
private eventsByThem = new Map<string, MatrixEvent>();
|
private eventsByThem = new Map<string, MatrixEvent>();
|
||||||
private _observeOnly = false;
|
private _observeOnly = false;
|
||||||
@@ -257,7 +249,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get canAccept(): boolean {
|
public get canAccept(): boolean {
|
||||||
return this.phase < PHASE_READY && !this._accepting && !this._declining;
|
return canAcceptVerificationRequest(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get accepting(): boolean {
|
public get accepting(): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user