Skip to content

Commit

Permalink
Add CryptoApi.requestVerificationDM
Browse files Browse the repository at this point in the history
  • Loading branch information
florianduros committed Jul 31, 2023
1 parent 0ada980 commit 346a298
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 4 deletions.
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> = {
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",
},
},
};

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/room/: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 () => {
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
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 @@ -583,6 +583,15 @@ describe("RustCrypto", () => {
expect(rustCrypto.findVerificationRequestDMInProgress(testData.TEST_ROOM_ID)).not.toBeDefined();
});
});

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 @@ -2441,6 +2441,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
61 changes: 60 additions & 1 deletion src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ 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 { randomString } from "../randomstring";
import { RustBackupManager } from "./backup";

const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
Expand Down Expand Up @@ -705,6 +710,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/room/${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> = {
"m.sas.v1": RustSdkCryptoJs.VerificationMethod.SasV1,
"m.qr_code.scan.v1": RustSdkCryptoJs.VerificationMethod.QrCodeScanV1,
"m.qr_code.show.v1": RustSdkCryptoJs.VerificationMethod.QrCodeShowV1,
Expand Down

0 comments on commit 346a298

Please sign in to comment.