You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
Add an integration test for verification (#3436)
* Move existing crypto integ tests into a subdirectory * Factor out some common bits from `crypto.spec.ts` * Integration test for device verification * Ignore generated file in prettier
This commit is contained in:
committed by
GitHub
parent
946a1cef0f
commit
858155e0ef
@ -24,3 +24,6 @@ out
|
|||||||
|
|
||||||
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
|
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
|
||||||
/CHANGELOG.md
|
/CHANGELOG.md
|
||||||
|
|
||||||
|
# This file is also autogenerated
|
||||||
|
/spec/test-utils/test-data/index.ts
|
||||||
|
@ -18,8 +18,8 @@ import fetchMock from "fetch-mock-jest";
|
|||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
import { IDBFactory } from "fake-indexeddb";
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
|
|
||||||
import { CRYPTO_BACKENDS, InitCrypto } from "../test-utils/test-utils";
|
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
|
||||||
import { createClient, MatrixClient, UIAuthCallback } from "../../src";
|
import { createClient, MatrixClient, UIAuthCallback } from "../../../src";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
@ -21,11 +21,11 @@ import "fake-indexeddb/auto";
|
|||||||
import { IDBFactory } from "fake-indexeddb";
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
import { MockResponse, MockResponseFunction } from "fetch-mock";
|
import { MockResponse, MockResponseFunction } from "fetch-mock";
|
||||||
|
|
||||||
import type { IDeviceKeys } from "../../src/@types/crypto";
|
import type { IDeviceKeys } from "../../../src/@types/crypto";
|
||||||
import * as testUtils from "../test-utils/test-utils";
|
import * as testUtils from "../../test-utils/test-utils";
|
||||||
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../test-utils/test-utils";
|
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../../TestClient";
|
||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../../src/logger";
|
||||||
import {
|
import {
|
||||||
createClient,
|
createClient,
|
||||||
IClaimOTKsResult,
|
IClaimOTKsResult,
|
||||||
@ -43,13 +43,14 @@ import {
|
|||||||
Room,
|
Room,
|
||||||
RoomMember,
|
RoomMember,
|
||||||
RoomStateEvent,
|
RoomStateEvent,
|
||||||
} from "../../src/matrix";
|
} from "../../../src/matrix";
|
||||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||||
import { E2EKeyReceiver, IE2EKeyReceiver } from "../test-utils/E2EKeyReceiver";
|
import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||||
import { ISyncResponder, SyncResponder } from "../test-utils/SyncResponder";
|
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||||
import { escapeRegExp } from "../../src/utils";
|
import { escapeRegExp } from "../../../src/utils";
|
||||||
import { downloadDeviceToJsDevice } from "../../src/rust-crypto/device-converter";
|
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
|
||||||
import { flushPromises } from "../test-utils/flushPromises";
|
import { flushPromises } from "../../test-utils/flushPromises";
|
||||||
|
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||||
|
|
||||||
const ROOM_ID = "!room:id";
|
const ROOM_ID = "!room:id";
|
||||||
|
|
||||||
@ -419,12 +420,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
async function startClientAndAwaitFirstSync(opts: IStartClientOpts = {}): Promise<void> {
|
async function startClientAndAwaitFirstSync(opts: IStartClientOpts = {}): Promise<void> {
|
||||||
logger.log(aliceClient.getUserId() + ": starting");
|
logger.log(aliceClient.getUserId() + ": starting");
|
||||||
|
|
||||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||||
fetchMock.get(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["r0.5.0"] });
|
|
||||||
fetchMock.get(new URL("/_matrix/client/r0/pushrules/", homeserverUrl).toString(), {});
|
|
||||||
fetchMock.post(new URL("/_matrix/client/r0/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
|
|
||||||
filter_id: "fid",
|
|
||||||
});
|
|
||||||
|
|
||||||
// we let the client do a very basic initial sync, which it needs before
|
// we let the client do a very basic initial sync, which it needs before
|
||||||
// it will upload one-time keys.
|
// it will upload one-time keys.
|
@ -16,12 +16,12 @@ limitations under the License.
|
|||||||
|
|
||||||
import { Account } from "@matrix-org/olm";
|
import { Account } from "@matrix-org/olm";
|
||||||
|
|
||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../../src/logger";
|
||||||
import { decodeRecoveryKey } from "../../src/crypto/recoverykey";
|
import { decodeRecoveryKey } from "../../../src/crypto/recoverykey";
|
||||||
import { IKeyBackupInfo, IKeyBackupSession } from "../../src/crypto/keybackup";
|
import { IKeyBackupInfo, IKeyBackupSession } from "../../../src/crypto/keybackup";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../../TestClient";
|
||||||
import { IEvent } from "../../src";
|
import { IEvent } from "../../../src";
|
||||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
||||||
|
|
||||||
const ROOM_ID = "!ROOM:ID";
|
const ROOM_ID = "!ROOM:ID";
|
||||||
|
|
@ -26,16 +26,16 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// load olm before the sdk if possible
|
// load olm before the sdk if possible
|
||||||
import "../olm-loader";
|
import "../../olm-loader";
|
||||||
|
|
||||||
import type { Session } from "@matrix-org/olm";
|
import type { Session } from "@matrix-org/olm";
|
||||||
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
import type { IDeviceKeys, IOneTimeKey } from "../../../src/@types/crypto";
|
||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../../src/logger";
|
||||||
import * as testUtils from "../test-utils/test-utils";
|
import * as testUtils from "../../test-utils/test-utils";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../../TestClient";
|
||||||
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
|
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client";
|
||||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../../src/matrix";
|
||||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||||
|
|
||||||
let aliTestClient: TestClient;
|
let aliTestClient: TestClient;
|
||||||
const roomId = "!room:localhost";
|
const roomId = "!room:localhost";
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
import { IDBFactory } from "fake-indexeddb";
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
|
|
||||||
import { createClient } from "../../src";
|
import { createClient } from "../../../src";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
255
spec/integ/crypto/verification.spec.ts
Normal file
255
spec/integ/crypto/verification.spec.ts
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fetchMock from "fetch-mock-jest";
|
||||||
|
import { MockResponse } from "fetch-mock";
|
||||||
|
|
||||||
|
import { createClient, MatrixClient } from "../../../src";
|
||||||
|
import { ShowSasCallbacks, VerifierEvent } from "../../../src/crypto-api/verification";
|
||||||
|
import { escapeRegExp } from "../../../src/utils";
|
||||||
|
import { VerificationBase } from "../../../src/crypto/verification/Base";
|
||||||
|
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
|
||||||
|
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||||
|
import {
|
||||||
|
SIGNED_TEST_DEVICE_DATA,
|
||||||
|
TEST_DEVICE_ID,
|
||||||
|
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
||||||
|
TEST_USER_ID,
|
||||||
|
} from "../../test-utils/test-data";
|
||||||
|
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||||
|
import {
|
||||||
|
Phase,
|
||||||
|
VerificationRequest,
|
||||||
|
VerificationRequestEvent,
|
||||||
|
} from "../../../src/crypto/verification/request/VerificationRequest";
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for verification functionality.
|
||||||
|
*
|
||||||
|
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||||
|
* to provide the most effective integration tests possible.
|
||||||
|
*/
|
||||||
|
describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%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;
|
||||||
|
|
||||||
|
/** the client under test */
|
||||||
|
let aliceClient: MatrixClient;
|
||||||
|
|
||||||
|
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||||
|
let syncResponder: SyncResponder;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// anything that we don't have a specific matcher for silently returns a 404
|
||||||
|
fetchMock.catch(404);
|
||||||
|
fetchMock.config.warnOnFallback = false;
|
||||||
|
|
||||||
|
const homeserverUrl = "https://alice-server.com";
|
||||||
|
aliceClient = createClient({
|
||||||
|
baseUrl: homeserverUrl,
|
||||||
|
userId: TEST_USER_ID,
|
||||||
|
accessToken: "akjgkrgjs",
|
||||||
|
deviceId: "device_under_test",
|
||||||
|
});
|
||||||
|
|
||||||
|
await initCrypto(aliceClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await aliceClient.stopClient();
|
||||||
|
fetchMock.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
syncResponder = new SyncResponder(aliceClient.getHomeserverUrl());
|
||||||
|
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||||
|
aliceClient.startClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
oldBackendOnly("Outgoing verification: can verify another device via SAS", async () => {
|
||||||
|
// expect requests to download our own keys
|
||||||
|
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
|
||||||
|
device_keys: {
|
||||||
|
[TEST_USER_ID]: {
|
||||||
|
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// have alice initiate a verification. She should send a m.key.verification.request
|
||||||
|
let [requestBody, request] = await Promise.all([
|
||||||
|
expectSendToDeviceMessage("m.key.verification.request"),
|
||||||
|
aliceClient.requestVerification(TEST_USER_ID, [TEST_DEVICE_ID]),
|
||||||
|
]);
|
||||||
|
const transactionId = request.channel.transactionId;
|
||||||
|
expect(transactionId).toBeDefined();
|
||||||
|
expect(request.phase).toEqual(Phase.Requested);
|
||||||
|
|
||||||
|
let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||||
|
expect(toDeviceMessage.methods).toContain("m.sas.v1");
|
||||||
|
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||||
|
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||||
|
|
||||||
|
// The dummy device replies with an m.key.verification.ready...
|
||||||
|
returnToDeviceMessageFromSync({
|
||||||
|
type: "m.key.verification.ready",
|
||||||
|
content: {
|
||||||
|
from_device: TEST_DEVICE_ID,
|
||||||
|
methods: ["m.sas.v1"],
|
||||||
|
transaction_id: transactionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await waitForVerificationRequestChanged(request);
|
||||||
|
expect(request.phase).toEqual(Phase.Ready);
|
||||||
|
|
||||||
|
// ... and picks a method with m.key.verification.start
|
||||||
|
returnToDeviceMessageFromSync({
|
||||||
|
type: "m.key.verification.start",
|
||||||
|
content: {
|
||||||
|
from_device: TEST_DEVICE_ID,
|
||||||
|
method: "m.sas.v1",
|
||||||
|
transaction_id: transactionId,
|
||||||
|
hashes: ["sha256"],
|
||||||
|
key_agreement_protocols: ["curve25519"],
|
||||||
|
message_authentication_codes: ["hkdf-hmac-sha256.v2"],
|
||||||
|
short_authentication_string: ["emoji"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await waitForVerificationRequestChanged(request);
|
||||||
|
expect(request.phase).toEqual(Phase.Started);
|
||||||
|
expect(request.chosenMethod).toEqual("m.sas.v1");
|
||||||
|
|
||||||
|
// there should now be a verifier
|
||||||
|
const verifier: VerificationBase = request.verifier!;
|
||||||
|
expect(verifier).toBeDefined();
|
||||||
|
|
||||||
|
// start off the verification process: alice will send an `accept`
|
||||||
|
const verificationPromise = verifier.verify();
|
||||||
|
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||||
|
jest.advanceTimersByTime(10);
|
||||||
|
|
||||||
|
requestBody = await expectSendToDeviceMessage("m.key.verification.accept");
|
||||||
|
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||||
|
expect(toDeviceMessage.key_agreement_protocol).toEqual("curve25519");
|
||||||
|
expect(toDeviceMessage.short_authentication_string).toEqual(["emoji"]);
|
||||||
|
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||||
|
|
||||||
|
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
|
||||||
|
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||||
|
const olmSAS = new global.Olm.SAS();
|
||||||
|
returnToDeviceMessageFromSync({
|
||||||
|
type: "m.key.verification.key",
|
||||||
|
content: {
|
||||||
|
transaction_id: transactionId,
|
||||||
|
key: olmSAS.get_pubkey(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// alice responds with a 'key' ...
|
||||||
|
requestBody = await expectSendToDeviceMessage("m.key.verification.key");
|
||||||
|
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||||
|
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||||
|
const aliceDevicePubKeyBase64 = toDeviceMessage.key;
|
||||||
|
olmSAS.set_their_key(aliceDevicePubKeyBase64);
|
||||||
|
|
||||||
|
// ... and the client is notified to show the emoji
|
||||||
|
const showSas = await new Promise<ShowSasCallbacks>((resolve) => {
|
||||||
|
verifier.once(VerifierEvent.ShowSas, resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
// user confirms that the emoji match, and alice sends a 'mac'
|
||||||
|
[requestBody] = await Promise.all([expectSendToDeviceMessage("m.key.verification.mac"), showSas.confirm()]);
|
||||||
|
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||||
|
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||||
|
|
||||||
|
// the dummy device also confirms that the emoji match, and sends a mac
|
||||||
|
const macInfoBase = `MATRIX_KEY_VERIFICATION_MAC${TEST_USER_ID}${TEST_DEVICE_ID}${TEST_USER_ID}${aliceClient.deviceId}${transactionId}`;
|
||||||
|
returnToDeviceMessageFromSync({
|
||||||
|
type: "m.key.verification.mac",
|
||||||
|
content: {
|
||||||
|
keys: calculateMAC(olmSAS, `ed25519:${TEST_DEVICE_ID}`, `${macInfoBase}KEY_IDS`),
|
||||||
|
transaction_id: transactionId,
|
||||||
|
mac: {
|
||||||
|
[`ed25519:${TEST_DEVICE_ID}`]: calculateMAC(
|
||||||
|
olmSAS,
|
||||||
|
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
||||||
|
`${macInfoBase}ed25519:${TEST_DEVICE_ID}`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// that should satisfy Alice, who should reply with a 'done'
|
||||||
|
await expectSendToDeviceMessage("m.key.verification.done");
|
||||||
|
|
||||||
|
// ... and the whole thing should be done!
|
||||||
|
await verificationPromise;
|
||||||
|
expect(request.phase).toEqual(Phase.Done);
|
||||||
|
|
||||||
|
// we're done with the temporary keypair
|
||||||
|
olmSAS.free();
|
||||||
|
});
|
||||||
|
|
||||||
|
function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
|
||||||
|
ev.sender ??= TEST_USER_ID;
|
||||||
|
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the client under test to send a to-device message of the given type.
|
||||||
|
*
|
||||||
|
* @param msgtype - type of to-device message we expect
|
||||||
|
* @returns A Promise which resolves with the body of the HTTP request
|
||||||
|
*/
|
||||||
|
function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
fetchMock.putOnce(
|
||||||
|
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
|
||||||
|
(url: string, opts: RequestInit): MockResponse => {
|
||||||
|
resolve(JSON.parse(opts.body as string));
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** wait for the verification request to emit a 'Change' event */
|
||||||
|
function waitForVerificationRequestChanged(request: VerificationRequest): Promise<void> {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
request.once(VerificationRequestEvent.Change, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Perform a MAC calculation on the given data
|
||||||
|
*
|
||||||
|
* Does an HKDR and HMAC as defined by the matrix spec (https://spec.matrix.org/v1.7/client-server-api/#mac-calculation,
|
||||||
|
* as amended by https://github.com/matrix-org/matrix-spec/issues/1553).
|
||||||
|
*
|
||||||
|
* @param olmSAS
|
||||||
|
* @param input
|
||||||
|
* @param info
|
||||||
|
*/
|
||||||
|
function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
|
||||||
|
const mac = olmSAS.calculate_mac_fixed_base64(input, info);
|
||||||
|
//console.info(`Test MAC: input:'${input}, info: '${info}' -> '${mac}`);
|
||||||
|
return mac;
|
||||||
|
}
|
30
spec/test-utils/mockEndpoints.ts
Normal file
30
spec/test-utils/mockEndpoints.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fetchMock from "fetch-mock-jest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`.
|
||||||
|
*
|
||||||
|
* @param homeserverUrl - the homeserver url for the client under test
|
||||||
|
*/
|
||||||
|
export function mockInitialApiRequests(homeserverUrl: string) {
|
||||||
|
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["r0.5.0"] });
|
||||||
|
fetchMock.getOnce(new URL("/_matrix/client/r0/pushrules/", homeserverUrl).toString(), {});
|
||||||
|
fetchMock.postOnce(new URL("/_matrix/client/r0/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
|
||||||
|
filter_id: "fid",
|
||||||
|
});
|
||||||
|
}
|
1
spec/test-utils/test-data/.gitignore
vendored
Normal file
1
spec/test-utils/test-data/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/env
|
117
spec/test-utils/test-data/generate-test-data.py
Executable file
117
spec/test-utils/test-data/generate-test-data.py
Executable file
@ -0,0 +1,117 @@
|
|||||||
|
#!/bin/env python
|
||||||
|
#
|
||||||
|
# Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
This file is a Python script to generate test data for crypto tests.
|
||||||
|
|
||||||
|
To run it:
|
||||||
|
|
||||||
|
python -m venv env
|
||||||
|
./env/bin/pip install cryptography canonicaljson
|
||||||
|
./env/bin/python generate-test-data.py > index.ts
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
|
||||||
|
from canonicaljson import encode_canonical_json
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||||
|
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||||
|
|
||||||
|
# input data
|
||||||
|
TEST_USER_ID = "@alice:localhost"
|
||||||
|
TEST_DEVICE_ID = "test_device"
|
||||||
|
# any 32-byte string can be an ed25519 private key.
|
||||||
|
TEST_DEVICE_PRIVATE_KEY_BYTES = b"deadbeefdeadbeefdeadbeefdeadbeef"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||||
|
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,
|
||||||
|
"keys": {
|
||||||
|
f"curve25519:{TEST_DEVICE_ID}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
|
||||||
|
f"ed25519:{TEST_DEVICE_ID}": b64_public_key,
|
||||||
|
},
|
||||||
|
"signatures": {TEST_USER_ID: {}},
|
||||||
|
"user_id": TEST_USER_ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
device_data["signatures"][TEST_USER_ID][ f"ed25519:{TEST_DEVICE_ID}"] = sign_json(
|
||||||
|
device_data, private_key
|
||||||
|
)
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
/* eslint-disable comma-dangle */
|
||||||
|
|
||||||
|
export const TEST_USER_ID = "{TEST_USER_ID}";
|
||||||
|
export const TEST_DEVICE_ID = "{TEST_DEVICE_ID}";
|
||||||
|
|
||||||
|
/** The base64-encoded public ed25519 key for this device */
|
||||||
|
export const 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)};
|
||||||
|
""", end='',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_base64(input_bytes: bytes) -> str:
|
||||||
|
"""Encode with unpadded base64"""
|
||||||
|
output_bytes = base64.b64encode(input_bytes)
|
||||||
|
output_string = output_bytes.decode("ascii")
|
||||||
|
return output_string.rstrip("=")
|
||||||
|
|
||||||
|
|
||||||
|
def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
|
||||||
|
"""
|
||||||
|
Sign the given json object
|
||||||
|
|
||||||
|
Returns the base64-encoded signature of signing `input` following the Matrix
|
||||||
|
JSON signature algorithm [1]
|
||||||
|
|
||||||
|
[1]: https://spec.matrix.org/v1.7/appendices/#signing-details
|
||||||
|
"""
|
||||||
|
signatures = json_object.pop("signatures", {})
|
||||||
|
unsigned = json_object.pop("unsigned", None)
|
||||||
|
|
||||||
|
signature = private_key.sign(encode_canonical_json(json_object))
|
||||||
|
signature_base64 = encode_base64(signature)
|
||||||
|
|
||||||
|
json_object["signatures"] = signatures
|
||||||
|
if unsigned is not None:
|
||||||
|
json_object["unsigned"] = unsigned
|
||||||
|
|
||||||
|
return signature_base64
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
33
spec/test-utils/test-data/index.ts
Normal file
33
spec/test-utils/test-data/index.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/* 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";
|
||||||
|
|
||||||
|
/* eslint-disable comma-dangle */
|
||||||
|
|
||||||
|
export const TEST_USER_ID = "@alice:localhost";
|
||||||
|
export const TEST_DEVICE_ID = "test_device";
|
||||||
|
|
||||||
|
/** The base64-encoded public ed25519 key for this device */
|
||||||
|
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU";
|
||||||
|
|
||||||
|
/** Signed device data, suitable for returning from a `/keys/query` call */
|
||||||
|
export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
|
||||||
|
"algorithms": [
|
||||||
|
"m.olm.v1.curve25519-aes-sha2",
|
||||||
|
"m.megolm.v1.aes-sha2"
|
||||||
|
],
|
||||||
|
"device_id": "test_device",
|
||||||
|
"keys": {
|
||||||
|
"curve25519:test_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
|
||||||
|
"ed25519:test_device": "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU"
|
||||||
|
},
|
||||||
|
"user_id": "@alice:localhost",
|
||||||
|
"signatures": {
|
||||||
|
"@alice:localhost": {
|
||||||
|
"ed25519:test_device": "LmQC/yAUZJmkxZ+3L0nEwvtVWOzjqQqADWBhk+C47SPaFYHeV+E291mgXaSCJVeGltX+HC49Aw7nb6ga7sw0Aw"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
Reference in New Issue
Block a user