mirror of
https://github.com/vector-im/element-web.git
synced 2025-08-06 06:17:03 +02:00
Avoid using accessSecretStorage to create 4S (#30244)
* remove resetCrossSigning flag, which is no longer in use * drop unnecessary check for cross-signing The only place where verifyUser is called already checks that cross-signing is set up. (The function name is also incorrect, since it checks for the cross-signing key, and not for 4S.) * avoid calling accessSecretStorage to set up cross-signing or 4S Send the user to the Encryption settings tab instead * only create secret storage when specifically asked to * deprecate using accessSecretStorage to create new 4S * also remove the obsolete snapshot * add tests * Tweak comment Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
parent
66d7c6a100
commit
9095ebdb1b
@ -177,7 +177,6 @@
|
||||
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.pcss";
|
||||
@import "./views/dialogs/security/_AccessSecretStorageDialog.pcss";
|
||||
@import "./views/dialogs/security/_CreateCrossSigningDialog.pcss";
|
||||
@import "./views/dialogs/security/_CreateKeyBackupDialog.pcss";
|
||||
@import "./views/dialogs/security/_CreateSecretStorageDialog.pcss";
|
||||
@import "./views/dialogs/security/_KeyBackupFailedDialog.pcss";
|
||||
@import "./views/dialogs/security/_RestoreKeyBackupDialog.pcss";
|
||||
|
@ -1,73 +0,0 @@
|
||||
/*
|
||||
Copyright 2018-2024 New Vector Ltd.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_CreateKeyBackupDialog .mx_Dialog_title {
|
||||
/* TODO: Consider setting this for all dialog titles. */
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.mx_CreateKeyBackupDialog_primaryContainer {
|
||||
/* FIXME: plinth colour in new theme(s). background-color: $accent; */
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.mx_CreateKeyBackupDialog_primaryContainer::after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_CreateKeyBackupDialog_passPhraseContainer {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.mx_CreateKeyBackupDialog_passPhraseInput {
|
||||
flex: none;
|
||||
width: 250px;
|
||||
border: 1px solid $accent;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.mx_CreateKeyBackupDialog_passPhraseMatch {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.mx_CreateKeyBackupDialog_recoveryKeyHeader {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.mx_CreateKeyBackupDialog_recoveryKeyContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mx_CreateKeyBackupDialog_recoveryKey {
|
||||
width: 262px;
|
||||
padding: 20px;
|
||||
color: $info-plinth-fg-color;
|
||||
background-color: $info-plinth-bg-color;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.mx_CreateKeyBackupDialog_recoveryKeyButtons {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_CreateKeyBackupDialog_recoveryKeyButtons button {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mx_CreateKeyBackupDialog {
|
||||
details .mx_AccessibleButton {
|
||||
margin: 1em 0; /* emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules */
|
||||
}
|
||||
}
|
@ -176,10 +176,11 @@ export async function withSecretStorageKeyCache<T>(func: () => Promise<T>): Prom
|
||||
}
|
||||
|
||||
export interface AccessSecretStorageOpts {
|
||||
/** Reset secret storage even if it's already set up. */
|
||||
/**
|
||||
* 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;
|
||||
/** Create new cross-signing keys. Only applicable if `forceReset` is `true`. */
|
||||
resetCrossSigning?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -189,8 +190,8 @@ export interface AccessSecretStorageOpts {
|
||||
* provided function.
|
||||
*
|
||||
* Bootstrapping secret storage may take one of these paths:
|
||||
* 1. Create secret storage from a passphrase and store cross-signing keys
|
||||
* in secret storage.
|
||||
* 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.
|
||||
@ -199,6 +200,8 @@ export interface AccessSecretStorageOpts {
|
||||
* 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.
|
||||
@ -219,16 +222,8 @@ async function doAccessSecretStorage(func: () => Promise<void>, opts: AccessSecr
|
||||
throw new Error("End-to-end encryption is disabled - unable to access secret storage.");
|
||||
}
|
||||
|
||||
let createNew = false;
|
||||
if (opts.forceReset) {
|
||||
logger.debug("accessSecretStorage: resetting 4S");
|
||||
createNew = true;
|
||||
} else if (!(await cli.secretStorage.hasKey())) {
|
||||
logger.debug("accessSecretStorage: no 4S key configured, creating a new one");
|
||||
createNew = true;
|
||||
}
|
||||
|
||||
if (createNew) {
|
||||
// This dialog calls bootstrap itself after guiding the user through
|
||||
// passphrase creation.
|
||||
const { finished } = Modal.createDialog(
|
||||
@ -251,6 +246,9 @@ async function doAccessSecretStorage(func: () => Promise<void>, opts: AccessSecr
|
||||
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({
|
||||
|
@ -1,186 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
|
||||
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 React, { type JSX } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { accessSecretStorage, withSecretStorageKeyCache } from "../../../../SecurityManager";
|
||||
import Spinner from "../../../../components/views/elements/Spinner";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
|
||||
enum Phase {
|
||||
BackingUp = "backing_up",
|
||||
Done = "done",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished(done?: boolean): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
passPhrase: string;
|
||||
passPhraseValid: boolean;
|
||||
passPhraseConfirm: string;
|
||||
copied: boolean;
|
||||
downloaded: boolean;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks the user through the process of setting up e2e key backups to a new backup, and storing the decryption key in
|
||||
* SSSS.
|
||||
*
|
||||
* Uses {@link accessSecretStorage}, which means that if 4S is not already configured, it will be bootstrapped (which
|
||||
* involves displaying an {@link CreateSecretStorageDialog} so the user can enter a passphrase and/or download the 4S
|
||||
* key).
|
||||
*/
|
||||
export default class CreateKeyBackupDialog extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
phase: Phase.BackingUp,
|
||||
passPhrase: "",
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: "",
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.createBackup();
|
||||
}
|
||||
|
||||
private createBackup = async (): Promise<void> => {
|
||||
this.setState({
|
||||
error: undefined,
|
||||
});
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
try {
|
||||
// Check if 4S already set up
|
||||
const secretStorageAlreadySetup = await cli.secretStorage.hasKey();
|
||||
|
||||
if (!secretStorageAlreadySetup) {
|
||||
// bootstrap secret storage; that will also create a backup version
|
||||
await accessSecretStorage(async (): Promise<void> => {
|
||||
// do nothing, all is now set up correctly
|
||||
});
|
||||
} else {
|
||||
await withSecretStorageKeyCache(async () => {
|
||||
const crypto = cli.getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("End-to-end encryption is disabled - unable to create backup.");
|
||||
}
|
||||
|
||||
// Before we reset the backup, let's make sure we can access secret storage, to
|
||||
// reduce the chance of us getting into a broken state where we have an outdated
|
||||
// secret in secret storage.
|
||||
// `SecretStorage.get` will ask the user to enter their passphrase/key if necessary;
|
||||
// it will then be cached for the actual backup reset operation.
|
||||
await cli.secretStorage.get("m.megolm_backup.v1");
|
||||
|
||||
// We now know we can store the new backup key in secret storage, so it is safe to
|
||||
// go ahead with the reset.
|
||||
await crypto.resetKeyBackup();
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
phase: Phase.Done,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Error creating key backup", e);
|
||||
// TODO: If creating a version succeeds, but backup fails, should we
|
||||
// delete the version, disable backup, or do nothing? If we just
|
||||
// disable without deleting, we'll enable on next app reload since
|
||||
// it is trusted.
|
||||
this.setState({
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private onDone = (): void => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
private renderBusyPhase(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPhaseDone(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("settings|key_backup|backup_in_progress")}</p>
|
||||
<DialogButtons primaryButton={_t("action|ok")} onPrimaryButtonClick={this.onDone} hasCancel={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private titleForPhase(phase: Phase): string {
|
||||
switch (phase) {
|
||||
case Phase.BackingUp:
|
||||
return _t("settings|key_backup|backup_starting");
|
||||
case Phase.Done:
|
||||
return _t("settings|key_backup|backup_success");
|
||||
default:
|
||||
return _t("settings|key_backup|create_title");
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
content = (
|
||||
<div>
|
||||
<p>{_t("settings|key_backup|cannot_create_backup")}</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|retry")}
|
||||
onPrimaryButtonClick={this.createBackup}
|
||||
hasCancel={true}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
switch (this.state.phase) {
|
||||
case Phase.BackingUp:
|
||||
content = this.renderBusyPhase();
|
||||
break;
|
||||
case Phase.Done:
|
||||
content = this.renderPhaseDone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_CreateKeyBackupDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.titleForPhase(this.state.phase)}
|
||||
hasCancel={[Phase.Done].includes(this.state.phase)}
|
||||
>
|
||||
<div>{content}</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
@ -56,7 +56,6 @@ const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow,
|
||||
|
||||
interface IProps {
|
||||
forceReset?: boolean;
|
||||
resetCrossSigning?: boolean;
|
||||
onFinished(ok?: boolean): void;
|
||||
}
|
||||
|
||||
@ -80,11 +79,12 @@ interface IState {
|
||||
* If the user already has a key backup, follows a "migration" flow (aka "Upgrade your encryption") which
|
||||
* prompts the user to enter their backup decryption password (a Curve25519 private key, possibly derived
|
||||
* from a passphrase), and uses that as the (AES) 4S encryption key.
|
||||
*
|
||||
* @deprecated send the user to EncryptionUserSettingsTab instead
|
||||
*/
|
||||
export default class CreateSecretStorageDialog extends React.PureComponent<IProps, IState> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
forceReset: false,
|
||||
resetCrossSigning: false,
|
||||
};
|
||||
private recoveryKey?: GeneratedSecretStorageKey;
|
||||
private recoveryKeyNode = createRef<HTMLElement>();
|
||||
@ -211,7 +211,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||
private bootstrapSecretStorage = async (): Promise<void> => {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const crypto = cli.getCrypto()!;
|
||||
const { forceReset, resetCrossSigning } = this.props;
|
||||
const { forceReset } = this.props;
|
||||
|
||||
let backupInfo;
|
||||
// First, unless we know we want to do a reset, we see if there is an existing key backup
|
||||
@ -246,13 +246,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||
createSecretStorageKey: async () => this.recoveryKey!,
|
||||
setupNewSecretStorage: true,
|
||||
});
|
||||
if (resetCrossSigning) {
|
||||
logger.log("Resetting cross signing");
|
||||
await crypto.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
|
||||
setupNewCrossSigning: true,
|
||||
});
|
||||
}
|
||||
logger.log("Resetting key backup");
|
||||
await crypto.resetKeyBackup();
|
||||
} else {
|
||||
|
@ -7,14 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { lazy } from "react";
|
||||
import React from "react";
|
||||
|
||||
import dis from "../../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Modal from "../../../../Modal";
|
||||
import { Action } from "../../../../dispatcher/actions";
|
||||
import { UserTab } from "../../../../components/views/dialogs/UserTab";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
import { type OpenToTabPayload } from "../../../../dispatcher/payloads/OpenToTabPayload";
|
||||
|
||||
interface IProps {
|
||||
onFinished(): void;
|
||||
@ -28,13 +29,12 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent<IPr
|
||||
|
||||
private onSetupClick = (): void => {
|
||||
this.props.onFinished();
|
||||
Modal.createDialog(
|
||||
lazy(() => import("./CreateKeyBackupDialog")),
|
||||
undefined,
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
/* static = */ true,
|
||||
);
|
||||
// Open the user settings dialog to the encryption tab and start the flow to reset encryption
|
||||
const payload: OpenToTabPayload = {
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Encryption,
|
||||
};
|
||||
dis.dispatch(payload);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
|
@ -13,9 +13,11 @@ import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import Modal from "../../../Modal";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { type OpenToTabPayload } from "../../../dispatcher/payloads/OpenToTabPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "../../../components/views/dialogs/UserTab";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import RestoreKeyBackupDialog from "./security/RestoreKeyBackupDialog";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
@ -138,26 +140,12 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private onSetRecoveryMethodClick = (): void => {
|
||||
if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) {
|
||||
// A key backup exists for this account, but the creating device is not
|
||||
// verified, so restore the backup which will give us the keys from it and
|
||||
// allow us to trust it (ie. upload keys to it)
|
||||
Modal.createDialog(
|
||||
RestoreKeyBackupDialog,
|
||||
undefined,
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
/* static = */ true,
|
||||
);
|
||||
} else {
|
||||
Modal.createDialog(
|
||||
lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")),
|
||||
undefined,
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
/* static = */ true,
|
||||
);
|
||||
}
|
||||
// Open the user settings dialog to the encryption tab and start the flow to reset encryption
|
||||
const payload: OpenToTabPayload = {
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Encryption,
|
||||
};
|
||||
dis.dispatch(payload);
|
||||
|
||||
// close dialog
|
||||
this.props.onFinished(true);
|
||||
@ -190,22 +178,13 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||
</div>
|
||||
);
|
||||
|
||||
let setupButtonCaption;
|
||||
if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) {
|
||||
setupButtonCaption = _t("settings|security|key_backup_connect");
|
||||
} else {
|
||||
// if there's an error fetching the backup info, we'll just assume there's
|
||||
// no backup for the purpose of the button caption
|
||||
setupButtonCaption = _t("auth|logout_dialog|use_key_backup");
|
||||
}
|
||||
|
||||
const dialogContent = (
|
||||
<div>
|
||||
<div className="mx_Dialog_content" id="mx_Dialog_content">
|
||||
{description}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={setupButtonCaption}
|
||||
primaryButton={_t("common|go_to_settings")}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
|
||||
focus={true}
|
||||
|
@ -240,8 +240,7 @@
|
||||
"setup_key_backup_title": "You'll lose access to your encrypted messages",
|
||||
"setup_secure_backup_description_1": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
|
||||
"setup_secure_backup_description_2": "When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.",
|
||||
"skip_key_backup": "I don't want my encrypted messages",
|
||||
"use_key_backup": "Start using Key Backup"
|
||||
"skip_key_backup": "I don't want my encrypted messages"
|
||||
},
|
||||
"misconfigured_body": "Ask your %(brand)s admin to check <a>your config</a> for incorrect or duplicate entries.",
|
||||
"misconfigured_title": "Your %(brand)s is misconfigured",
|
||||
@ -2712,11 +2711,6 @@
|
||||
},
|
||||
"jump_to_bottom_on_send": "Jump to the bottom of the timeline when you send a message",
|
||||
"key_backup": {
|
||||
"backup_in_progress": "Your keys are being backed up (the first backup could take a few minutes).",
|
||||
"backup_starting": "Starting backup…",
|
||||
"backup_success": "Success!",
|
||||
"cannot_create_backup": "Unable to create key backup",
|
||||
"create_title": "Create key backup",
|
||||
"setup_secure_backup": {
|
||||
"backup_setup_success_description": "Your keys are now being backed up from this device.",
|
||||
"backup_setup_success_title": "Secure Backup successful",
|
||||
@ -2877,7 +2871,6 @@
|
||||
"ignore_users_empty": "You have no ignored users.",
|
||||
"ignore_users_section": "Ignored users",
|
||||
"key_backup_algorithm": "Algorithm:",
|
||||
"key_backup_connect": "Connect this session to Key Backup",
|
||||
"message_search_disable_warning": "If disabled, messages from encrypted rooms won't appear in search results.",
|
||||
"message_search_disabled": "Securely cache encrypted messages locally for them to appear in search results.",
|
||||
"message_search_enabled": {
|
||||
|
@ -154,6 +154,7 @@ export const showToast = (kind: Kind): void => {
|
||||
|
||||
const onPrimaryClick = async (): Promise<void> => {
|
||||
switch (kind) {
|
||||
case Kind.SET_UP_RECOVERY:
|
||||
case Kind.TURN_ON_KEY_STORAGE: {
|
||||
// Open the user settings dialog to the encryption tab
|
||||
const payload: OpenToTabPayload = {
|
||||
@ -166,7 +167,6 @@ export const showToast = (kind: Kind): void => {
|
||||
case Kind.VERIFY_THIS_SESSION:
|
||||
Modal.createDialog(SetupEncryptionDialog, {}, undefined, /* priority = */ false, /* static = */ true);
|
||||
break;
|
||||
case Kind.SET_UP_RECOVERY:
|
||||
case Kind.KEY_STORAGE_OUT_OF_SYNC:
|
||||
case Kind.KEY_STORAGE_OUT_OF_SYNC_STORE: {
|
||||
const modal = Modal.createDialog(
|
||||
@ -248,7 +248,7 @@ export const showToast = (kind: Kind): void => {
|
||||
* key, to create a new 4S that we can store the secrets in.
|
||||
*/
|
||||
const onAccessSecretStorageFailed = (
|
||||
kind: Kind.SET_UP_RECOVERY | Kind.KEY_STORAGE_OUT_OF_SYNC | Kind.KEY_STORAGE_OUT_OF_SYNC_STORE,
|
||||
kind: Kind.KEY_STORAGE_OUT_OF_SYNC | Kind.KEY_STORAGE_OUT_OF_SYNC_STORE,
|
||||
error: Error,
|
||||
): void => {
|
||||
if (error instanceof AccessCancelledError) {
|
||||
|
@ -7,35 +7,24 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type User, type MatrixClient, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { CrossSigningKey, type VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
||||
import { type VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases";
|
||||
import { accessSecretStorage } from "./SecurityManager";
|
||||
import RightPanelStore from "./stores/right-panel/RightPanelStore";
|
||||
import { type IRightPanelCardState } from "./stores/right-panel/RightPanelStoreIPanelState";
|
||||
import { findDMForUser } from "./utils/dm/findDMForUser";
|
||||
|
||||
async function enable4SIfNeeded(matrixClient: MatrixClient): Promise<boolean> {
|
||||
const crypto = matrixClient.getCrypto();
|
||||
if (!crypto) return false;
|
||||
const usk = await crypto.getCrossSigningKeyId(CrossSigningKey.UserSigning);
|
||||
if (!usk) {
|
||||
await accessSecretStorage();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify another user.
|
||||
*
|
||||
* Note: cross-signing must be set up before calling this function.
|
||||
*/
|
||||
export async function verifyUser(matrixClient: MatrixClient, user: User): Promise<void> {
|
||||
if (matrixClient.isGuest()) {
|
||||
dis.dispatch({ action: "require_registration" });
|
||||
return;
|
||||
}
|
||||
if (!(await enable4SIfNeeded(matrixClient))) {
|
||||
return;
|
||||
}
|
||||
const existingRequest = pendingVerificationRequestForUser(matrixClient, user);
|
||||
setRightPanel({ member: user, verificationRequest: existingRequest });
|
||||
}
|
||||
|
@ -67,6 +67,17 @@ describe("SecurityManager", () => {
|
||||
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 () => {
|
||||
|
@ -9,7 +9,9 @@ import React from "react";
|
||||
import { render, screen, fireEvent, waitFor } from "jest-matrix-react";
|
||||
|
||||
import RecoveryMethodRemovedDialog from "../../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog";
|
||||
import Modal from "../../../../../src/Modal.tsx";
|
||||
import dispatch from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { UserTab } from "../../../../../src/components/views/dialogs/UserTab";
|
||||
|
||||
describe("<RecoveryMethodRemovedDialog />", () => {
|
||||
afterEach(() => {
|
||||
@ -18,16 +20,15 @@ describe("<RecoveryMethodRemovedDialog />", () => {
|
||||
|
||||
it("should open CreateKeyBackupDialog on primary action click", async () => {
|
||||
const onFinished = jest.fn();
|
||||
const spy = jest.spyOn(Modal, "createDialog");
|
||||
jest.mock("../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog", () => ({
|
||||
__test: true,
|
||||
__esModule: true,
|
||||
default: () => <span>mocked dialog</span>,
|
||||
}));
|
||||
jest.spyOn(dispatch, "dispatch");
|
||||
|
||||
render(<RecoveryMethodRemovedDialog onFinished={onFinished} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" }));
|
||||
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
|
||||
expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true }));
|
||||
await waitFor(() =>
|
||||
expect(dispatch.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Encryption,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -10,10 +10,13 @@ import React from "react";
|
||||
import { mocked, type MockedObject } from "jest-mock";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { type CryptoApi, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { fireEvent, render, type RenderResult, screen } from "jest-matrix-react";
|
||||
import { fireEvent, render, type RenderResult, screen, waitFor } from "jest-matrix-react";
|
||||
|
||||
import { filterConsole, getMockClientWithEventEmitter, mockClientMethodsCrypto } from "../../../../test-utils";
|
||||
import LogoutDialog from "../../../../../src/components/views/dialogs/LogoutDialog";
|
||||
import dispatch from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { UserTab } from "../../../../../src/components/views/dialogs/UserTab";
|
||||
|
||||
describe("LogoutDialog", () => {
|
||||
let mockClient: MockedObject<MatrixClient>;
|
||||
@ -56,17 +59,26 @@ describe("LogoutDialog", () => {
|
||||
await rendered.findByText("You'll lose access to your encrypted messages");
|
||||
});
|
||||
|
||||
it("Prompts user to connect backup if there is a backup on the server", async () => {
|
||||
it("Prompts user to go to settings if there is a backup on the server", async () => {
|
||||
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo);
|
||||
const rendered = renderComponent();
|
||||
await rendered.findByText("Connect this session to Key Backup");
|
||||
await rendered.findByText("Go to Settings");
|
||||
expect(rendered.container).toMatchSnapshot();
|
||||
|
||||
jest.spyOn(dispatch, "dispatch");
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Go to Settings" }));
|
||||
await waitFor(() =>
|
||||
expect(dispatch.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Encryption,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("Prompts user to set up backup if there is no backup on the server", async () => {
|
||||
it("Prompts user to go to settings if there is no backup on the server", async () => {
|
||||
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
|
||||
const rendered = renderComponent();
|
||||
await rendered.findByText("Start using Key Backup");
|
||||
await rendered.findByText("Go to Settings");
|
||||
expect(rendered.container).toMatchSnapshot();
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Manually export keys" }));
|
||||
@ -75,12 +87,12 @@ describe("LogoutDialog", () => {
|
||||
|
||||
describe("when there is an error fetching backups", () => {
|
||||
filterConsole("Unable to fetch key backup status");
|
||||
it("prompts user to set up backup", async () => {
|
||||
it("prompts user to go to settings", async () => {
|
||||
mockCrypto.getKeyBackupInfo.mockImplementation(async () => {
|
||||
throw new Error("beep");
|
||||
});
|
||||
const rendered = renderComponent();
|
||||
await rendered.findByText("Start using Key Backup");
|
||||
await rendered.findByText("Go to Settings");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LogoutDialog Prompts user to connect backup if there is a backup on the server 1`] = `
|
||||
exports[`LogoutDialog Prompts user to go to settings if there is a backup on the server 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
@ -55,7 +55,7 @@ exports[`LogoutDialog Prompts user to connect backup if there is a backup on the
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Connect this session to Key Backup
|
||||
Go to Settings
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@ -87,7 +87,7 @@ exports[`LogoutDialog Prompts user to connect backup if there is a backup on the
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LogoutDialog Prompts user to set up backup if there is no backup on the server 1`] = `
|
||||
exports[`LogoutDialog Prompts user to go to settings if there is no backup on the server 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
@ -142,7 +142,7 @@ exports[`LogoutDialog Prompts user to set up backup if there is no backup on the
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Start using Key Backup
|
||||
Go to Settings
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -1,77 +0,0 @@
|
||||
/*
|
||||
* 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 { render, screen, waitFor } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import CreateKeyBackupDialog from "../../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog";
|
||||
import { createTestClient } from "../../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
|
||||
jest.mock("../../../../../../src/SecurityManager", () => ({
|
||||
accessSecretStorage: jest.fn().mockResolvedValue(undefined),
|
||||
withSecretStorageKeyCache: jest.fn().mockImplementation((fn) => fn()),
|
||||
}));
|
||||
|
||||
describe("CreateKeyBackupDialog", () => {
|
||||
beforeEach(() => {
|
||||
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => createTestClient();
|
||||
});
|
||||
|
||||
it("should display the spinner when creating backup", () => {
|
||||
const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
|
||||
|
||||
// Check if the spinner is displayed
|
||||
expect(screen.getByTestId("spinner")).toBeDefined();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display an error message when backup creation failed", async () => {
|
||||
const matrixClient = createTestClient();
|
||||
jest.spyOn(matrixClient.secretStorage, "hasKey").mockResolvedValue(true);
|
||||
mocked(matrixClient.getCrypto()!.resetKeyBackup).mockImplementation(() => {
|
||||
throw new Error("failed");
|
||||
});
|
||||
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
|
||||
|
||||
const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
|
||||
|
||||
// Check if the error message is displayed
|
||||
await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined());
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display an error message when there is no Crypto available", async () => {
|
||||
const matrixClient = createTestClient();
|
||||
jest.spyOn(matrixClient.secretStorage, "hasKey").mockResolvedValue(true);
|
||||
mocked(matrixClient.getCrypto).mockReturnValue(undefined);
|
||||
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
|
||||
|
||||
render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
|
||||
|
||||
// Check if the error message is displayed
|
||||
await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined());
|
||||
});
|
||||
|
||||
it("should display the success dialog when the key backup is finished", async () => {
|
||||
const onFinished = jest.fn();
|
||||
const { asFragment } = render(<CreateKeyBackupDialog onFinished={onFinished} />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText("Your keys are being backed up (the first backup could take a few minutes)."),
|
||||
).toBeDefined(),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
// Click on the OK button
|
||||
screen.getByRole("button", { name: "OK" }).click();
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
@ -13,7 +13,7 @@ import { mocked, type MockedObject } from "jest-mock";
|
||||
import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { filterConsole, flushPromises, stubClient } from "../../../../../test-utils";
|
||||
import { filterConsole, stubClient } from "../../../../../test-utils";
|
||||
import CreateSecretStorageDialog from "../../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog";
|
||||
|
||||
describe("CreateSecretStorageDialog", () => {
|
||||
@ -97,39 +97,4 @@ describe("CreateSecretStorageDialog", () => {
|
||||
await screen.findByText("Your keys are now being backed up from this device.");
|
||||
});
|
||||
});
|
||||
|
||||
it("resets keys in the right order when resetting secret storage and cross-signing", async () => {
|
||||
const result = renderComponent({ forceReset: true, resetCrossSigning: true });
|
||||
|
||||
await result.findByText(/Set up Secure Backup/);
|
||||
jest.spyOn(mockClient.getCrypto()!, "createRecoveryKeyFromPassphrase").mockResolvedValue({
|
||||
privateKey: new Uint8Array(),
|
||||
encodedPrivateKey: "abcd efgh ijkl",
|
||||
});
|
||||
result.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await result.findByText(/Save your Recovery Key/);
|
||||
result.getByRole("button", { name: "Copy" }).click();
|
||||
|
||||
// Resetting should reset secret storage, cross signing, and key
|
||||
// backup. We make sure that all three are reset, and done in the
|
||||
// right order.
|
||||
const resetFunctionCallLog: string[] = [];
|
||||
jest.spyOn(mockClient.getCrypto()!, "bootstrapSecretStorage").mockImplementation(async () => {
|
||||
resetFunctionCallLog.push("bootstrapSecretStorage");
|
||||
});
|
||||
jest.spyOn(mockClient.getCrypto()!, "bootstrapCrossSigning").mockImplementation(async () => {
|
||||
resetFunctionCallLog.push("bootstrapCrossSigning");
|
||||
});
|
||||
jest.spyOn(mockClient.getCrypto()!, "resetKeyBackup").mockImplementation(async () => {
|
||||
resetFunctionCallLog.push("resetKeyBackup");
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
result.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await result.findByText("Your keys are now being backed up from this device.");
|
||||
|
||||
expect(resetFunctionCallLog).toEqual(["bootstrapSecretStorage", "bootstrapCrossSigning", "resetKeyBackup"]);
|
||||
});
|
||||
});
|
||||
|
@ -1,168 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreateKeyBackupDialog should display an error message when backup creation failed 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateKeyBackupDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Starting backup…
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<p>
|
||||
Unable to create key backup
|
||||
</p>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
data-testid="dialog-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`CreateKeyBackupDialog should display the spinner when creating backup 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateKeyBackupDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Starting backup…
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`CreateKeyBackupDialog should display the success dialog when the key backup is finished 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateKeyBackupDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Success!
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<p>
|
||||
Your keys are being backed up (the first backup could take a few minutes).
|
||||
</p>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
Loading…
Reference in New Issue
Block a user