feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { act } from "react";
|
||||
import { Crypto } from "@peculiar/webcrypto";
|
||||
import { type CryptoApi, deriveRecoveryKeyFromPassphrase } from "matrix-js-sdk/src/crypto-api";
|
||||
import { SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { accessSecretStorage, crossSigningCallbacks } from "../../src/SecurityManager";
|
||||
import { filterConsole, stubClient } from "../test-utils";
|
||||
import Modal from "../../src/Modal.tsx";
|
||||
import {
|
||||
default as AccessSecretStorageDialog,
|
||||
type KeyParams,
|
||||
} from "../../src/components/views/dialogs/security/AccessSecretStorageDialog.tsx";
|
||||
|
||||
jest.mock("react", () => {
|
||||
const React = jest.requireActual("react");
|
||||
React.lazy = (children: any) => children(); // stub out lazy for dialog test
|
||||
return React;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("SecurityManager", () => {
|
||||
describe("accessSecretStorage", () => {
|
||||
filterConsole("Not setting dehydration key: no SSSS key found");
|
||||
|
||||
it("runs the function passed in", async () => {
|
||||
// Given a client
|
||||
const crypto = {
|
||||
bootstrapCrossSigning: () => {},
|
||||
bootstrapSecretStorage: () => {},
|
||||
} as unknown as CryptoApi;
|
||||
const client = stubClient();
|
||||
client.secretStorage.hasKey = jest.fn().mockResolvedValue(true);
|
||||
mocked(client.getCrypto).mockReturnValue(crypto);
|
||||
|
||||
// When I run accessSecretStorage
|
||||
const func = jest.fn();
|
||||
await accessSecretStorage(func);
|
||||
|
||||
// Then we call the passed-in function
|
||||
expect(func).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe("expecting errors", () => {
|
||||
filterConsole("End-to-end encryption is disabled - unable to access secret storage");
|
||||
|
||||
it("throws if crypto is unavailable", async () => {
|
||||
// Given a client with no crypto
|
||||
const client = stubClient();
|
||||
client.secretStorage.hasKey = jest.fn().mockResolvedValue(true);
|
||||
mocked(client.getCrypto).mockReturnValue(undefined);
|
||||
|
||||
// When I run accessSecretStorage
|
||||
// Then we throw an error
|
||||
await expect(async () => {
|
||||
await accessSecretStorage(jest.fn());
|
||||
}).rejects.toThrow("End-to-end encryption is disabled - unable to access secret storage");
|
||||
});
|
||||
|
||||
it("throws if there is no 4S", async () => {
|
||||
// Given a client with no default 4S key ID
|
||||
stubClient();
|
||||
|
||||
// When I run accessSecretStorage
|
||||
// Then we throw an error
|
||||
await expect(async () => {
|
||||
await accessSecretStorage(jest.fn());
|
||||
}).rejects.toThrow("Secret storage has not been created yet");
|
||||
});
|
||||
});
|
||||
|
||||
it("should show CreateSecretStorageDialog if forceReset=true", async () => {
|
||||
jest.mock("../../src/async-components/views/dialogs/security/CreateSecretStorageDialog", () => ({
|
||||
__test: true,
|
||||
__esModule: true,
|
||||
default: () => jest.fn(),
|
||||
}));
|
||||
const spy = jest.spyOn(Modal, "createDialog");
|
||||
stubClient();
|
||||
|
||||
const func = jest.fn();
|
||||
accessSecretStorage(func, { forceReset: true });
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
await expect(spy.mock.lastCall![0]).resolves.toEqual(expect.objectContaining({ __test: true }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSecretStorageKey", () => {
|
||||
const { getSecretStorageKey } = crossSigningCallbacks;
|
||||
|
||||
/** Polyfill crypto.subtle, which is unavailable in jsdom */
|
||||
function polyFillSubtleCrypto() {
|
||||
Object.defineProperty(globalThis.crypto, "subtle", { value: new Crypto().subtle });
|
||||
}
|
||||
|
||||
it("should prompt the user if the key is uncached", async () => {
|
||||
polyFillSubtleCrypto();
|
||||
|
||||
const client = stubClient();
|
||||
mocked(client.secretStorage.getDefaultKeyId).mockResolvedValue("my_default_key");
|
||||
|
||||
const passphrase = "s3cret";
|
||||
const { recoveryKey, keyInfo } = await deriveKeyFromPassphrase(passphrase);
|
||||
|
||||
jest.spyOn(Modal, "createDialog").mockImplementation((component) => {
|
||||
expect(component).toBe(AccessSecretStorageDialog);
|
||||
|
||||
const modalFunc = async () => [{ passphrase }] as [KeyParams];
|
||||
return {
|
||||
finished: modalFunc(),
|
||||
close: () => {},
|
||||
};
|
||||
});
|
||||
|
||||
const [keyId, key] = (await act(() =>
|
||||
getSecretStorageKey!({ keys: { my_default_key: keyInfo } }, "my_secret"),
|
||||
))!;
|
||||
expect(keyId).toEqual("my_default_key");
|
||||
expect(key).toEqual(recoveryKey);
|
||||
});
|
||||
|
||||
it("should not prompt the user if the requested key is not the default", async () => {
|
||||
const client = stubClient();
|
||||
mocked(client.secretStorage.getDefaultKeyId).mockResolvedValue("my_default_key");
|
||||
const createDialogSpy = jest.spyOn(Modal, "createDialog");
|
||||
|
||||
await expect(
|
||||
act(() =>
|
||||
getSecretStorageKey!(
|
||||
{ keys: { other_key: {} as SecretStorage.SecretStorageKeyDescription } },
|
||||
"my_secret",
|
||||
),
|
||||
),
|
||||
).rejects.toThrow("Request for non-default 4S key");
|
||||
expect(createDialogSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** Derive a key from a passphrase, also returning the KeyInfo */
|
||||
async function deriveKeyFromPassphrase(
|
||||
passphrase: string,
|
||||
): Promise<{ recoveryKey: Uint8Array; keyInfo: SecretStorage.SecretStorageKeyDescription }> {
|
||||
const salt = "SALTYGOODNESS";
|
||||
const iterations = 1000;
|
||||
|
||||
const recoveryKey = await deriveRecoveryKeyFromPassphrase(passphrase, salt, iterations);
|
||||
|
||||
const check = await SecretStorage.calculateKeyCheck(recoveryKey);
|
||||
return {
|
||||
recoveryKey,
|
||||
keyInfo: {
|
||||
iv: check.iv,
|
||||
mac: check.mac,
|
||||
algorithm: SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
name: "",
|
||||
passphrase: {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations,
|
||||
salt,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user