From 83d447adfe76c292f443dba0a1b8f2ba07e1da5f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:23:02 +0100 Subject: [PATCH] Clean up megolm-backup integ test (#3631) * Add `CryptoApi.setDeviceVerified` I need a way to mark devices as trusted for the backup tests. * More tests * Simplify E2EKeyResponder.addDeviceKeys The user and device IDs are in the test data, so no need to pass them in * Clean up key backup integration test Make it use the CryptoApi rather than legacy `MatrixClient.crypto`, and use a pre-signed backup instead of requiring a "blindlySignAnything" method. * run megolm-backup tests on both crypto stacks * avoid internal backupManager --- spec/integ/crypto/megolm-backup.spec.ts | 65 ++++++++----------- spec/integ/crypto/verification.spec.ts | 6 +- spec/test-utils/E2EKeyResponder.ts | 6 +- .../test-data/generate-test-data.py | 49 ++++++++++---- spec/test-utils/test-data/index.ts | 18 +++++ 5 files changed, 88 insertions(+), 56 deletions(-) diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index 1e956b52e..09d125b9e 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -15,17 +15,16 @@ limitations under the License. */ import fetchMock from "fetch-mock-jest"; +import "fake-indexeddb/auto"; -import { logger } from "../../../src/logger"; -import { decodeRecoveryKey } from "../../../src/crypto/recoverykey"; -import { IKeyBackupInfo, IKeyBackupSession } from "../../../src/crypto/keybackup"; +import { IKeyBackupSession } from "../../../src/crypto/keybackup"; import { createClient, ICreateClientOpts, IEvent, MatrixClient } from "../../../src"; -import { MatrixEventEvent } from "../../../src/models/event"; import { SyncResponder } from "../../test-utils/SyncResponder"; import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; import { mockInitialApiRequests } from "../../test-utils/mockEndpoints"; -import { syncPromise } from "../../test-utils/test-utils"; +import { awaitDecryption, CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils"; +import * as testData from "../../test-utils/test-data"; const ROOM_ID = "!ROOM:ID"; @@ -72,22 +71,14 @@ const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = { }, }; -const CURVE25519_BACKUP_INFO: IKeyBackupInfo = { - algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", - version: "1", - auth_data: { - public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", - // Will be updated with correct value on the fly - signatures: {}, - }, -}; - -const RECOVERY_KEY = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"; - const TEST_USER_ID = "@alice:localhost"; const TEST_DEVICE_ID = "xzcvb"; -describe("megolm key backups", function () { +describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => { + // oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the + // Rust backend. Once we have full support in the rust sdk, it will go away. + const oldBackendOnly = backend === "rust-sdk" ? test.skip : test; + let aliceClient: MatrixClient; /** an object which intercepts `/sync` requests on the test homeserver */ let syncResponder: SyncResponder; @@ -108,6 +99,7 @@ describe("megolm key backups", function () { syncResponder = new SyncResponder(TEST_HOMESERVER_URL); e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL); e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL); + e2eKeyResponder.addDeviceKeys(testData.SIGNED_TEST_DEVICE_DATA); e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver); }); @@ -130,12 +122,12 @@ describe("megolm key backups", function () { deviceId: TEST_DEVICE_ID, ...opts, }); - await client.initCrypto(); + await initCrypto(client); return client; } - it("Alice checks key backups when receiving a message she can't decrypt", async function () { + oldBackendOnly("Alice checks key backups when receiving a message she can't decrypt", async function () { const syncResponse = { next_batch: 1, rooms: { @@ -150,35 +142,32 @@ describe("megolm key backups", function () { }; fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", CURVE25519_KEY_BACKUP_DATA); - - // mock for the outgoing key requests that will be sent - fetchMock.put("express:/_matrix/client/r0/sendToDevice/m.room_key_request/:txid", {}); - - // We'll need to add a signature to the backup data, so take a copy to avoid mutating global state. - const backupData = JSON.parse(JSON.stringify(CURVE25519_BACKUP_INFO)); - fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData); + fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA); aliceClient = await initTestClient(); - await aliceClient.crypto!.signObject(backupData.auth_data); - await aliceClient.crypto!.storeSessionBackupPrivateKey(decodeRecoveryKey(RECOVERY_KEY)); - await aliceClient.crypto!.backupManager!.checkAndStart(); + const aliceCrypto = aliceClient.getCrypto()!; + await aliceCrypto.storeSessionBackupPrivateKey(Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64")); // start after saving the private key await aliceClient.startClient(); + // Persuade alice to fetch the device list. Completing the initial sync will make the device list download + // outdated device lists (of which our own user will be one). + syncResponder.sendOrQueueSyncResponse({}); + await jest.advanceTimersByTimeAsync(10); // DeviceList has a sleep(5) which we need to make happen + + // tell Alice to trust the dummy device that signed the backup, and re-check the backup. + // XXX: should we automatically re-check after a device becomes verified? + await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID); + await aliceClient.checkKeyBackup(); + + // Now, send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup. syncResponder.sendOrQueueSyncResponse(syncResponse); await syncPromise(aliceClient); const room = aliceClient.getRoom(ROOM_ID)!; - const event = room.getLiveTimeline().getEvents()[0]; - await new Promise((resolve, reject) => { - event.once(MatrixEventEvent.Decrypted, (ev) => { - logger.log(`${Date.now()} event ${event.getId()} now decrypted`); - resolve(ev); - }); - }); - + await awaitDecryption(event, { waitOnDecryptionFailure: true }); expect(event.getContent()).toEqual("testytest"); }); }); diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index e13b80c54..b9fd38143 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -124,7 +124,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st describe("Outgoing verification requests for another device", () => { beforeEach(async () => { // pretend that we have another device, which we will verify - e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA); + e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA); }); // test with (1) the default verification method list, (2) a custom verification method list. @@ -626,7 +626,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st describe("cancellation", () => { beforeEach(async () => { // pretend that we have another device, which we will start verifying - e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA); + e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA); e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA); aliceClient = await startTestClient(); @@ -743,7 +743,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st describe("Incoming verification from another device", () => { beforeEach(async () => { - e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA); + e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA); aliceClient = await startTestClient(); await waitForDeviceList(); diff --git a/spec/test-utils/E2EKeyResponder.ts b/spec/test-utils/E2EKeyResponder.ts index c232fd819..e779e8c5e 100644 --- a/spec/test-utils/E2EKeyResponder.ts +++ b/spec/test-utils/E2EKeyResponder.ts @@ -89,12 +89,10 @@ export class E2EKeyResponder { /** * Add a set of device keys for return by a future `/keys/query`, as if they had been `/upload`ed * - * @param userId - user the keys belong to - * @param deviceId - device the keys belong to * @param keys - device keys for this device. */ - public addDeviceKeys(userId: string, deviceId: string, keys: IDeviceKeys) { - this.deviceKeysByUserByDevice.getOrCreate(userId).set(deviceId, keys); + public addDeviceKeys(keys: IDeviceKeys) { + this.deviceKeysByUserByDevice.getOrCreate(keys.user_id).set(keys.device_id, keys); } /** Add a set of cross-signing keys for return by a future `/keys/query`, as if they had been `/keys/device_signing/upload`ed diff --git a/spec/test-utils/test-data/generate-test-data.py b/spec/test-utils/test-data/generate-test-data.py index d48bb37d7..a5cc8bd8f 100755 --- a/spec/test-utils/test-data/generate-test-data.py +++ b/spec/test-utils/test-data/generate-test-data.py @@ -28,7 +28,7 @@ import base64 import json from canonicaljson import encode_canonical_json -from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives.asymmetric import ed25519, x25519 from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat # input data @@ -41,6 +41,8 @@ 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=" def main() -> None: private_key = ed25519.Ed25519PrivateKey.from_private_bytes( @@ -71,29 +73,47 @@ def main() -> None: 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(MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES) self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( 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 + 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) user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( 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) + 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_CROSS_SIGNING_PRIVATE_KEY_BYTES) + + backup_decryption_key = x25519.X25519PrivateKey.from_private_bytes( + base64.b64decode(B64_BACKUP_DECRYPTION_KEY) ) + b64_backup_public_key = encode_base64( + backup_decryption_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + ) + + backup_data = { + "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", + "version": "1", + "auth_data": { + "public_key": b64_backup_public_key, + }, + } + # 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} + } print( f"""\ @@ -104,6 +124,7 @@ def main() -> None: import {{ IDeviceKeys }} from "../../../src/@types/crypto"; import {{ IDownloadKeyResult }} from "../../../src"; +import {{ KeyBackupInfo }} from "../../../src/crypto-api"; /* eslint-disable comma-dangle */ @@ -138,6 +159,12 @@ export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_ export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial = { json.dumps(build_cross_signing_keys_data(), indent=4) }; + +/** base64-encoded backup decryption (private) key */ +export const BACKUP_DECRYPTION_KEY_BASE64 = "{ 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="", ) diff --git a/spec/test-utils/test-data/index.ts b/spec/test-utils/test-data/index.ts index c25a2f78a..6b904e62d 100644 --- a/spec/test-utils/test-data/index.ts +++ b/spec/test-utils/test-data/index.ts @@ -5,6 +5,7 @@ import { IDeviceKeys } from "../../../src/@types/crypto"; import { IDownloadKeyResult } from "../../../src"; +import { KeyBackupInfo } from "../../../src/crypto-api"; /* eslint-disable comma-dangle */ @@ -97,3 +98,20 @@ export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial = { } } }; + +/** base64-encoded backup decryption (private) key */ +export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo="; + +/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */ +export const SIGNED_BACKUP_DATA: KeyBackupInfo = { + "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", + "version": "1", + "auth_data": { + "public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", + "signatures": { + "@alice:localhost": { + "ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA" + } + } + } +};