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

ElementR: Add CryptoApi.requestVerificationDM #3643

Merged
merged 11 commits into from
Aug 21, 2023
115 changes: 113 additions & 2 deletions spec/integ/crypto/verification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import fetchMock from "fetch-mock-jest";
import { IDBFactory } from "fake-indexeddb";
import { createHash } from "crypto";

import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src";
import { createClient, CryptoEvent, IContent, ICreateClientOpts, IDownloadKeyResult, MatrixClient } from "../../../src";
import {
canAcceptVerificationRequest,
ShowQrCodeCallbacks,
Expand All @@ -34,19 +34,21 @@ import {
VerifierEvent,
} from "../../../src/crypto-api/verification";
import { escapeRegExp } from "../../../src/utils";
import { CRYPTO_BACKENDS, emitPromise, InitCrypto } from "../../test-utils/test-utils";
import { CRYPTO_BACKENDS, emitPromise, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { SyncResponder } from "../../test-utils/SyncResponder";
import {
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
SIGNED_CROSS_SIGNING_KEYS_DATA,
SIGNED_TEST_DEVICE_DATA,
TEST_DEVICE_ID,
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
TEST_ROOM_ID,
TEST_USER_ID,
} from "../../test-utils/test-data";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { IDeviceKeys } from "../../../src/@types/crypto";

// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
// to ensure that we don't end up with dangling timeouts.
Expand Down Expand Up @@ -808,6 +810,115 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
});
});

describe("Send verification request in DM", () => {
const crossSingingKeys: Partial<IDownloadKeyResult> = {
florianduros marked this conversation as resolved.
Show resolved Hide resolved
master_keys: {
"@bob:xyz": {
keys: {
"ed25519:i2y3s50NUzxxtIdyxEc9yFybLIq6qgBSb9r7k9yICBY":
"i2y3s50NUzxxtIdyxEc9yFybLIq6qgBSb9r7k9yICBY",
},
user_id: "@bob:xyz",
usage: ["master"],
},
},
self_signing_keys: {
"@bob:xyz": {
keys: {
"ed25519:cyTNi7Z7LZHlV1qnirzRypdsIWNLAH/z7Jc9PAIkf1g":
"cyTNi7Z7LZHlV1qnirzRypdsIWNLAH/z7Jc9PAIkf1g",
},
user_id: "@bob:xyz",
usage: ["self_signing"],
signatures: {
"@bob:xyz": {
"ed25519:i2y3s50NUzxxtIdyxEc9yFybLIq6qgBSb9r7k9yICBY":
"SrAMqvGSkwzxXoLZAcmuMl+gbQBlHbWObY1IX1wZ9ADlOJkeEzzjxzsyah25I29TLFHrTG+dFphreItE3pGQAA",
},
},
},
},
user_signing_keys: {
"@bob:xyz": {
keys: {
"ed25519:mdF9bCRhssc1OTWqsmSdS4usif2PboaT/cAJMNqykbA":
"mdF9bCRhssc1OTWqsmSdS4usif2PboaT/cAJMNqykbA",
},
user_id: "@bob:xyz",
usage: ["user_signing"],
signatures: {
"@bob:xyz": {
"ed25519:i2y3s50NUzxxtIdyxEc9yFybLIq6qgBSb9r7k9yICBY":
"7DoXVLc4iNvQQOSR9G9QQEot93M7CM76z0fUtraIo/hnMH1/M96DIskLl9MBONOL5nEekw2LXXFWh55AANMMCg",
},
},
},
},
};

const deviceKeys: IDeviceKeys = {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "bob_device",
keys: {
"curve25519:bob_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
"ed25519:bob_device": "PNsWknI+bXfeeV7GFfnuBX3sATD+/riD0gvvv8Ynd5s",
},
user_id: "@bob:xyz",
signatures: {
"@bob:xyz": {
"ed25519:bob_device":
"T+UUApQlgZHNfM4gdkcrJcjTmeF7wEYAZH2OS99gKKizEgEFL89acAC7yuLfm2MI5J9Ks2AGkQJp3jjNjC/kBA",
},
},
};
florianduros marked this conversation as resolved.
Show resolved Hide resolved

const bobId = "@bob:xyz";

beforeEach(async () => {
aliceClient = await startTestClient();

e2eKeyResponder.addCrossSigningData(crossSingingKeys);
e2eKeyResponder.addDeviceKeys(deviceKeys);

syncResponder.sendOrQueueSyncResponse(getSyncResponse([bobId]));

// Wait for the sync response to be processed
await syncPromise(aliceClient);
});

function awaitRoomMessageRequest(): Promise<IContent> {
return new Promise((resolve) => {
fetchMock.put(
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId",
(url: string, options: RequestInit) => {
resolve(JSON.parse(options.body as string));
return { event_id: "$YUwRidLecu:example.com" };
},
);
});
}

newBackendOnly("alice send verification request in a DM to bob", async () => {
florianduros marked this conversation as resolved.
Show resolved Hide resolved
const messageRequestPromise = awaitRoomMessageRequest();
const verificationRequest = await aliceClient.getCrypto()!.requestVerificationDM(bobId, TEST_ROOM_ID);
const requestContent = await messageRequestPromise;

expect(requestContent.from_device).toBe(aliceClient.getDeviceId());
expect(requestContent.methods).toStrictEqual([
"m.sas.v1",
"m.qr_code.scan.v1",
"m.qr_code.show.v1",
"m.reciprocate.v1",
]);
expect(requestContent.msgtype).toBe("m.key.verification.request");
expect(requestContent.to).toBe(bobId);

expect(verificationRequest.roomId).toBe(TEST_ROOM_ID);
expect(verificationRequest.isSelfVerification).toBe(false);
expect(verificationRequest.otherUserId).toBe(bobId);
});
});

async function startTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
const client = createClient({
baseUrl: TEST_HOMESERVER_URL,
Expand Down
2 changes: 1 addition & 1 deletion spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ describe("OutgoingRequestProcessor", () => {
.when("PUT", "/_matrix")
.check((req) => {
expect(req.path).toEqual(
"https://example.com/_matrix/client/v3/room/test%2Froom/send/test%2Ftype/test%2Ftxnid",
"https://example.com/_matrix/client/v3/rooms/test%2Froom/send/test%2Ftype/test%2Ftxnid",
);
expect(req.rawData).toEqual(testBody);
expect(req.headers["Accept"]).toEqual("application/json");
Expand Down
9 changes: 9 additions & 0 deletions spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,15 @@ describe("RustCrypto", () => {
);
});
});

describe("requestVerificationDM", () => {
it("send verification request to an unknown user", async () => {
const rustCrypto = await makeTestRustCrypto();
await expect(() =>
rustCrypto.requestVerificationDM("@bob:example.com", testData.TEST_ROOM_ID),
).rejects.toThrow("unknown userId @bob:example.com");
});
});
});

/** build a basic RustCrypto instance for testing
Expand Down
2 changes: 2 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2443,6 +2443,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @returns resolves to a VerificationRequest
* when the request has been sent to the other party.
*
* @deprecated Prefer {@link CryptoApi.requestVerificationDM}.
*/
public requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
if (!this.crypto) {
Expand Down
10 changes: 10 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,16 @@ export interface CryptoApi {
*/
findVerificationRequestDMInProgress(roomId: string, userId?: string): VerificationRequest | undefined;

/**
* Request a key verification from another user, using a DM.
*
* @param userId - the user to request verification with.
* @param roomId - the room to use for verification.
*
* @returns resolves to a VerificationRequest when the request has been sent to the other party.
*/
requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest>;

/**
* Send a verification request to our other devices.
*
Expand Down
2 changes: 1 addition & 1 deletion src/rust-crypto/OutgoingRequestProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class OutgoingRequestProcessor {
resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
} else if (msg instanceof RoomMessageRequest) {
const path =
`/_matrix/client/v3/room/${encodeURIComponent(msg.room_id)}/send/` +
`/_matrix/client/v3/rooms/${encodeURIComponent(msg.room_id)}/send/` +
`${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`;
resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
} else if (msg instanceof SigningKeysUploadRequest) {
Expand Down
61 changes: 60 additions & 1 deletion src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,17 @@ import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
import { keyFromPassphrase } from "../crypto/key_passphrase";
import { encodeRecoveryKey } from "../crypto/recoverykey";
import { crypto } from "../crypto/crypto";
import { RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification";
import {
RustVerificationRequest,
verificationMethodIdentifierToMethod,
verificationMethodsByIdentifier,
} from "./verification";
import { EventType, MsgType } from "../@types/event";
import { CryptoEvent } from "../crypto";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } from "./backup";
import { TypedReEmitter } from "../ReEmitter";
import { randomString } from "../randomstring";

const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];

Expand Down Expand Up @@ -709,6 +714,60 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
}
}

/**
* Implementation of {@link CryptoApi#requestVerificationDM}
*/
public async requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
const userIdentity: RustSdkCryptoJs.UserIdentity | undefined = await this.olmMachine.getIdentity(
new RustSdkCryptoJs.UserId(userId),
);

if (!userIdentity) throw new Error(`unknown userId ${userId}`);

// Transform the verification methods into rust objects
const methods = this._supportedVerificationMethods.map((method) => verificationMethodsByIdentifier[method]);
// Get the request content to send to the DM room
const verificationEventContent: string = await userIdentity.verificationRequestContent(methods);

// Send the request content to send to the DM room
const eventId = await this.sendVerificationRequestContent(roomId, verificationEventContent);

// Get a verification request
const request: RustSdkCryptoJs.VerificationRequest = await userIdentity.requestVerification(
new RustSdkCryptoJs.RoomId(roomId),
new RustSdkCryptoJs.EventId(eventId),
methods,
);
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
}

/**
* Send the verification content to a room
* See https://spec.matrix.org/v1.7/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
*
* Prefer to use {@link OutgoingRequestProcessor.makeOutgoingRequest} when dealing with {@link RustSdkCryptoJs.RoomMessageRequest}
*
* @param roomId - the targeted room
* @param verificationEventContent - the request body.
*
* @returns the event id
*/
private async sendVerificationRequestContent(roomId: string, verificationEventContent: string): Promise<string> {
const txId = randomString(32);
// Send the verification request content to the DM room
const { event_id: eventId } = await this.http.authedRequest<{ event_id: string }>(
Method.Put,
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${encodeURIComponent(txId)}`,
undefined,
verificationEventContent,
{
prefix: "",
},
);

return eventId;
}

/**
* The verification methods we offer to the other side during an interactive verification.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/rust-crypto/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ export class RustSASVerifier extends BaseRustVerifer<RustSdkCryptoJs.Sas> implem
}

/** For each specced verification method, the rust-side `VerificationMethod` corresponding to it */
const verificationMethodsByIdentifier: Record<string, RustSdkCryptoJs.VerificationMethod> = {
export const verificationMethodsByIdentifier: Record<string, RustSdkCryptoJs.VerificationMethod> = {
florianduros marked this conversation as resolved.
Show resolved Hide resolved
"m.sas.v1": RustSdkCryptoJs.VerificationMethod.SasV1,
"m.qr_code.scan.v1": RustSdkCryptoJs.VerificationMethod.QrCodeScanV1,
"m.qr_code.show.v1": RustSdkCryptoJs.VerificationMethod.QrCodeShowV1,
Expand Down