mv element.io @types __mocks__/ debian docker module_system/ playwright res src test webapp Dockerfile .dockerignore .eslintignore .stylelintrc.cjs babel.config.cjs recorder-worklet-loader.cjs .modernizr.json components.json config.json config.sample.json package.json project.json tsconfig.json tsconfig.module_system.json jest.config.ts playwright.config.ts webpack.config.ts build_config.sample.yaml apps/web/
mkdir apps/web/scripts
mv scripts/{cleanup.sh,ci_package.sh,copy-res.ts,deploy.py,package.sh} apps/web/scripts
And a couple of gitignore tweaks
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019, 2020 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 { lazy } from "react";
|
||||
import { type SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||
import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey, type CryptoCallbacks } from "matrix-js-sdk/src/crypto-api";
|
||||
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import Modal from "./Modal";
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import { _t } from "./languageHandler";
|
||||
import AccessSecretStorageDialog, {
|
||||
type KeyParams,
|
||||
} from "./components/views/dialogs/security/AccessSecretStorageDialog";
|
||||
import { ModuleRunner } from "./modules/ModuleRunner";
|
||||
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
|
||||
|
||||
// This stores the secret storage private keys in memory for the JS SDK. This is
|
||||
// only meant to act as a cache to avoid prompting the user multiple times
|
||||
// during the same single operation. Use `accessSecretStorage` below to scope a
|
||||
// single secret storage operation, as it will clear the cached keys once the
|
||||
// operation ends.
|
||||
let secretStorageKeys: Record<string, Uint8Array<ArrayBuffer>> = {};
|
||||
let secretStorageKeyInfo: Record<string, SecretStorage.SecretStorageKeyDescription> = {};
|
||||
let secretStorageBeingAccessed = false;
|
||||
|
||||
const logger = rootLogger.getChild("SecurityManager:");
|
||||
|
||||
/**
|
||||
* This can be used by other components to check if secret storage access is in
|
||||
* progress, so that we can e.g. avoid intermittently showing toasts during
|
||||
* secret storage setup.
|
||||
*
|
||||
* @returns {bool}
|
||||
*/
|
||||
export function isSecretStorageBeingAccessed(): boolean {
|
||||
return secretStorageBeingAccessed;
|
||||
}
|
||||
|
||||
export class AccessCancelledError extends Error {
|
||||
public constructor() {
|
||||
super("Secret storage access canceled");
|
||||
}
|
||||
}
|
||||
|
||||
function makeInputToKey(
|
||||
keyInfo: SecretStorage.SecretStorageKeyDescription,
|
||||
): (keyParams: KeyParams) => Promise<Uint8Array<ArrayBuffer>> {
|
||||
return async ({ passphrase, recoveryKey }): Promise<Uint8Array<ArrayBuffer>> => {
|
||||
if (passphrase) {
|
||||
return deriveRecoveryKeyFromPassphrase(passphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations);
|
||||
} else if (recoveryKey) {
|
||||
return decodeRecoveryKey(recoveryKey);
|
||||
}
|
||||
throw new Error("Invalid input, passphrase or recoveryKey need to be provided");
|
||||
};
|
||||
}
|
||||
|
||||
async function getSecretStorageKey(
|
||||
{
|
||||
keys: keyInfos,
|
||||
}: {
|
||||
keys: Record<string, SecretStorage.SecretStorageKeyDescription>;
|
||||
},
|
||||
secretName: string,
|
||||
): Promise<[string, Uint8Array<ArrayBuffer>]> {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const defaultKeyId = await cli.secretStorage.getDefaultKeyId();
|
||||
|
||||
let keyId: string;
|
||||
// If the defaultKey is useful, use that
|
||||
if (defaultKeyId && keyInfos[defaultKeyId]) {
|
||||
keyId = defaultKeyId;
|
||||
} else {
|
||||
// Fall back to a heuristic of using the
|
||||
// only available key, if only one key is set
|
||||
const usefulKeys = Object.keys(keyInfos);
|
||||
if (usefulKeys.length > 1) {
|
||||
throw new Error("Multiple storage key requests not implemented");
|
||||
}
|
||||
keyId = usefulKeys[0];
|
||||
}
|
||||
const keyInfo = keyInfos[keyId];
|
||||
logger.debug(
|
||||
`getSecretStorageKey: request for 4S keys [${Object.keys(keyInfos)}] for secret \`${secretName}\`: looking for key ${keyId}`,
|
||||
);
|
||||
|
||||
// Check the in-memory cache
|
||||
if (secretStorageBeingAccessed && secretStorageKeys[keyId]) {
|
||||
logger.debug(`getSecretStorageKey: returning key ${keyId} from cache`);
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
}
|
||||
|
||||
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.getSecretStorageKey();
|
||||
if (keyFromCustomisations) {
|
||||
logger.debug("getSecretStorageKey: Using secret storage key from CryptoSetupExtension");
|
||||
cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations);
|
||||
return [keyId, keyFromCustomisations];
|
||||
}
|
||||
|
||||
// We only prompt the user for the default key
|
||||
if (keyId !== defaultKeyId) {
|
||||
logger.debug(`getSecretStorageKey: request for non-default key ${keyId}: not prompting user`);
|
||||
throw new Error("Request for non-default 4S key");
|
||||
}
|
||||
|
||||
logger.debug(`getSecretStorageKey: prompting user for key ${keyId}`);
|
||||
const inputToKey = makeInputToKey(keyInfo);
|
||||
const { finished } = Modal.createDialog(
|
||||
AccessSecretStorageDialog,
|
||||
/* props= */
|
||||
{
|
||||
keyInfo,
|
||||
checkPrivateKey: async (input: KeyParams): Promise<boolean> => {
|
||||
const key = await inputToKey(input);
|
||||
return MatrixClientPeg.safeGet().secretStorage.checkKey(key, keyInfo);
|
||||
},
|
||||
},
|
||||
);
|
||||
const [keyParams] = await finished;
|
||||
if (!keyParams) {
|
||||
throw new AccessCancelledError();
|
||||
}
|
||||
logger.debug(`getSecretStorageKey: got key ${keyId} from user`);
|
||||
const key = await inputToKey(keyParams);
|
||||
|
||||
// Save to cache to avoid future prompts in the current session
|
||||
cacheSecretStorageKey(keyId, keyInfo, key);
|
||||
|
||||
return [keyId, key];
|
||||
}
|
||||
|
||||
function cacheSecretStorageKey(
|
||||
keyId: string,
|
||||
keyInfo: SecretStorage.SecretStorageKeyDescription,
|
||||
key: Uint8Array<ArrayBuffer>,
|
||||
): void {
|
||||
if (secretStorageBeingAccessed) {
|
||||
logger.debug(`Caching 4S key ${keyId}`);
|
||||
secretStorageKeys[keyId] = key;
|
||||
secretStorageKeyInfo[keyId] = keyInfo;
|
||||
}
|
||||
}
|
||||
|
||||
export const crossSigningCallbacks: CryptoCallbacks = {
|
||||
getSecretStorageKey,
|
||||
cacheSecretStorageKey,
|
||||
};
|
||||
|
||||
/**
|
||||
* Carry out an operation that may require multiple accesses to secret storage, caching the key.
|
||||
*
|
||||
* Use this helper to wrap an operation that may require multiple accesses to secret storage; the user will be prompted
|
||||
* to enter the 4S key or passphrase on the first access, and the key will be cached for the rest of the operation.
|
||||
*
|
||||
* @param func - The operation to be wrapped.
|
||||
*/
|
||||
export async function withSecretStorageKeyCache<T>(func: () => Promise<T>): Promise<T> {
|
||||
logger.debug("enabling 4S key cache");
|
||||
secretStorageBeingAccessed = true;
|
||||
try {
|
||||
return await func();
|
||||
} finally {
|
||||
// Clear secret storage key cache now that work is complete
|
||||
logger.debug("disabling 4S key cache");
|
||||
secretStorageBeingAccessed = false;
|
||||
secretStorageKeys = {};
|
||||
secretStorageKeyInfo = {};
|
||||
}
|
||||
}
|
||||
|
||||
export interface AccessSecretStorageOpts {
|
||||
/**
|
||||
* Reset secret storage even if it's already set up.
|
||||
* @deprecated send the user to the Encryption settings tab to reset secret storage
|
||||
*/
|
||||
forceReset?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper should be used whenever you need to access secret storage. It
|
||||
* ensures that secret storage (and also cross-signing since they each depend on
|
||||
* each other in a cycle of sorts) have been bootstrapped before running the
|
||||
* provided function.
|
||||
*
|
||||
* Bootstrapping secret storage may take one of these paths:
|
||||
* 1. (Only if `opts.forceReset` is set) create secret storage from a passphrase
|
||||
* and store cross-signing keys in secret storage.
|
||||
* 2. Access existing secret storage by requesting passphrase and accessing
|
||||
* cross-signing keys as needed.
|
||||
* 3. All keys are loaded and there's nothing to do.
|
||||
*
|
||||
* Additionally, the secret storage keys are cached during the scope of this function
|
||||
* to ensure the user is prompted only once for their secret storage
|
||||
* passphrase. The cache is then cleared once the provided function completes.
|
||||
*
|
||||
* Throws an error if secret storage is not set up (and `opts.forceReset` is not set)
|
||||
*
|
||||
* @param {Function} [func] An operation to perform once secret storage has been
|
||||
* bootstrapped. Optional.
|
||||
* @param [opts] The options to use when accessing secret storage.
|
||||
*/
|
||||
export async function accessSecretStorage(
|
||||
func = async (): Promise<void> => {},
|
||||
opts: AccessSecretStorageOpts = {},
|
||||
): Promise<void> {
|
||||
await withSecretStorageKeyCache(() => doAccessSecretStorage(func, opts));
|
||||
}
|
||||
|
||||
/** Helper for {@link #accessSecretStorage} */
|
||||
async function doAccessSecretStorage(func: () => Promise<void>, opts: AccessSecretStorageOpts): Promise<void> {
|
||||
try {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const crypto = cli.getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("End-to-end encryption is disabled - unable to access secret storage.");
|
||||
}
|
||||
|
||||
if (opts.forceReset) {
|
||||
logger.debug("accessSecretStorage: resetting 4S");
|
||||
// This dialog calls bootstrap itself after guiding the user through
|
||||
// passphrase creation.
|
||||
const { finished } = Modal.createDialog(
|
||||
lazy(() => import("./async-components/views/dialogs/security/CreateSecretStorageDialog")),
|
||||
opts,
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
/* static = */ true,
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Secret storage creation canceled");
|
||||
}
|
||||
} else if (!(await cli.secretStorage.hasKey())) {
|
||||
logger.debug("accessSecretStorage: no 4S key configured");
|
||||
throw new Error("Secret storage has not been created yet.");
|
||||
} else {
|
||||
logger.debug("accessSecretStorage: bootstrapCrossSigning");
|
||||
await crypto.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => {
|
||||
logger.debug("accessSecretStorage: performing UIA to upload cross-signing keys");
|
||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||
title: _t("encryption|bootstrap_title"),
|
||||
matrixClient: cli,
|
||||
makeRequest,
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
logger.debug("accessSecretStorage: Cross-signing key upload successful");
|
||||
},
|
||||
});
|
||||
logger.debug("accessSecretStorage: bootstrapSecretStorage");
|
||||
await crypto.bootstrapSecretStorage({});
|
||||
}
|
||||
|
||||
logger.debug("accessSecretStorage: 4S now ready");
|
||||
// `return await` needed here to ensure `finally` block runs after the
|
||||
// inner operation completes.
|
||||
await func();
|
||||
logger.debug("accessSecretStorage: operation complete");
|
||||
} catch (e) {
|
||||
ModuleRunner.instance.extensions.cryptoSetup.catchAccessSecretStorageError(e as Error);
|
||||
logger.error("accessSecretStorage: error during operation", e);
|
||||
// Re-throw so that higher level logic can abort as needed
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user