You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
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
This commit is contained in:
committed by
GitHub
parent
73c9f4e322
commit
83d447adfe
@ -15,17 +15,16 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
|
import "fake-indexeddb/auto";
|
||||||
|
|
||||||
import { logger } from "../../../src/logger";
|
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
|
||||||
import { decodeRecoveryKey } from "../../../src/crypto/recoverykey";
|
|
||||||
import { IKeyBackupInfo, IKeyBackupSession } from "../../../src/crypto/keybackup";
|
|
||||||
import { createClient, ICreateClientOpts, IEvent, MatrixClient } from "../../../src";
|
import { createClient, ICreateClientOpts, IEvent, MatrixClient } from "../../../src";
|
||||||
import { MatrixEventEvent } from "../../../src/models/event";
|
|
||||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
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";
|
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_USER_ID = "@alice:localhost";
|
||||||
const TEST_DEVICE_ID = "xzcvb";
|
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;
|
let aliceClient: MatrixClient;
|
||||||
/** an object which intercepts `/sync` requests on the test homeserver */
|
/** an object which intercepts `/sync` requests on the test homeserver */
|
||||||
let syncResponder: SyncResponder;
|
let syncResponder: SyncResponder;
|
||||||
@ -108,6 +99,7 @@ describe("megolm key backups", function () {
|
|||||||
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
|
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
|
||||||
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
|
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
|
||||||
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
|
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
|
||||||
|
e2eKeyResponder.addDeviceKeys(testData.SIGNED_TEST_DEVICE_DATA);
|
||||||
e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver);
|
e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -130,12 +122,12 @@ describe("megolm key backups", function () {
|
|||||||
deviceId: TEST_DEVICE_ID,
|
deviceId: TEST_DEVICE_ID,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
await client.initCrypto();
|
await initCrypto(client);
|
||||||
|
|
||||||
return 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 = {
|
const syncResponse = {
|
||||||
next_batch: 1,
|
next_batch: 1,
|
||||||
rooms: {
|
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);
|
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", CURVE25519_KEY_BACKUP_DATA);
|
||||||
|
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_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);
|
|
||||||
|
|
||||||
aliceClient = await initTestClient();
|
aliceClient = await initTestClient();
|
||||||
await aliceClient.crypto!.signObject(backupData.auth_data);
|
const aliceCrypto = aliceClient.getCrypto()!;
|
||||||
await aliceClient.crypto!.storeSessionBackupPrivateKey(decodeRecoveryKey(RECOVERY_KEY));
|
await aliceCrypto.storeSessionBackupPrivateKey(Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"));
|
||||||
await aliceClient.crypto!.backupManager!.checkAndStart();
|
|
||||||
|
|
||||||
// start after saving the private key
|
// start after saving the private key
|
||||||
await aliceClient.startClient();
|
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);
|
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||||
await syncPromise(aliceClient);
|
await syncPromise(aliceClient);
|
||||||
|
|
||||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||||
|
|
||||||
const event = room.getLiveTimeline().getEvents()[0];
|
const event = room.getLiveTimeline().getEvents()[0];
|
||||||
await new Promise((resolve, reject) => {
|
await awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
|
||||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
|
||||||
resolve(ev);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(event.getContent()).toEqual("testytest");
|
expect(event.getContent()).toEqual("testytest");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -124,7 +124,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
|||||||
describe("Outgoing verification requests for another device", () => {
|
describe("Outgoing verification requests for another device", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// pretend that we have another device, which we will verify
|
// 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.
|
// 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", () => {
|
describe("cancellation", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// pretend that we have another device, which we will start verifying
|
// 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);
|
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||||
|
|
||||||
aliceClient = await startTestClient();
|
aliceClient = await startTestClient();
|
||||||
@ -743,7 +743,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
|||||||
|
|
||||||
describe("Incoming verification from another device", () => {
|
describe("Incoming verification from another device", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
|
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||||
|
|
||||||
aliceClient = await startTestClient();
|
aliceClient = await startTestClient();
|
||||||
await waitForDeviceList();
|
await waitForDeviceList();
|
||||||
|
@ -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
|
* 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.
|
* @param keys - device keys for this device.
|
||||||
*/
|
*/
|
||||||
public addDeviceKeys(userId: string, deviceId: string, keys: IDeviceKeys) {
|
public addDeviceKeys(keys: IDeviceKeys) {
|
||||||
this.deviceKeysByUserByDevice.getOrCreate(userId).set(deviceId, keys);
|
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
|
/** Add a set of cross-signing keys for return by a future `/keys/query`, as if they had been `/keys/device_signing/upload`ed
|
||||||
|
@ -28,7 +28,7 @@ import base64
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from canonicaljson import encode_canonical_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
|
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||||
|
|
||||||
# input data
|
# input data
|
||||||
@ -41,6 +41,8 @@ MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"doyouspeakwhaaaaaaaaaaaaaaaaaale"
|
|||||||
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"useruseruseruseruseruseruseruser"
|
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"useruseruseruseruseruseruseruser"
|
||||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"selfselfselfselfselfselfselfself"
|
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:
|
def main() -> None:
|
||||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||||
@ -71,29 +73,47 @@ def main() -> None:
|
|||||||
b64_master_public_key = encode_base64(
|
b64_master_public_key = encode_base64(
|
||||||
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||||
)
|
)
|
||||||
b64_master_private_key = encode_base64(
|
b64_master_private_key = encode_base64(MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES)
|
||||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
|
||||||
)
|
|
||||||
|
|
||||||
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||||
)
|
)
|
||||||
b64_self_signing_public_key = encode_base64(
|
b64_self_signing_public_key = encode_base64(
|
||||||
self_signing_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
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(SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES)
|
||||||
|
|
||||||
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||||
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||||
)
|
)
|
||||||
b64_user_signing_public_key = encode_base64(
|
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(
|
b64_user_signing_private_key = encode_base64(USER_CROSS_SIGNING_PRIVATE_KEY_BYTES)
|
||||||
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(
|
print(
|
||||||
f"""\
|
f"""\
|
||||||
@ -104,6 +124,7 @@ def main() -> None:
|
|||||||
|
|
||||||
import {{ IDeviceKeys }} from "../../../src/@types/crypto";
|
import {{ IDeviceKeys }} from "../../../src/@types/crypto";
|
||||||
import {{ IDownloadKeyResult }} from "../../../src";
|
import {{ IDownloadKeyResult }} from "../../../src";
|
||||||
|
import {{ KeyBackupInfo }} from "../../../src/crypto-api";
|
||||||
|
|
||||||
/* eslint-disable comma-dangle */
|
/* 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<IDownloadKeyResult> = {
|
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||||
json.dumps(build_cross_signing_keys_data(), indent=4)
|
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="",
|
end="",
|
||||||
)
|
)
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { IDeviceKeys } from "../../../src/@types/crypto";
|
import { IDeviceKeys } from "../../../src/@types/crypto";
|
||||||
import { IDownloadKeyResult } from "../../../src";
|
import { IDownloadKeyResult } from "../../../src";
|
||||||
|
import { KeyBackupInfo } from "../../../src/crypto-api";
|
||||||
|
|
||||||
/* eslint-disable comma-dangle */
|
/* eslint-disable comma-dangle */
|
||||||
|
|
||||||
@ -97,3 +98,20 @@ export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user