From 346a298fb3a3702a2b8a8a1b51478ee9060ace9b Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 31 Jul 2023 17:57:27 +0200 Subject: [PATCH 1/7] Add `CryptoApi.requestVerificationDM` --- spec/integ/crypto/verification.spec.ts | 115 +++++++++++++++++++++- spec/unit/rust-crypto/rust-crypto.spec.ts | 9 ++ src/client.ts | 2 + src/crypto-api.ts | 10 ++ src/rust-crypto/rust-crypto.ts | 61 +++++++++++- src/rust-crypto/verification.ts | 2 +- 6 files changed, 195 insertions(+), 4 deletions(-) diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index b9fd38143d8..e2f85f61de0 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -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, @@ -34,7 +34,7 @@ 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, @@ -42,11 +42,13 @@ import { 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. @@ -808,6 +810,115 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st }); }); + describe("Send verification request in DM", () => { + const crossSingingKeys: Partial = { + 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 { + 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 = {}): Promise { const client = createClient({ baseUrl: TEST_HOMESERVER_URL, diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index c21e34531f2..0cd3fb86f76 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -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 diff --git a/src/client.ts b/src/client.ts index ea17dfb8395..dd11a800e37 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2441,6 +2441,8 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { diff --git a/src/crypto-api.ts b/src/crypto-api.ts index 78eb0c9fd05..12b0213025c 100644 --- a/src/crypto-api.ts +++ b/src/crypto-api.ts @@ -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; + /** * Send a verification request to our other devices. * diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 8bed4c9e9a0..da6ff47f926 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -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"]; @@ -705,6 +710,60 @@ export class RustCrypto extends TypedEventEmitter { + 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 { + 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. */ diff --git a/src/rust-crypto/verification.ts b/src/rust-crypto/verification.ts index 252189fca91..1da0c3ad0e7 100644 --- a/src/rust-crypto/verification.ts +++ b/src/rust-crypto/verification.ts @@ -677,7 +677,7 @@ export class RustSASVerifier extends BaseRustVerifer implem } /** For each specced verification method, the rust-side `VerificationMethod` corresponding to it */ -const verificationMethodsByIdentifier: Record = { +export const verificationMethodsByIdentifier: Record = { "m.sas.v1": RustSdkCryptoJs.VerificationMethod.SasV1, "m.qr_code.scan.v1": RustSdkCryptoJs.VerificationMethod.QrCodeScanV1, "m.qr_code.show.v1": RustSdkCryptoJs.VerificationMethod.QrCodeShowV1, From 81570979726495ac497b43d2adaad912f258122b Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 3 Aug 2023 17:00:12 +0200 Subject: [PATCH 2/7] Fix RoomMessageRequest url --- spec/integ/crypto/verification.spec.ts | 2 +- spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts | 2 +- src/rust-crypto/OutgoingRequestProcessor.ts | 2 +- src/rust-crypto/rust-crypto.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index e2f85f61de0..a24d030cd44 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -889,7 +889,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st function awaitRoomMessageRequest(): Promise { return new Promise((resolve) => { fetchMock.put( - "express:/_matrix/client/v3/room/:roomId/send/m.room.message/:txId", + "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" }; diff --git a/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts b/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts index 94ce93b7ef1..00b7f6148ad 100644 --- a/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts +++ b/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts @@ -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"); diff --git a/src/rust-crypto/OutgoingRequestProcessor.ts b/src/rust-crypto/OutgoingRequestProcessor.ts index c67bcd7c298..7ad8e365d42 100644 --- a/src/rust-crypto/OutgoingRequestProcessor.ts +++ b/src/rust-crypto/OutgoingRequestProcessor.ts @@ -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) { diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index e46640b7078..447dfe37138 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -752,7 +752,7 @@ export class RustCrypto extends TypedEventEmitter( Method.Put, - `/_matrix/client/v3/room/${encodeURIComponent(roomId)}/send/m.room.message/${encodeURIComponent(txId)}`, + `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${encodeURIComponent(txId)}`, undefined, verificationEventContent, { From e4e9b695a60c899db02281376fe520f45d4a9137 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 10 Aug 2023 16:30:35 +0200 Subject: [PATCH 3/7] Review changes --- spec/integ/crypto/verification.spec.ts | 6 +++--- src/rust-crypto/verification.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index a24d030cd44..e4a202ed329 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -811,7 +811,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st }); describe("Send verification request in DM", () => { - const crossSingingKeys: Partial = { + const crossSigningKeys: Partial = { master_keys: { "@bob:xyz": { keys: { @@ -877,7 +877,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st beforeEach(async () => { aliceClient = await startTestClient(); - e2eKeyResponder.addCrossSigningData(crossSingingKeys); + e2eKeyResponder.addCrossSigningData(crossSigningKeys); e2eKeyResponder.addDeviceKeys(deviceKeys); syncResponder.sendOrQueueSyncResponse(getSyncResponse([bobId])); @@ -898,7 +898,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st }); } - newBackendOnly("alice send verification request in a DM to bob", async () => { + newBackendOnly("alice sends a 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; diff --git a/src/rust-crypto/verification.ts b/src/rust-crypto/verification.ts index 1da0c3ad0e7..252189fca91 100644 --- a/src/rust-crypto/verification.ts +++ b/src/rust-crypto/verification.ts @@ -677,7 +677,7 @@ export class RustSASVerifier extends BaseRustVerifer implem } /** For each specced verification method, the rust-side `VerificationMethod` corresponding to it */ -export const verificationMethodsByIdentifier: Record = { +const verificationMethodsByIdentifier: Record = { "m.sas.v1": RustSdkCryptoJs.VerificationMethod.SasV1, "m.qr_code.scan.v1": RustSdkCryptoJs.VerificationMethod.QrCodeScanV1, "m.qr_code.show.v1": RustSdkCryptoJs.VerificationMethod.QrCodeShowV1, From a233ebb4cbe044f30f7e283e5567afc3a178c02a Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 14 Aug 2023 11:57:55 +0200 Subject: [PATCH 4/7] Merge fixes --- src/rust-crypto/rust-crypto.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 0f1a3b5c343..82cd9310211 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -55,11 +55,7 @@ import { secretStorageContainsCrossSigningKeys } from "./secret-storage"; import { keyFromPassphrase } from "../crypto/key_passphrase"; import { encodeRecoveryKey } from "../crypto/recoverykey"; import { crypto } from "../crypto/crypto"; -import { - RustVerificationRequest, - verificationMethodIdentifierToMethod, - verificationMethodsByIdentifier -} from "./verification"; +import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification"; import { EventType } from "../@types/event"; import { CryptoEvent } from "../crypto"; import { TypedEventEmitter } from "../models/typed-event-emitter"; @@ -725,7 +721,9 @@ export class RustCrypto extends TypedEventEmitter verificationMethodsByIdentifier[method]); + const methods = this._supportedVerificationMethods.map((method) => + verificationMethodIdentifierToMethod(method), + ); // Get the request content to send to the DM room const verificationEventContent: string = await userIdentity.verificationRequestContent(methods); From 4af3b635366b5d9d0965c31d70ffab6e57fa1098 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 14 Aug 2023 17:24:45 +0200 Subject: [PATCH 5/7] Add BOB test data --- spec/integ/crypto/verification.spec.ts | 83 ++------- .../test-data/generate-test-data.py | 158 +++++++++++------- spec/test-utils/test-data/index.ts | 115 ++++++++++++- 3 files changed, 225 insertions(+), 131 deletions(-) diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index e4a202ed329..19b076e65ac 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -22,7 +22,7 @@ import fetchMock from "fetch-mock-jest"; import { IDBFactory } from "fake-indexeddb"; import { createHash } from "crypto"; -import { createClient, CryptoEvent, IContent, ICreateClientOpts, IDownloadKeyResult, MatrixClient } from "../../../src"; +import { createClient, CryptoEvent, IContent, ICreateClientOpts, MatrixClient } from "../../../src"; import { canAcceptVerificationRequest, ShowQrCodeCallbacks, @@ -37,6 +37,9 @@ import { escapeRegExp } from "../../../src/utils"; import { CRYPTO_BACKENDS, emitPromise, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils"; import { SyncResponder } from "../../test-utils/SyncResponder"; import { + BOB_SIGNED_CROSS_SIGNING_KEYS_DATA, + BOB_SIGNED_TEST_DEVICE_DATA, + BOB_TEST_USER_ID, MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, SIGNED_CROSS_SIGNING_KEYS_DATA, SIGNED_TEST_DEVICE_DATA, @@ -48,7 +51,6 @@ import { 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. @@ -811,76 +813,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st }); describe("Send verification request in DM", () => { - const crossSigningKeys: Partial = { - 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(crossSigningKeys); - e2eKeyResponder.addDeviceKeys(deviceKeys); + e2eKeyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA); + e2eKeyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA); - syncResponder.sendOrQueueSyncResponse(getSyncResponse([bobId])); + syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID])); // Wait for the sync response to be processed await syncPromise(aliceClient); @@ -900,7 +839,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st newBackendOnly("alice sends a verification request in a DM to bob", async () => { const messageRequestPromise = awaitRoomMessageRequest(); - const verificationRequest = await aliceClient.getCrypto()!.requestVerificationDM(bobId, TEST_ROOM_ID); + const verificationRequest = await aliceClient + .getCrypto()! + .requestVerificationDM(BOB_TEST_USER_ID, TEST_ROOM_ID); const requestContent = await messageRequestPromise; expect(requestContent.from_device).toBe(aliceClient.getDeviceId()); @@ -911,11 +852,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st "m.reciprocate.v1", ]); expect(requestContent.msgtype).toBe("m.key.verification.request"); - expect(requestContent.to).toBe(bobId); + expect(requestContent.to).toBe(BOB_TEST_USER_ID); expect(verificationRequest.roomId).toBe(TEST_ROOM_ID); expect(verificationRequest.isSelfVerification).toBe(false); - expect(verificationRequest.otherUserId).toBe(bobId); + expect(verificationRequest.otherUserId).toBe(BOB_TEST_USER_ID); }); }); diff --git a/spec/test-utils/test-data/generate-test-data.py b/spec/test-utils/test-data/generate-test-data.py index 9bc63dbd0ea..b9f07b7aed4 100755 --- a/spec/test-utils/test-data/generate-test-data.py +++ b/spec/test-utils/test-data/generate-test-data.py @@ -31,6 +31,36 @@ from cryptography.hazmat.primitives.asymmetric import ed25519, x25519 from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat +ALICE_DATA = { + "TEST_USER_ID": "@alice:localhost", + "TEST_DEVICE_ID": "test_device", + "TEST_ROOM_ID": "!room:id", + # any 32-byte string can be an ed25519 private key. + "TEST_DEVICE_PRIVATE_KEY_BYTES": b"deadbeefdeadbeefdeadbeefdeadbeef", + + "MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"doyouspeakwhaaaaaaaaaaaaaaaaaale", + "USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"useruseruseruseruseruseruseruser", + "SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"selfselfselfselfselfselfselfself", + + # Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts + "B64_BACKUP_DECRYPTION_KEY": "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=" +} + +BOB_DATA = { + "TEST_USER_ID": "@bob:xyz", + "TEST_DEVICE_ID": "bob_device", + "TEST_ROOM_ID": "!room:id", + # any 32-byte string can be an ed25519 private key. + "TEST_DEVICE_PRIVATE_KEY_BYTES": b"Deadbeefdeadbeefdeadbeefdeadbeef", + + "MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale", + "USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser", + "SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself", + + # Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts + "B64_BACKUP_DECRYPTION_KEY": "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=" +} + # input data TEST_USER_ID = "@alice:localhost" TEST_DEVICE_ID = "test_device" @@ -46,58 +76,82 @@ B64_BACKUP_DECRYPTION_KEY = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=" def main() -> None: - private_key = ed25519.Ed25519PrivateKey.from_private_bytes( - TEST_DEVICE_PRIVATE_KEY_BYTES + print( + f"""\ +/* Test data for cryptography tests + * + * Do not edit by hand! This file is generated by `./generate-test-data.py` + */ + +import {{ IDeviceKeys }} from "../../../src/@types/crypto"; +import {{ IDownloadKeyResult }} from "../../../src"; +import {{ KeyBackupInfo }} from "../../../src/crypto-api"; + +/* eslint-disable comma-dangle */ + +// Alice data + +{build_test_data(ALICE_DATA)} +// Bob data + +{build_test_data(BOB_DATA, "BOB_")} +""", + end="", ) + +def build_test_data(user_data, prefix = "") -> str: + private_key = ed25519.Ed25519PrivateKey.from_private_bytes( + user_data["TEST_DEVICE_PRIVATE_KEY_BYTES"] + ) b64_public_key = encode_base64( private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) ) device_data = { "algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"], - "device_id": TEST_DEVICE_ID, + "device_id": user_data["TEST_DEVICE_ID"], "keys": { - f"curve25519:{TEST_DEVICE_ID}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0", - f"ed25519:{TEST_DEVICE_ID}": b64_public_key, + f"curve25519:{user_data['TEST_DEVICE_ID']}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0", + f"ed25519:{user_data['TEST_DEVICE_ID']}": b64_public_key, }, - "signatures": {TEST_USER_ID: {}}, - "user_id": TEST_USER_ID, + "signatures": {user_data['TEST_USER_ID']: {}}, + "user_id": user_data["TEST_USER_ID"], } - device_data["signatures"][TEST_USER_ID][f"ed25519:{TEST_DEVICE_ID}"] = sign_json( + device_data["signatures"][user_data["TEST_USER_ID"]][f"ed25519:{user_data['TEST_DEVICE_ID']}"] = sign_json( device_data, private_key ) master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( - MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES + user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"] ) b64_master_public_key = encode_base64( master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) ) - b64_master_private_key = encode_base64(MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES) + b64_master_private_key = encode_base64(user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]) self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( - SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES + user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"] ) b64_self_signing_public_key = encode_base64( self_signing_private_key.public_key().public_bytes( Encoding.Raw, PublicFormat.Raw ) ) - b64_self_signing_private_key = encode_base64(SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES) + b64_self_signing_private_key = encode_base64( user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"]) user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( - USER_CROSS_SIGNING_PRIVATE_KEY_BYTES + user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"] ) b64_user_signing_public_key = encode_base64( user_signing_private_key.public_key().public_bytes( Encoding.Raw, PublicFormat.Raw ) ) - b64_user_signing_private_key = encode_base64(USER_CROSS_SIGNING_PRIVATE_KEY_BYTES) + b64_user_signing_private_key = encode_base64(user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]) backup_decryption_key = x25519.X25519PrivateKey.from_private_bytes( - base64.b64decode(B64_BACKUP_DECRYPTION_KEY) + base64.b64decode(user_data["B64_BACKUP_DECRYPTION_KEY"]) ) b64_backup_public_key = encode_base64( backup_decryption_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) @@ -113,75 +167,61 @@ def main() -> None: # sign with our device key sig = sign_json(backup_data["auth_data"], private_key) backup_data["auth_data"]["signatures"] = { - TEST_USER_ID: {f"ed25519:{TEST_DEVICE_ID}": sig} + user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": sig} } - print( - f"""\ -/* Test data for cryptography tests - * - * Do not edit by hand! This file is generated by `./generate-test-data.py` - */ - -import {{ IDeviceKeys }} from "../../../src/@types/crypto"; -import {{ IDownloadKeyResult }} from "../../../src"; -import {{ KeyBackupInfo }} from "../../../src/crypto-api"; - -/* eslint-disable comma-dangle */ - -export const TEST_USER_ID = "{TEST_USER_ID}"; -export const TEST_DEVICE_ID = "{TEST_DEVICE_ID}"; -export const TEST_ROOM_ID = "{TEST_ROOM_ID}"; + return f"""\ +export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}"; +export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}"; +export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}"; /** The base64-encoded public ed25519 key for this device */ -export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}"; +export const {prefix}TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}"; /** Signed device data, suitable for returning from a `/keys/query` call */ -export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)}; +export const {prefix}SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)}; /** base64-encoded public master cross-signing key */ -export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}"; +export const {prefix}MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}"; /** base64-encoded private master cross-signing key */ -export const MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_master_private_key}"; +export const {prefix}MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_master_private_key}"; /** base64-encoded public self cross-signing key */ -export const SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_self_signing_public_key}"; +export const {prefix}SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_self_signing_public_key}"; /** base64-encoded private self signing cross-signing key */ -export const SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_self_signing_private_key}"; +export const {prefix}SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_self_signing_private_key}"; /** base64-encoded public user cross-signing key */ -export const USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_user_signing_public_key}"; +export const {prefix}USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_user_signing_public_key}"; /** base64-encoded private user signing cross-signing key */ -export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_key}"; +export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_key}"; /** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */ -export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial = { - json.dumps(build_cross_signing_keys_data(), indent=4) +export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial = { + json.dumps(build_cross_signing_keys_data(user_data), indent=4) }; /** base64-encoded backup decryption (private) key */ -export const BACKUP_DECRYPTION_KEY_BASE64 = "{ B64_BACKUP_DECRYPTION_KEY }"; +export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }"; /** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */ -export const SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) }; -""", - end="", - ) +export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) }; +""" -def build_cross_signing_keys_data() -> dict: +def build_cross_signing_keys_data(user_data) -> dict: """Build the signed cross-signing-keys data for return from /keys/query""" master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( - MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES + user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"] ) b64_master_public_key = encode_base64( master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) ) self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( - SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES + user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"] ) b64_self_signing_public_key = encode_base64( self_signing_private_key.public_key().public_bytes( @@ -189,7 +229,7 @@ def build_cross_signing_keys_data() -> dict: ) ) user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( - USER_CROSS_SIGNING_PRIVATE_KEY_BYTES + user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"] ) b64_user_signing_public_key = encode_base64( user_signing_private_key.public_key().public_bytes( @@ -199,39 +239,39 @@ def build_cross_signing_keys_data() -> dict: # create without signatures initially cross_signing_keys_data = { "master_keys": { - TEST_USER_ID: { + user_data["TEST_USER_ID"]: { "keys": { f"ed25519:{b64_master_public_key}": b64_master_public_key, }, - "user_id": TEST_USER_ID, + "user_id": user_data["TEST_USER_ID"], "usage": ["master"], } }, "self_signing_keys": { - TEST_USER_ID: { + user_data["TEST_USER_ID"]: { "keys": { f"ed25519:{b64_self_signing_public_key}": b64_self_signing_public_key, }, - "user_id": TEST_USER_ID, + "user_id": user_data["TEST_USER_ID"], "usage": ["self_signing"], }, }, "user_signing_keys": { - TEST_USER_ID: { + user_data["TEST_USER_ID"]: { "keys": { f"ed25519:{b64_user_signing_public_key}": b64_user_signing_public_key, }, - "user_id": TEST_USER_ID, + "user_id": user_data["TEST_USER_ID"], "usage": ["user_signing"], }, }, } # sign the sub-keys with the master for k in ["self_signing_keys", "user_signing_keys"]: - to_sign = cross_signing_keys_data[k][TEST_USER_ID] + to_sign = cross_signing_keys_data[k][user_data["TEST_USER_ID"]] sig = sign_json(to_sign, master_private_key) to_sign["signatures"] = { - TEST_USER_ID: {f"ed25519:{b64_master_public_key}": sig} + user_data["TEST_USER_ID"]: {f"ed25519:{b64_master_public_key}": sig} } return cross_signing_keys_data diff --git a/spec/test-utils/test-data/index.ts b/spec/test-utils/test-data/index.ts index 5a2e3fa9d68..991ddde9956 100644 --- a/spec/test-utils/test-data/index.ts +++ b/spec/test-utils/test-data/index.ts @@ -9,6 +9,8 @@ import { KeyBackupInfo } from "../../../src/crypto-api"; /* eslint-disable comma-dangle */ +// Alice data + export const TEST_USER_ID = "@alice:localhost"; export const TEST_DEVICE_ID = "test_device"; export const TEST_ROOM_ID = "!room:id"; @@ -115,4 +117,115 @@ export const SIGNED_BACKUP_DATA: KeyBackupInfo = { } } } -}; \ No newline at end of file +}; + +// Bob data + +export const BOB_TEST_USER_ID = "@bob:xyz"; +export const BOB_TEST_DEVICE_ID = "bob_device"; +export const BOB_TEST_ROOM_ID = "!room:id"; + +/** The base64-encoded public ed25519 key for this device */ +export const BOB_TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50"; + +/** Signed device data, suitable for returning from a `/keys/query` call */ +export const BOB_SIGNED_TEST_DEVICE_DATA: 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": "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50" + }, + "user_id": "@bob:xyz", + "signatures": { + "@bob:xyz": { + "ed25519:bob_device": "4ApBs9jaeGyfdYaWRUdBvQAkDyXjACJ9KJ0xLHMgiFT/1yo6VqPTx2iziKGnrBiGhbtKNxEhDPOvZZkBU73cDQ" + } + } +}; + +/** base64-encoded public master cross-signing key */ +export const BOB_MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA"; + +/** base64-encoded private master cross-signing key */ +export const BOB_MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "RG95b3VzcGVha3doYWFhYWFhYWFhYWFhYWFhYWFhbGU"; + +/** base64-encoded public self cross-signing key */ +export const BOB_SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"; + +/** base64-encoded private self signing cross-signing key */ +export const BOB_SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "U2VsZnNlbGZzZWxmc2VsZnNlbGZzZWxmc2VsZnNlbGY"; + +/** base64-encoded public user cross-signing key */ +export const BOB_USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"; + +/** base64-encoded private user signing cross-signing key */ +export const BOB_USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "VXNlcnVzZXJ1c2VydXNlcnVzZXJ1c2VydXNlcnVzZXI"; + +/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */ +export const BOB_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial = { + "master_keys": { + "@bob:xyz": { + "keys": { + "ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA" + }, + "user_id": "@bob:xyz", + "usage": [ + "master" + ] + } + }, + "self_signing_keys": { + "@bob:xyz": { + "keys": { + "ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A" + }, + "user_id": "@bob:xyz", + "usage": [ + "self_signing" + ], + "signatures": { + "@bob:xyz": { + "ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "RxM8iJU6ZkyzQSVtNnXIJMPyEahVsN+fQQTBNKAs+kqySFyXBgchx+8czZaAhJCpXh9gD1nskT4yyFd2eyUXBw" + } + } + } + }, + "user_signing_keys": { + "@bob:xyz": { + "keys": { + "ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw" + }, + "user_id": "@bob:xyz", + "usage": [ + "user_signing" + ], + "signatures": { + "@bob:xyz": { + "ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "jF8fvnPZulrPyh/4E8dNDVBP3iHHl9bRc+rRArVyGzoom+uVrokOck7BN2YmPyCRFZJJx7fgRA1Bveyu+mTVAg" + } + } + } + } +}; + +/** base64-encoded backup decryption (private) key */ +export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo="; + +/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */ +export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = { + "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", + "version": "1", + "auth_data": { + "public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY", + "signatures": { + "@bob:xyz": { + "ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA" + } + } + } +}; + From 8a0baa02db4f6eb04998dbc7d7d5c80a795534a3 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 21 Aug 2023 15:41:51 +0200 Subject: [PATCH 6/7] `requestVerificationDM` test works against old crypto (encrypted verification request) --- spec/integ/crypto/verification.spec.ts | 55 ++++++++++++++++--- .../test-data/generate-test-data.py | 43 +++++++++------ spec/test-utils/test-data/index.ts | 32 +++++++++++ 3 files changed, 105 insertions(+), 25 deletions(-) diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index 19b076e65ac..5bc890e61c4 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -22,7 +22,15 @@ import fetchMock from "fetch-mock-jest"; import { IDBFactory } from "fake-indexeddb"; import { createHash } from "crypto"; -import { createClient, CryptoEvent, IContent, ICreateClientOpts, MatrixClient } from "../../../src"; +import { + createClient, + CryptoEvent, + IContent, + ICreateClientOpts, + MatrixClient, + MatrixEvent, + MatrixEventEvent, +} from "../../../src"; import { canAcceptVerificationRequest, ShowQrCodeCallbacks, @@ -47,6 +55,7 @@ import { TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64, TEST_ROOM_ID, TEST_USER_ID, + BOB_ONE_TIME_KEYS, } from "../../test-utils/test-data"; import { mockInitialApiRequests } from "../../test-utils/mockEndpoints"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; @@ -815,18 +824,23 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st describe("Send verification request in DM", () => { beforeEach(async () => { aliceClient = await startTestClient(); + aliceClient.setGlobalErrorOnUnknownDevices(false); e2eKeyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA); e2eKeyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA); - syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID])); // Wait for the sync response to be processed await syncPromise(aliceClient); }); + /** + * Create a mock to respond when the verification request is sent + * Handle both encrypted and unencrypted requests + */ function awaitRoomMessageRequest(): Promise { return new Promise((resolve) => { + // Case of unencrypted message of the new crypto fetchMock.put( "express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId", (url: string, options: RequestInit) => { @@ -834,10 +848,36 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st return { event_id: "$YUwRidLecu:example.com" }; }, ); + + // Case of encrypted message of the old crypto + fetchMock.put( + "express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId", + async (url: string, options: RequestInit) => { + const encryptedMessage = JSON.parse(options.body as string); + const event = new MatrixEvent({ + content: encryptedMessage, + type: "m.room.encrypted", + room_id: TEST_ROOM_ID, + }); + // Try to decrypt the event + event.once(MatrixEventEvent.Decrypted, (decryptedEvent: MatrixEvent, error?: Error) => { + expect(error).not.toBeDefined(); + resolve(decryptedEvent.getContent()); + }); + await aliceClient.decryptEventIfNeeded(event); + return { event_id: "$YUwRidLecu:example.com" }; + }, + ); }); } - newBackendOnly("alice sends a verification request in a DM to bob", async () => { + it("alice sends a verification request in a DM to bob", async () => { + fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({ one_time_keys: BOB_ONE_TIME_KEYS })); + + // In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms + // 5ms by users, ie Bob and Alice + await jest.advanceTimersByTimeAsync(10); + const messageRequestPromise = awaitRoomMessageRequest(); const verificationRequest = await aliceClient .getCrypto()! @@ -845,12 +885,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st 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.methods.sort()).toStrictEqual( + ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"].sort(), + ); expect(requestContent.msgtype).toBe("m.key.verification.request"); expect(requestContent.to).toBe(BOB_TEST_USER_ID); diff --git a/spec/test-utils/test-data/generate-test-data.py b/spec/test-utils/test-data/generate-test-data.py index b9f07b7aed4..d49a4ad1516 100755 --- a/spec/test-utils/test-data/generate-test-data.py +++ b/spec/test-utils/test-data/generate-test-data.py @@ -43,7 +43,9 @@ "SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"selfselfselfselfselfselfselfself", # Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts - "B64_BACKUP_DECRYPTION_KEY": "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=" + "B64_BACKUP_DECRYPTION_KEY": "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=", + + "OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw" } BOB_DATA = { @@ -58,22 +60,10 @@ "SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself", # Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts - "B64_BACKUP_DECRYPTION_KEY": "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=" -} - -# input data -TEST_USER_ID = "@alice:localhost" -TEST_DEVICE_ID = "test_device" -TEST_ROOM_ID = "!room:id" -# any 32-byte string can be an ed25519 private key. -TEST_DEVICE_PRIVATE_KEY_BYTES = b"deadbeefdeadbeefdeadbeefdeadbeef" + "B64_BACKUP_DECRYPTION_KEY": "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=", -MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"doyouspeakwhaaaaaaaaaaaaaaaaaale" -USER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"useruseruseruseruseruseruseruser" -SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"selfselfselfselfselfselfselfself" - -# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts -B64_BACKUP_DECRYPTION_KEY = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=" + "OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw" +} def main() -> None: print( @@ -170,6 +160,24 @@ def build_test_data(user_data, prefix = "") -> str: user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": sig} } + otk_to_sign = { + "key": user_data['OTK'] + } + # sign our public otk key with our device key + otk = sign_json(otk_to_sign, private_key) + otks = { + user_data["TEST_USER_ID"]: { + user_data['TEST_DEVICE_ID']: { + "signed_curve25519:AAAAHQ": { + "key": user_data["OTK"], + "signatures": { + user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": otk} + } + } + } + } + } + return f"""\ export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}"; export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}"; @@ -209,6 +217,9 @@ def build_test_data(user_data, prefix = "") -> str: /** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */ export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) }; + +/** Signed OTKs, returned by `POST /keys/claim` */ +export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) }; """ diff --git a/spec/test-utils/test-data/index.ts b/spec/test-utils/test-data/index.ts index 991ddde9956..720e0d9d779 100644 --- a/spec/test-utils/test-data/index.ts +++ b/spec/test-utils/test-data/index.ts @@ -119,6 +119,22 @@ export const SIGNED_BACKUP_DATA: KeyBackupInfo = { } }; +/** Signed OTKs, returned by `POST /keys/claim` */ +export const ONE_TIME_KEYS = { + "@alice:localhost": { + "test_device": { + "signed_curve25519:AAAAHQ": { + "key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw", + "signatures": { + "@alice:localhost": { + "ed25519:test_device": "25djC6Rk6gIgFBMVawY9X9LnY8XMMziey6lKqL8Q5Bbp7T1vw9uk0RE7eKO2a/jNLcYroO2xRztGhBrKz5sOCQ" + } + } + } + } + } +}; + // Bob data export const BOB_TEST_USER_ID = "@bob:xyz"; @@ -229,3 +245,19 @@ export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = { } }; +/** Signed OTKs, returned by `POST /keys/claim` */ +export const BOB_ONE_TIME_KEYS = { + "@bob:xyz": { + "bob_device": { + "signed_curve25519:AAAAHQ": { + "key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw", + "signatures": { + "@bob:xyz": { + "ed25519:bob_device": "dlZc9VA/hP980Mxvu9qwi0qJx8VK7sADGOM48CE01YM7K/Mbty9lis/QjtQAWqDg371QyynVRjEzt9qj7eSFCg" + } + } + } + } + } +}; + From a7e5f8f9095b54463a37f8dda3b5181e330f7844 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 21 Aug 2023 15:51:35 +0200 Subject: [PATCH 7/7] Update test data --- spec/test-utils/test-data/index.ts | 40 +++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/spec/test-utils/test-data/index.ts b/spec/test-utils/test-data/index.ts index 5b42955c074..640962d11fa 100644 --- a/spec/test-utils/test-data/index.ts +++ b/spec/test-utils/test-data/index.ts @@ -158,7 +158,6 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = { "forwarding_curve25519_key_chain": [] }; - /** Signed OTKs, returned by `POST /keys/claim` */ export const ONE_TIME_KEYS = { "@alice:localhost": { @@ -285,6 +284,45 @@ export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = { } }; +/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */ +export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [ + { + "algorithm": "m.megolm.v1.aes-sha2", + "room_id": "!roomA:example.org", + "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", + "session_id": "X9SK5JceUdvUr9gZkmdfS78qmKztoL60oORJ9JA5XD8", + "session_key": "AQAAAADbfOdUj/ec5bK4xVEhhRw+jfd1FD4uA0MLy7NyVHugZxyXNZUx5YEof4H9YyjBretviZreMSXqflbgYKz257rkKu7MPeKFf7zmln2GxX0F/p++GOnvpY1FqOglhfRQi3tqiyOa7SL4f7TuERDTOpMqlWhIfTKQnqy0AyF2vpDi5V/UiuSXHlHb1K/YGZJnX0u/Kpis7aC+tKDkSfSQOVw/", + "sender_claimed_keys": { + "ed25519": "ZG6lrfATe+958wN1xaGf3dKG/CThEfkmNdp1jcu4zok" + }, + "forwarding_curve25519_key_chain": [] + }, + { + "algorithm": "m.megolm.v1.aes-sha2", + "room_id": "!roomA:example.org", + "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", + "session_id": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA", + "session_key": "AQAAAACv0khqPrQ91MmWCgm0RTzfpn65AGCrRnAKLxGJdfSfECNZ8gyj34FZLwi+F+xC6ibFddcbLXW0mzR6PnTnHF3VHM4g/h+2rcxtlix8fySpIwFzaXViba7cOSy/b+dHTMZB40iA7F4y7AdTdHLv4N1XUj3puU/KVUIKf9/lEDLqyReD+39WdEY24mTIB5NcQQhtyguPzYPT5sSyeIUNd7Bw", + "sender_claimed_keys": { + "ed25519": "HxUKnGfeUu0fF3cLyCFSDXYtVCQHy/+33q9RkzKfsiU" + }, + "forwarding_curve25519_key_chain": [] + } +]; + +/** An exported megolm session */ +export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = { + "algorithm": "m.megolm.v1.aes-sha2", + "room_id": "!roomA:example.org", + "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", + "session_id": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM", + "session_key": "AQAAAACvcoGk7mOY59fOqZaxFUiTCBRV1Ia94KBjAZx6kgdgBtkkvs50z8od8/Nc9ncK2UsEiXNvCTTp2dlN3du+Rx0/m7vet2ZOEEp2oYDjHMLLFmwd1gtlGuWYPdXA6Y1+9Yyph0/EDVfS+zd3XvbL0QgbyL43+yQnFNHKlxVJX1eiKTrGTHQtYEOZz6/i/bbk+sV7GhSZFT5IMT9hXsRxdf0D", + "sender_claimed_keys": { + "ed25519": "dV0TIhhkToXpL+gZLo+zXDHJfw7MWYxpg80cynIQDv0" + }, + "forwarding_curve25519_key_chain": [] +}; + /** Signed OTKs, returned by `POST /keys/claim` */ export const BOB_ONE_TIME_KEYS = { "@bob:xyz": {