diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index b9fd38143d8..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, ICreateClientOpts, MatrixClient } from "../../../src"; +import { + createClient, + CryptoEvent, + IContent, + ICreateClientOpts, + MatrixClient, + MatrixEvent, + MatrixEventEvent, +} from "../../../src"; import { canAcceptVerificationRequest, ShowQrCodeCallbacks, @@ -34,15 +42,20 @@ 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 { + 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, TEST_DEVICE_ID, 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"; @@ -808,6 +821,82 @@ 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) => { + resolve(JSON.parse(options.body as string)); + 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" }; + }, + ); + }); + } + + 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()! + .requestVerificationDM(BOB_TEST_USER_ID, TEST_ROOM_ID); + const requestContent = await messageRequestPromise; + + expect(requestContent.from_device).toBe(aliceClient.getDeviceId()); + 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); + + expect(verificationRequest.roomId).toBe(TEST_ROOM_ID); + expect(verificationRequest.isSelfVerification).toBe(false); + expect(verificationRequest.otherUserId).toBe(BOB_TEST_USER_ID); + }); + }); + async function startTestClient(opts: Partial = {}): Promise { const client = createClient({ baseUrl: TEST_HOMESERVER_URL, diff --git a/spec/test-utils/test-data/generate-test-data.py b/spec/test-utils/test-data/generate-test-data.py index a3b8b134660..d0846e021be 100755 --- a/spec/test-utils/test-data/generate-test-data.py +++ b/spec/test-utils/test-data/generate-test-data.py @@ -32,73 +32,120 @@ from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from random import randbytes, seed -# 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" +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" + "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=" + # 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" +} + +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=", + + "OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw" +} 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, IMegolmSessionData }} 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="", ) + +# Use static seed to have stable random test data upon new generation +seed(10) + +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) @@ -114,92 +161,96 @@ 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} } set_of_exported_room_keys = [build_exported_megolm_key(), build_exported_megolm_key()] additional_exported_room_key = build_exported_megolm_key() - print( - f"""\ -/* Test data for cryptography tests - * - * Do not edit by hand! This file is generated by `./generate-test-data.py` - */ - -import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto"; -import {{ IDownloadKeyResult }} from "../../../src"; -import {{ KeyBackupInfo }} from "../../../src/crypto-api"; - -/* eslint-disable comma-dangle */ + 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} + } + } + } + } + } -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) }; +export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) }; /** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */ -export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = { +export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = { json.dumps(set_of_exported_room_keys, indent=4) }; /** An exported megolm session */ -export const MEGOLM_SESSION_DATA: IMegolmSessionData = { +export const {prefix}MEGOLM_SESSION_DATA: IMegolmSessionData = { json.dumps(additional_exported_room_key, indent=4) }; -""", - end="", - ) +/** Signed OTKs, returned by `POST /keys/claim` */ +export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) }; +""" -# Use static seed to have stable random test data upon new generation -seed(10) -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( @@ -207,7 +258,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( @@ -217,39 +268,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 8212068fb62..640962d11fa 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"; @@ -155,3 +157,185 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = { }, "forwarding_curve25519_key_chain": [] }; + +/** 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"; +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" + } + } + } +}; + +/** 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": { + "bob_device": { + "signed_curve25519:AAAAHQ": { + "key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw", + "signatures": { + "@bob:xyz": { + "ed25519:bob_device": "dlZc9VA/hP980Mxvu9qwi0qJx8VK7sADGOM48CE01YM7K/Mbty9lis/QjtQAWqDg371QyynVRjEzt9qj7eSFCg" + } + } + } + } + } +}; + 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/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 0985256731e..e5fc8a2650a 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -560,6 +560,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 diff --git a/src/client.ts b/src/client.ts index e664cc9f512..09e0798425c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2444,6 +2444,8 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { diff --git a/src/crypto-api.ts b/src/crypto-api.ts index 814aacc2f62..f75dcd7dfaa 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/OutgoingRequestProcessor.ts b/src/rust-crypto/OutgoingRequestProcessor.ts index 6a9848aa8d2..d5d12468bae 100644 --- a/src/rust-crypto/OutgoingRequestProcessor.ts +++ b/src/rust-crypto/OutgoingRequestProcessor.ts @@ -88,7 +88,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 e2ca176a822..e00cc454aa2 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -61,6 +61,7 @@ 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"]; @@ -714,6 +715,62 @@ 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) => + verificationMethodIdentifierToMethod(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/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. */