Merge matrix-react-sdk into element-web
Merge remote-tracking branch 'repomerge/t3chguy/repomerge' into t3chguy/repo-merge Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
commit
f0ee7f7905
3265 changed files with 484599 additions and 699 deletions
|
@ -7,10 +7,10 @@ Please see LICENSE files in the repository root for full details.
|
|||
|
||||
import React, { ReactNode } from "react";
|
||||
import { Text, Heading, Button, Separator } from "@vector-im/compound-web";
|
||||
import SdkConfig from "matrix-react-sdk/src/SdkConfig";
|
||||
import { Flex } from "matrix-react-sdk/src/components/utils/Flex";
|
||||
import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";
|
||||
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import { Flex } from "../../components/utils/Flex";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { Icon as AppleIcon } from "../../../res/themes/element/img/compound/apple.svg";
|
||||
import { Icon as MicrosoftIcon } from "../../../res/themes/element/img/compound/microsoft.svg";
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import Spinner from "../../../../components/views/elements/Spinner";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
import dis from "../../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
||||
import { Action } from "../../../../dispatcher/actions";
|
||||
import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success?: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
disabling: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
* Allows the user to disable the Event Index.
|
||||
*/
|
||||
export default class DisableEventIndexDialog extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
disabling: false,
|
||||
};
|
||||
}
|
||||
|
||||
private onDisable = async (): Promise<void> => {
|
||||
this.setState({
|
||||
disabling: true,
|
||||
});
|
||||
|
||||
await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, false);
|
||||
await EventIndexPeg.deleteEventIndex();
|
||||
this.props.onFinished(true);
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<BaseDialog onFinished={this.props.onFinished} title={_t("common|are_you_sure")}>
|
||||
{_t("settings|security|message_search_disable_warning")}
|
||||
{this.state.disabling ? <Spinner /> : <div />}
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|disable")}
|
||||
onPrimaryButtonClick={this.onDisable}
|
||||
primaryButtonClass="danger"
|
||||
cancelButtonClass="warning"
|
||||
onCancel={this.props.onFinished}
|
||||
disabled={this.state.disabling}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import SdkConfig from "../../../../SdkConfig";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
import Modal from "../../../../Modal";
|
||||
import { formatBytes, formatCountLong } from "../../../../utils/FormattingUtils";
|
||||
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
||||
import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||
import Field from "../../../../components/views/elements/Field";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
import { IIndexStats } from "../../../../indexing/BaseEventIndexManager";
|
||||
|
||||
interface IProps {
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
eventIndexSize: number;
|
||||
eventCount: number;
|
||||
crawlingRoomsCount: number;
|
||||
roomCount: number;
|
||||
currentRoom: string | null;
|
||||
crawlerSleepTime: number;
|
||||
}
|
||||
|
||||
/*
|
||||
* Allows the user to introspect the event index state and disable it.
|
||||
*/
|
||||
export default class ManageEventIndexDialog extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
eventIndexSize: 0,
|
||||
eventCount: 0,
|
||||
crawlingRoomsCount: 0,
|
||||
roomCount: 0,
|
||||
currentRoom: null,
|
||||
crawlerSleepTime: SettingsStore.getValueAt(SettingLevel.DEVICE, "crawlerSleepTime"),
|
||||
};
|
||||
}
|
||||
|
||||
public updateCurrentRoom = async (room: Room): Promise<void> => {
|
||||
const eventIndex = EventIndexPeg.get();
|
||||
if (!eventIndex) return;
|
||||
let stats: IIndexStats | undefined;
|
||||
|
||||
try {
|
||||
stats = await eventIndex.getStats();
|
||||
} catch {
|
||||
// This call may fail if sporadically, not a huge issue as we will
|
||||
// try later again and probably succeed.
|
||||
return;
|
||||
}
|
||||
|
||||
let currentRoom: string | null = null;
|
||||
|
||||
if (room) currentRoom = room.name;
|
||||
const roomStats = eventIndex.crawlingRooms();
|
||||
const crawlingRoomsCount = roomStats.crawlingRooms.size;
|
||||
const roomCount = roomStats.totalRooms.size;
|
||||
|
||||
this.setState({
|
||||
eventIndexSize: stats?.size ?? 0,
|
||||
eventCount: stats?.eventCount ?? 0,
|
||||
crawlingRoomsCount: crawlingRoomsCount,
|
||||
roomCount: roomCount,
|
||||
currentRoom: currentRoom,
|
||||
});
|
||||
};
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
const eventIndex = EventIndexPeg.get();
|
||||
|
||||
if (eventIndex !== null) {
|
||||
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom);
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount(): Promise<void> {
|
||||
let eventIndexSize = 0;
|
||||
let crawlingRoomsCount = 0;
|
||||
let roomCount = 0;
|
||||
let eventCount = 0;
|
||||
let currentRoom: string | null = null;
|
||||
|
||||
const eventIndex = EventIndexPeg.get();
|
||||
|
||||
if (eventIndex !== null) {
|
||||
eventIndex.on("changedCheckpoint", this.updateCurrentRoom);
|
||||
|
||||
try {
|
||||
const stats = await eventIndex.getStats();
|
||||
if (stats) {
|
||||
eventIndexSize = stats.size;
|
||||
eventCount = stats.eventCount;
|
||||
}
|
||||
} catch {
|
||||
// This call may fail if sporadically, not a huge issue as we
|
||||
// will try later again in the updateCurrentRoom call and
|
||||
// probably succeed.
|
||||
}
|
||||
|
||||
const roomStats = eventIndex.crawlingRooms();
|
||||
crawlingRoomsCount = roomStats.crawlingRooms.size;
|
||||
roomCount = roomStats.totalRooms.size;
|
||||
|
||||
const room = eventIndex.currentRoom();
|
||||
if (room) currentRoom = room.name;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
eventIndexSize,
|
||||
eventCount,
|
||||
crawlingRoomsCount,
|
||||
roomCount,
|
||||
currentRoom,
|
||||
});
|
||||
}
|
||||
|
||||
private onDisable = async (): Promise<void> => {
|
||||
const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default;
|
||||
Modal.createDialog(DisableEventIndexDialog, undefined, undefined, /* priority = */ false, /* static = */ true);
|
||||
};
|
||||
|
||||
private onCrawlerSleepTimeChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({ crawlerSleepTime: parseInt(e.target.value, 10) });
|
||||
SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
let crawlerState;
|
||||
if (this.state.currentRoom === null) {
|
||||
crawlerState = _t("settings|security|message_search_indexing_idle");
|
||||
} else {
|
||||
crawlerState = _t("settings|security|message_search_indexing", { currentRoom: this.state.currentRoom });
|
||||
}
|
||||
|
||||
const doneRooms = Math.max(0, this.state.roomCount - this.state.crawlingRoomsCount);
|
||||
|
||||
const eventIndexingSettings = (
|
||||
<div>
|
||||
{_t("settings|security|message_search_intro", {
|
||||
brand,
|
||||
})}
|
||||
<div className="mx_SettingsTab_subsectionText">
|
||||
{crawlerState}
|
||||
<br />
|
||||
{_t("settings|security|message_search_space_used")} {formatBytes(this.state.eventIndexSize, 0)}
|
||||
<br />
|
||||
{_t("settings|security|message_search_indexed_messages")} {formatCountLong(this.state.eventCount)}
|
||||
<br />
|
||||
{_t("settings|security|message_search_indexed_rooms")}{" "}
|
||||
{_t("settings|security|message_search_room_progress", {
|
||||
doneRooms: formatCountLong(doneRooms),
|
||||
totalRooms: formatCountLong(this.state.roomCount),
|
||||
})}{" "}
|
||||
<br />
|
||||
<Field
|
||||
label={_t("settings|security|message_search_sleep_time")}
|
||||
type="number"
|
||||
value={this.state.crawlerSleepTime.toString()}
|
||||
onChange={this.onCrawlerSleepTimeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_ManageEventIndexDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("settings|security|message_search_section")}
|
||||
>
|
||||
{eventIndexingSettings}
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|done")}
|
||||
onPrimaryButtonClick={this.props.onFinished}
|
||||
primaryButtonClass="primary"
|
||||
cancelButton={_t("action|disable")}
|
||||
onCancel={this.onDisable}
|
||||
cancelButtonClass="danger"
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React 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.hasSecretStorageKey();
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,954 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019, 2020 , 2023 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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
import FileSaver from "file-saver";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
import classNames from "classnames";
|
||||
import { BackupTrustInfo, GeneratedSecretStorageKey, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import { _t, _td } from "../../../../languageHandler";
|
||||
import Modal from "../../../../Modal";
|
||||
import { copyNode } from "../../../../utils/strings";
|
||||
import { SSOAuthEntry } from "../../../../components/views/auth/InteractiveAuthEntryComponents";
|
||||
import PassphraseField from "../../../../components/views/auth/PassphraseField";
|
||||
import StyledRadioButton from "../../../../components/views/elements/StyledRadioButton";
|
||||
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
|
||||
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
|
||||
import {
|
||||
getSecureBackupSetupMethods,
|
||||
isSecureBackupRequired,
|
||||
SecureBackupSetupMethod,
|
||||
} from "../../../../utils/WellKnownUtils";
|
||||
import { ModuleRunner } from "../../../../modules/ModuleRunner";
|
||||
import Field from "../../../../components/views/elements/Field";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import Spinner from "../../../../components/views/elements/Spinner";
|
||||
import InteractiveAuthDialog from "../../../../components/views/dialogs/InteractiveAuthDialog";
|
||||
import { IValidationResult } from "../../../../components/views/elements/Validation";
|
||||
import { Icon as CheckmarkIcon } from "../../../../../res/img/element-icons/check.svg";
|
||||
import PassphraseConfirmField from "../../../../components/views/auth/PassphraseConfirmField";
|
||||
import { initialiseDehydration } from "../../../../utils/device/dehydration";
|
||||
|
||||
// I made a mistake while converting this and it has to be fixed!
|
||||
enum Phase {
|
||||
Loading = "loading",
|
||||
LoadError = "load_error",
|
||||
ChooseKeyPassphrase = "choose_key_passphrase",
|
||||
Migrate = "migrate",
|
||||
Passphrase = "passphrase",
|
||||
PassphraseConfirm = "passphrase_confirm",
|
||||
ShowKey = "show_key",
|
||||
Storing = "storing",
|
||||
Stored = "stored",
|
||||
ConfirmSkip = "confirm_skip",
|
||||
}
|
||||
|
||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
||||
|
||||
interface IProps {
|
||||
hasCancel?: boolean;
|
||||
accountPassword?: string;
|
||||
forceReset?: boolean;
|
||||
onFinished(ok?: boolean): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
passPhrase: string;
|
||||
passPhraseValid: boolean;
|
||||
passPhraseConfirm: string;
|
||||
copied: boolean;
|
||||
downloaded: boolean;
|
||||
setPassphrase: boolean;
|
||||
|
||||
/** Information on the current key backup version, as returned by the server.
|
||||
*
|
||||
* `null` could mean any of:
|
||||
* * we haven't yet requested the data from the server.
|
||||
* * we were unable to reach the server.
|
||||
* * the server returned key backup version data we didn't understand or was malformed.
|
||||
* * there is actually no backup on the server.
|
||||
*/
|
||||
backupInfo: KeyBackupInfo | null;
|
||||
|
||||
/**
|
||||
* Information on whether the backup in `backupInfo` is correctly signed, and whether we have the right key to
|
||||
* decrypt it.
|
||||
*
|
||||
* `undefined` if `backupInfo` is null, or if crypto is not enabled in the client.
|
||||
*/
|
||||
backupTrustInfo: BackupTrustInfo | undefined;
|
||||
|
||||
// does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
canUploadKeysWithPasswordOnly: boolean | null;
|
||||
accountPassword: string;
|
||||
accountPasswordCorrect: boolean | null;
|
||||
canSkip: boolean;
|
||||
passPhraseKeySelected: string;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks the user through the process of creating a 4S passphrase and bootstrapping secret storage.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export default class CreateSecretStorageDialog extends React.PureComponent<IProps, IState> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
hasCancel: true,
|
||||
forceReset: false,
|
||||
};
|
||||
private recoveryKey?: GeneratedSecretStorageKey;
|
||||
private recoveryKeyNode = createRef<HTMLElement>();
|
||||
private passphraseField = createRef<Field>();
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
|
||||
let passPhraseKeySelected: SecureBackupSetupMethod;
|
||||
const setupMethods = getSecureBackupSetupMethods(cli);
|
||||
if (setupMethods.includes(SecureBackupSetupMethod.Key)) {
|
||||
passPhraseKeySelected = SecureBackupSetupMethod.Key;
|
||||
} else {
|
||||
passPhraseKeySelected = SecureBackupSetupMethod.Passphrase;
|
||||
}
|
||||
|
||||
const accountPassword = props.accountPassword || "";
|
||||
let canUploadKeysWithPasswordOnly: boolean | null = null;
|
||||
if (accountPassword) {
|
||||
// If we have an account password in memory, let's simplify and
|
||||
// assume it means password auth is also supported for device
|
||||
// signing key upload as well. This avoids hitting the server to
|
||||
// test auth flows, which may be slow under high load.
|
||||
canUploadKeysWithPasswordOnly = true;
|
||||
} else {
|
||||
this.queryKeyUploadAuth();
|
||||
}
|
||||
|
||||
this.state = {
|
||||
phase: Phase.Loading,
|
||||
passPhrase: "",
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: "",
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
setPassphrase: false,
|
||||
backupInfo: null,
|
||||
backupTrustInfo: undefined,
|
||||
// does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
accountPasswordCorrect: null,
|
||||
canSkip: !isSecureBackupRequired(cli),
|
||||
canUploadKeysWithPasswordOnly,
|
||||
passPhraseKeySelected,
|
||||
accountPassword,
|
||||
};
|
||||
|
||||
cli.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChange);
|
||||
|
||||
this.getInitialPhase();
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
MatrixClientPeg.get()?.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChange);
|
||||
}
|
||||
|
||||
private getInitialPhase(): void {
|
||||
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.createSecretStorageKey();
|
||||
if (keyFromCustomisations) {
|
||||
logger.log("CryptoSetupExtension: Created key via extension, jumping to bootstrap step");
|
||||
this.recoveryKey = {
|
||||
privateKey: keyFromCustomisations,
|
||||
};
|
||||
this.bootstrapSecretStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchBackupInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get information on the current backup from the server, and update the state.
|
||||
*
|
||||
* Updates {@link IState.backupInfo} and {@link IState.backupTrustInfo}, and picks an appropriate phase for
|
||||
* {@link IState.phase}.
|
||||
*
|
||||
* @returns If the backup data was retrieved successfully, the trust info for the backup. Otherwise, undefined.
|
||||
*/
|
||||
private async fetchBackupInfo(): Promise<BackupTrustInfo | undefined> {
|
||||
try {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
const backupTrustInfo =
|
||||
// we may not have started crypto yet, in which case we definitely don't trust the backup
|
||||
backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined;
|
||||
|
||||
const { forceReset } = this.props;
|
||||
const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase;
|
||||
|
||||
this.setState({
|
||||
phase,
|
||||
backupInfo,
|
||||
backupTrustInfo,
|
||||
});
|
||||
|
||||
return backupTrustInfo;
|
||||
} catch (e) {
|
||||
console.error("Error fetching backup data from server", e);
|
||||
this.setState({ phase: Phase.LoadError });
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async queryKeyUploadAuth(): Promise<void> {
|
||||
try {
|
||||
await MatrixClientPeg.safeGet().uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
|
||||
// We should never get here: the server should always require
|
||||
// UI auth to upload device signing keys. If we do, we upload
|
||||
// no keys which would be a no-op.
|
||||
logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
|
||||
} catch (error) {
|
||||
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
|
||||
logger.log("uploadDeviceSigningKeys advertised no flows!");
|
||||
return;
|
||||
}
|
||||
const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => {
|
||||
return f.stages.length === 1 && f.stages[0] === "m.login.password";
|
||||
});
|
||||
this.setState({
|
||||
canUploadKeysWithPasswordOnly,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onKeyBackupStatusChange = (): void => {
|
||||
if (this.state.phase === Phase.Migrate) this.fetchBackupInfo();
|
||||
};
|
||||
|
||||
private onKeyPassphraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
passPhraseKeySelected: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
private onChooseKeyPassphraseFormSubmit = async (): Promise<void> => {
|
||||
if (this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key) {
|
||||
this.recoveryKey = await MatrixClientPeg.safeGet().getCrypto()!.createRecoveryKeyFromPassphrase();
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
setPassphrase: false,
|
||||
phase: Phase.ShowKey,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
phase: Phase.Passphrase,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onMigrateFormSubmit = (e: React.FormEvent): void => {
|
||||
e.preventDefault();
|
||||
if (this.state.backupTrustInfo?.trusted) {
|
||||
this.bootstrapSecretStorage();
|
||||
} else {
|
||||
this.restoreBackup();
|
||||
}
|
||||
};
|
||||
|
||||
private onCopyClick = (): void => {
|
||||
const successful = copyNode(this.recoveryKeyNode.current);
|
||||
if (successful) {
|
||||
this.setState({
|
||||
copied: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onDownloadClick = (): void => {
|
||||
if (!this.recoveryKey) return;
|
||||
const blob = new Blob([this.recoveryKey.encodedPrivateKey!], {
|
||||
type: "text/plain;charset=us-ascii",
|
||||
});
|
||||
FileSaver.saveAs(blob, "security-key.txt");
|
||||
|
||||
this.setState({
|
||||
downloaded: true,
|
||||
});
|
||||
};
|
||||
|
||||
private doBootstrapUIAuth = async (
|
||||
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
|
||||
): Promise<void> => {
|
||||
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
await makeRequest({
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: MatrixClientPeg.safeGet().getSafeUserId(),
|
||||
},
|
||||
password: this.state.accountPassword,
|
||||
});
|
||||
} else {
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("auth|uia|sso_title"),
|
||||
body: _t("auth|uia|sso_preauth_body"),
|
||||
continueText: _t("auth|sso"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
title: _t("encryption|confirm_encryption_setup_title"),
|
||||
body: _t("encryption|confirm_encryption_setup_body"),
|
||||
continueText: _t("action|confirm"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||
title: _t("encryption|bootstrap_title"),
|
||||
matrixClient: MatrixClientPeg.safeGet(),
|
||||
makeRequest,
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
},
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private bootstrapSecretStorage = async (): Promise<void> => {
|
||||
this.setState({
|
||||
phase: Phase.Storing,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const crypto = cli.getCrypto()!;
|
||||
|
||||
const { forceReset } = this.props;
|
||||
|
||||
try {
|
||||
if (forceReset) {
|
||||
logger.log("Forcing secret storage reset");
|
||||
await crypto.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => this.recoveryKey!,
|
||||
setupNewKeyBackup: true,
|
||||
setupNewSecretStorage: true,
|
||||
});
|
||||
} else {
|
||||
// For password authentication users after 2020-09, this cross-signing
|
||||
// step will be a no-op since it is now setup during registration or login
|
||||
// when needed. We should keep this here to cover other cases such as:
|
||||
// * Users with existing sessions prior to 2020-09 changes
|
||||
// * SSO authentication users which require interactive auth to upload
|
||||
// keys (and also happen to skip all post-authentication flows at the
|
||||
// moment via token login)
|
||||
await crypto.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
|
||||
});
|
||||
await crypto.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => this.recoveryKey!,
|
||||
keyBackupInfo: this.state.backupInfo!,
|
||||
setupNewKeyBackup: !this.state.backupInfo,
|
||||
});
|
||||
}
|
||||
await initialiseDehydration(true);
|
||||
|
||||
this.setState({
|
||||
phase: Phase.Stored,
|
||||
});
|
||||
} catch (e) {
|
||||
if (
|
||||
this.state.canUploadKeysWithPasswordOnly &&
|
||||
e instanceof MatrixError &&
|
||||
e.httpStatus === 401 &&
|
||||
e.data.flows
|
||||
) {
|
||||
this.setState({
|
||||
accountPassword: "",
|
||||
accountPasswordCorrect: false,
|
||||
phase: Phase.Migrate,
|
||||
});
|
||||
} else {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
logger.error("Error bootstrapping secret storage", e);
|
||||
}
|
||||
};
|
||||
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private restoreBackup = async (): Promise<void> => {
|
||||
const keyCallback = (k: Uint8Array): void => {};
|
||||
|
||||
const { finished } = Modal.createDialog(
|
||||
RestoreKeyBackupDialog,
|
||||
{
|
||||
showSummary: false,
|
||||
keyCallback,
|
||||
},
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
/* static = */ false,
|
||||
);
|
||||
|
||||
await finished;
|
||||
const backupTrustInfo = await this.fetchBackupInfo();
|
||||
if (backupTrustInfo?.trusted && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
this.bootstrapSecretStorage();
|
||||
}
|
||||
};
|
||||
|
||||
private onLoadRetryClick = (): void => {
|
||||
this.setState({ phase: Phase.Loading });
|
||||
this.fetchBackupInfo();
|
||||
};
|
||||
|
||||
private onShowKeyContinueClick = (): void => {
|
||||
this.bootstrapSecretStorage();
|
||||
};
|
||||
|
||||
private onCancelClick = (): void => {
|
||||
this.setState({ phase: Phase.ConfirmSkip });
|
||||
};
|
||||
|
||||
private onGoBackClick = (): void => {
|
||||
this.setState({ phase: Phase.ChooseKeyPassphrase });
|
||||
};
|
||||
|
||||
private onPassPhraseNextClick = async (e: React.FormEvent): Promise<void> => {
|
||||
e.preventDefault();
|
||||
if (!this.passphraseField.current) return; // unmounting
|
||||
|
||||
await this.passphraseField.current.validate({ allowEmpty: false });
|
||||
if (!this.passphraseField.current.state.valid) {
|
||||
this.passphraseField.current.focus();
|
||||
this.passphraseField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ phase: Phase.PassphraseConfirm });
|
||||
};
|
||||
|
||||
private onPassPhraseConfirmNextClick = async (e: React.FormEvent): Promise<void> => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
|
||||
|
||||
this.recoveryKey = await MatrixClientPeg.safeGet()
|
||||
.getCrypto()!
|
||||
.createRecoveryKeyFromPassphrase(this.state.passPhrase);
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
setPassphrase: true,
|
||||
phase: Phase.ShowKey,
|
||||
});
|
||||
};
|
||||
|
||||
private onSetAgainClick = (): void => {
|
||||
this.setState({
|
||||
passPhrase: "",
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: "",
|
||||
phase: Phase.Passphrase,
|
||||
});
|
||||
};
|
||||
|
||||
private onPassPhraseValidate = (result: IValidationResult): void => {
|
||||
this.setState({
|
||||
passPhraseValid: !!result.valid,
|
||||
});
|
||||
};
|
||||
|
||||
private onPassPhraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
passPhrase: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
private onPassPhraseConfirmChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
passPhraseConfirm: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
private onAccountPasswordChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
accountPassword: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
private renderOptionKey(): JSX.Element {
|
||||
return (
|
||||
<StyledRadioButton
|
||||
key={SecureBackupSetupMethod.Key}
|
||||
value={SecureBackupSetupMethod.Key}
|
||||
name="keyPassphrase"
|
||||
checked={this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key}
|
||||
onChange={this.onKeyPassphraseChange}
|
||||
outlined
|
||||
>
|
||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup" />
|
||||
{_t("settings|key_backup|setup_secure_backup|generate_security_key_title")}
|
||||
</div>
|
||||
<div>{_t("settings|key_backup|setup_secure_backup|generate_security_key_description")}</div>
|
||||
</StyledRadioButton>
|
||||
);
|
||||
}
|
||||
|
||||
private renderOptionPassphrase(): JSX.Element {
|
||||
return (
|
||||
<StyledRadioButton
|
||||
key={SecureBackupSetupMethod.Passphrase}
|
||||
value={SecureBackupSetupMethod.Passphrase}
|
||||
name="keyPassphrase"
|
||||
checked={this.state.passPhraseKeySelected === SecureBackupSetupMethod.Passphrase}
|
||||
onChange={this.onKeyPassphraseChange}
|
||||
outlined
|
||||
>
|
||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase" />
|
||||
{_t("settings|key_backup|setup_secure_backup|enter_phrase_title")}
|
||||
</div>
|
||||
<div>{_t("settings|key_backup|setup_secure_backup|use_phrase_only_you_know")}</div>
|
||||
</StyledRadioButton>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPhaseChooseKeyPassphrase(): JSX.Element {
|
||||
const setupMethods = getSecureBackupSetupMethods(MatrixClientPeg.safeGet());
|
||||
const optionKey = setupMethods.includes(SecureBackupSetupMethod.Key) ? this.renderOptionKey() : null;
|
||||
const optionPassphrase = setupMethods.includes(SecureBackupSetupMethod.Passphrase)
|
||||
? this.renderOptionPassphrase()
|
||||
: null;
|
||||
|
||||
return (
|
||||
<form onSubmit={this.onChooseKeyPassphraseFormSubmit}>
|
||||
<p className="mx_CreateSecretStorageDialog_centeredBody">
|
||||
{_t("settings|key_backup|setup_secure_backup|description")}
|
||||
</p>
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
|
||||
{optionKey}
|
||||
{optionPassphrase}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|continue")}
|
||||
onPrimaryButtonClick={this.onChooseKeyPassphraseFormSubmit}
|
||||
onCancel={this.onCancelClick}
|
||||
hasCancel={this.state.canSkip}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPhaseMigrate(): JSX.Element {
|
||||
let authPrompt;
|
||||
let nextCaption = _t("action|next");
|
||||
if (this.state.canUploadKeysWithPasswordOnly) {
|
||||
authPrompt = (
|
||||
<div>
|
||||
<div>{_t("settings|key_backup|setup_secure_backup|requires_password_confirmation")}</div>
|
||||
<div>
|
||||
<Field
|
||||
id="mx_CreateSecretStorageDialog_password"
|
||||
type="password"
|
||||
label={_t("common|password")}
|
||||
value={this.state.accountPassword}
|
||||
onChange={this.onAccountPasswordChange}
|
||||
forceValidity={this.state.accountPasswordCorrect === false ? false : undefined}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (!this.state.backupTrustInfo?.trusted) {
|
||||
authPrompt = (
|
||||
<div>
|
||||
<div>{_t("settings|key_backup|setup_secure_backup|requires_key_restore")}</div>
|
||||
</div>
|
||||
);
|
||||
nextCaption = _t("action|restore");
|
||||
} else {
|
||||
authPrompt = <p>{_t("settings|key_backup|setup_secure_backup|requires_server_authentication")}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.onMigrateFormSubmit}>
|
||||
<p>{_t("settings|key_backup|setup_secure_backup|session_upgrade_description")}</p>
|
||||
<div>{authPrompt}</div>
|
||||
<DialogButtons
|
||||
primaryButton={nextCaption}
|
||||
onPrimaryButtonClick={this.onMigrateFormSubmit}
|
||||
hasCancel={false}
|
||||
primaryDisabled={!!this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
||||
>
|
||||
<button type="button" className="danger" onClick={this.onCancelClick}>
|
||||
{_t("action|skip")}
|
||||
</button>
|
||||
</DialogButtons>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPhasePassPhrase(): JSX.Element {
|
||||
return (
|
||||
<form onSubmit={this.onPassPhraseNextClick}>
|
||||
<p>{_t("settings|key_backup|setup_secure_backup|enter_phrase_description")}</p>
|
||||
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<PassphraseField
|
||||
id="mx_passPhraseInput"
|
||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||
onChange={this.onPassPhraseChange}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.passPhrase}
|
||||
onValidate={this.onPassPhraseValidate}
|
||||
fieldRef={this.passphraseField}
|
||||
autoFocus={true}
|
||||
label={_td("settings|key_backup|setup_secure_backup|enter_phrase_title")}
|
||||
labelEnterPassword={_td("settings|key_backup|setup_secure_backup|enter_phrase_title")}
|
||||
labelStrongPassword={_td("settings|key_backup|setup_secure_backup|phrase_strong_enough")}
|
||||
labelAllowedButUnsafe={_td("settings|key_backup|setup_secure_backup|phrase_strong_enough")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|continue")}
|
||||
onPrimaryButtonClick={this.onPassPhraseNextClick}
|
||||
hasCancel={false}
|
||||
disabled={!this.state.passPhraseValid}
|
||||
>
|
||||
<button type="button" onClick={this.onCancelClick} className="danger">
|
||||
{_t("action|cancel")}
|
||||
</button>
|
||||
</DialogButtons>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPhasePassPhraseConfirm(): JSX.Element {
|
||||
let matchText;
|
||||
let changeText;
|
||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||
matchText = _t("settings|key_backup|setup_secure_backup|pass_phrase_match_success");
|
||||
changeText = _t("settings|key_backup|setup_secure_backup|use_different_passphrase");
|
||||
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
|
||||
// only tell them they're wrong if they've actually gone wrong.
|
||||
// Security conscious readers will note that if you left element-web unattended
|
||||
// on this screen, this would make it easy for a malicious person to guess
|
||||
// your passphrase one letter at a time, but they could get this faster by
|
||||
// just opening the browser's developer tools and reading it.
|
||||
// Note that not having typed anything at all will not hit this clause and
|
||||
// fall through so empty box === no hint.
|
||||
matchText = _t("settings|key_backup|setup_secure_backup|pass_phrase_match_failed");
|
||||
changeText = _t("settings|key_backup|setup_secure_backup|set_phrase_again");
|
||||
}
|
||||
|
||||
let passPhraseMatch: JSX.Element | undefined;
|
||||
if (matchText) {
|
||||
passPhraseMatch = (
|
||||
<div>
|
||||
<div>{matchText}</div>
|
||||
<AccessibleButton kind="link" onClick={this.onSetAgainClick}>
|
||||
{changeText}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<form onSubmit={this.onPassPhraseConfirmNextClick}>
|
||||
<p>{_t("settings|key_backup|setup_secure_backup|enter_phrase_to_confirm")}</p>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<PassphraseConfirmField
|
||||
id="mx_passPhraseInput"
|
||||
onChange={this.onPassPhraseConfirmChange}
|
||||
value={this.state.passPhraseConfirm}
|
||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||
label={_td("settings|key_backup|setup_secure_backup|confirm_security_phrase")}
|
||||
labelRequired={_td("settings|key_backup|setup_secure_backup|confirm_security_phrase")}
|
||||
labelInvalid={_td("settings|key_backup|setup_secure_backup|pass_phrase_match_failed")}
|
||||
autoFocus={true}
|
||||
password={this.state.passPhrase}
|
||||
/>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">{passPhraseMatch}</div>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|continue")}
|
||||
onPrimaryButtonClick={this.onPassPhraseConfirmNextClick}
|
||||
hasCancel={false}
|
||||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||
>
|
||||
<button type="button" onClick={this.onCancelClick} className="danger">
|
||||
{_t("action|skip")}
|
||||
</button>
|
||||
</DialogButtons>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPhaseShowKey(): JSX.Element {
|
||||
let continueButton: JSX.Element;
|
||||
if (this.state.phase === Phase.ShowKey) {
|
||||
continueButton = (
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|continue")}
|
||||
disabled={!this.state.downloaded && !this.state.copied && !this.state.setPassphrase}
|
||||
onPrimaryButtonClick={this.onShowKeyContinueClick}
|
||||
hasCancel={false}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
continueButton = (
|
||||
<div className="mx_CreateSecretStorageDialog_continueSpinner">
|
||||
<InlineSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("settings|key_backup|setup_secure_backup|security_key_safety_reminder")}</p>
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer mx_CreateSecretStorageDialog_recoveryKeyPrimarycontainer">
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKey">
|
||||
<code ref={this.recoveryKeyNode}>{this.recoveryKey?.encodedPrivateKey}</code>
|
||||
</div>
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
className="mx_Dialog_primary"
|
||||
onClick={this.onDownloadClick}
|
||||
disabled={this.state.phase === Phase.Storing}
|
||||
>
|
||||
{_t("action|download")}
|
||||
</AccessibleButton>
|
||||
<span>
|
||||
{_t("settings|key_backup|setup_secure_backup|download_or_copy", {
|
||||
downloadButton: "",
|
||||
copyButton: "",
|
||||
})}
|
||||
</span>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
|
||||
onClick={this.onCopyClick}
|
||||
disabled={this.state.phase === Phase.Storing}
|
||||
>
|
||||
{this.state.copied ? _t("common|copied") : _t("action|copy")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{continueButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderBusyPhase(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderStoredPhase(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<p className="mx_Dialog_content">
|
||||
{_t("settings|key_backup|setup_secure_backup|backup_setup_success_description")}
|
||||
</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|done")}
|
||||
onPrimaryButtonClick={() => this.props.onFinished(true)}
|
||||
hasCancel={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPhaseLoadError(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("settings|key_backup|setup_secure_backup|secret_storage_query_failure")}</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|retry")}
|
||||
onPrimaryButtonClick={this.onLoadRetryClick}
|
||||
hasCancel={this.state.canSkip}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPhaseSkipConfirm(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("settings|key_backup|setup_secure_backup|cancel_warning")}</p>
|
||||
<p>{_t("settings|key_backup|setup_secure_backup|settings_reminder")}</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|go_back")}
|
||||
onPrimaryButtonClick={this.onGoBackClick}
|
||||
hasCancel={false}
|
||||
>
|
||||
<button type="button" className="danger" onClick={this.onCancel}>
|
||||
{_t("action|cancel")}
|
||||
</button>
|
||||
</DialogButtons>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private titleForPhase(phase: Phase): string {
|
||||
switch (phase) {
|
||||
case Phase.ChooseKeyPassphrase:
|
||||
return _t("encryption|set_up_toast_title");
|
||||
case Phase.Migrate:
|
||||
return _t("settings|key_backup|setup_secure_backup|title_upgrade_encryption");
|
||||
case Phase.Passphrase:
|
||||
return _t("settings|key_backup|setup_secure_backup|title_set_phrase");
|
||||
case Phase.PassphraseConfirm:
|
||||
return _t("settings|key_backup|setup_secure_backup|title_confirm_phrase");
|
||||
case Phase.ConfirmSkip:
|
||||
return _t("common|are_you_sure");
|
||||
case Phase.ShowKey:
|
||||
return _t("settings|key_backup|setup_secure_backup|title_save_key");
|
||||
case Phase.Storing:
|
||||
return _t("encryption|bootstrap_title");
|
||||
case Phase.Stored:
|
||||
return _t("settings|key_backup|setup_secure_backup|backup_setup_success_title");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private get topComponent(): React.ReactNode | null {
|
||||
if (this.state.phase === Phase.Stored) {
|
||||
return <CheckmarkIcon className="mx_Icon mx_Icon_circle-40 mx_Icon_accent mx_Icon_bg-accent-light" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private get classNames(): string {
|
||||
return classNames("mx_CreateSecretStorageDialog", {
|
||||
mx_SuccessDialog: this.state.phase === Phase.Stored,
|
||||
});
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
content = (
|
||||
<div>
|
||||
<p>{_t("settings|key_backup|setup_secure_backup|unable_to_setup")}</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|retry")}
|
||||
onPrimaryButtonClick={this.bootstrapSecretStorage}
|
||||
hasCancel={this.state.canSkip}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
switch (this.state.phase) {
|
||||
case Phase.Loading:
|
||||
content = this.renderBusyPhase();
|
||||
break;
|
||||
case Phase.LoadError:
|
||||
content = this.renderPhaseLoadError();
|
||||
break;
|
||||
case Phase.ChooseKeyPassphrase:
|
||||
content = this.renderPhaseChooseKeyPassphrase();
|
||||
break;
|
||||
case Phase.Migrate:
|
||||
content = this.renderPhaseMigrate();
|
||||
break;
|
||||
case Phase.Passphrase:
|
||||
content = this.renderPhasePassPhrase();
|
||||
break;
|
||||
case Phase.PassphraseConfirm:
|
||||
content = this.renderPhasePassPhraseConfirm();
|
||||
break;
|
||||
case Phase.ShowKey:
|
||||
content = this.renderPhaseShowKey();
|
||||
break;
|
||||
case Phase.Storing:
|
||||
content = this.renderBusyPhase();
|
||||
break;
|
||||
case Phase.Stored:
|
||||
content = this.renderStoredPhase();
|
||||
break;
|
||||
case Phase.ConfirmSkip:
|
||||
content = this.renderPhaseSkipConfirm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let titleClass: string | string[] | undefined;
|
||||
switch (this.state.phase) {
|
||||
case Phase.Passphrase:
|
||||
case Phase.PassphraseConfirm:
|
||||
titleClass = [
|
||||
"mx_CreateSecretStorageDialog_titleWithIcon",
|
||||
"mx_CreateSecretStorageDialog_securePhraseTitle",
|
||||
];
|
||||
break;
|
||||
case Phase.ShowKey:
|
||||
titleClass = [
|
||||
"mx_CreateSecretStorageDialog_titleWithIcon",
|
||||
"mx_CreateSecretStorageDialog_secureBackupTitle",
|
||||
];
|
||||
break;
|
||||
case Phase.ChooseKeyPassphrase:
|
||||
titleClass = "mx_CreateSecretStorageDialog_centeredTitle";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className={this.classNames}
|
||||
onFinished={this.props.onFinished}
|
||||
top={this.topComponent}
|
||||
title={this.titleForPhase(this.state.phase)}
|
||||
titleClass={titleClass}
|
||||
hasCancel={this.props.hasCancel && [Phase.Passphrase].includes(this.state.phase)}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div>{content}</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import FileSaver from "file-saver";
|
||||
import React, { ChangeEvent } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t, _td } from "../../../../languageHandler";
|
||||
import * as MegolmExportEncryption from "../../../../utils/MegolmExportEncryption";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import { KeysStartingWith } from "../../../../@types/common";
|
||||
import PassphraseField from "../../../../components/views/auth/PassphraseField";
|
||||
import PassphraseConfirmField from "../../../../components/views/auth/PassphraseConfirmField";
|
||||
import Field from "../../../../components/views/elements/Field";
|
||||
|
||||
enum Phase {
|
||||
Edit = "edit",
|
||||
Exporting = "exporting",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
onFinished(doExport?: boolean): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
errStr: string | null;
|
||||
passphrase1: string;
|
||||
passphrase2: string;
|
||||
}
|
||||
|
||||
type AnyPassphrase = KeysStartingWith<IState, "passphrase">;
|
||||
|
||||
export default class ExportE2eKeysDialog extends React.Component<IProps, IState> {
|
||||
private fieldPassword: Field | null = null;
|
||||
private fieldPasswordConfirm: Field | null = null;
|
||||
|
||||
private unmounted = false;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
phase: Phase.Edit,
|
||||
errStr: null,
|
||||
passphrase1: "",
|
||||
passphrase2: "",
|
||||
};
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private async verifyFieldsBeforeSubmit(): Promise<boolean> {
|
||||
const fieldsInDisplayOrder = [this.fieldPassword, this.fieldPasswordConfirm];
|
||||
|
||||
const invalidFields: Field[] = [];
|
||||
|
||||
for (const field of fieldsInDisplayOrder) {
|
||||
if (!field) continue;
|
||||
|
||||
const valid = await field.validate({ allowEmpty: false });
|
||||
if (!valid) {
|
||||
invalidFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidFields.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Focus on the first invalid field, then re-validate,
|
||||
// which will result in the error tooltip being displayed for that field.
|
||||
invalidFields[0].focus();
|
||||
invalidFields[0].validate({ allowEmpty: false, focused: true });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private onPassphraseFormSubmit = async (ev: React.FormEvent): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!(await this.verifyFieldsBeforeSubmit())) return;
|
||||
if (this.unmounted) return;
|
||||
|
||||
const passphrase = this.state.passphrase1;
|
||||
this.startExport(passphrase);
|
||||
};
|
||||
|
||||
private startExport(passphrase: string): void {
|
||||
// extra Promise.resolve() to turn synchronous exceptions into
|
||||
// asynchronous ones.
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
return this.props.matrixClient.getCrypto()!.exportRoomKeysAsJson();
|
||||
})
|
||||
.then((k) => {
|
||||
return MegolmExportEncryption.encryptMegolmKeyFile(k, passphrase);
|
||||
})
|
||||
.then((f) => {
|
||||
const blob = new Blob([f], {
|
||||
type: "text/plain;charset=us-ascii",
|
||||
});
|
||||
FileSaver.saveAs(blob, "element-keys.txt");
|
||||
this.props.onFinished(true);
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Error exporting e2e keys:", e);
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
const msg = e.friendlyText || _t("error|unknown");
|
||||
this.setState({
|
||||
errStr: msg,
|
||||
phase: Phase.Edit,
|
||||
});
|
||||
});
|
||||
|
||||
this.setState({
|
||||
errStr: null,
|
||||
phase: Phase.Exporting,
|
||||
});
|
||||
}
|
||||
|
||||
private onCancelClick = (ev: React.MouseEvent): boolean => {
|
||||
ev.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
return false;
|
||||
};
|
||||
|
||||
private onPassphraseChange = (ev: React.ChangeEvent<HTMLInputElement>, phrase: AnyPassphrase): void => {
|
||||
this.setState({
|
||||
[phrase]: ev.target.value,
|
||||
} as Pick<IState, AnyPassphrase>);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const disableForm = this.state.phase === Phase.Exporting;
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_exportE2eKeysDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("settings|key_export_import|export_title")}
|
||||
>
|
||||
<form onSubmit={this.onPassphraseFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{_t("settings|key_export_import|export_description_1")}</p>
|
||||
<p>{_t("settings|key_export_import|export_description_2")}</p>
|
||||
<div className="error">{this.state.errStr}</div>
|
||||
<div className="mx_E2eKeysDialog_inputTable">
|
||||
<div className="mx_E2eKeysDialog_inputRow">
|
||||
<PassphraseField
|
||||
minScore={3}
|
||||
label={_td("settings|key_export_import|enter_passphrase")}
|
||||
labelEnterPassword={_td("settings|key_export_import|enter_passphrase")}
|
||||
labelStrongPassword={_td("settings|key_export_import|phrase_strong_enough")}
|
||||
labelAllowedButUnsafe={_td("settings|key_export_import|phrase_strong_enough")}
|
||||
value={this.state.passphrase1}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
this.onPassphraseChange(e, "passphrase1")
|
||||
}
|
||||
autoFocus={true}
|
||||
size={64}
|
||||
type="password"
|
||||
disabled={disableForm}
|
||||
autoComplete="new-password"
|
||||
fieldRef={(field) => (this.fieldPassword = field)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_E2eKeysDialog_inputRow">
|
||||
<PassphraseConfirmField
|
||||
password={this.state.passphrase1}
|
||||
label={_td("settings|key_export_import|confirm_passphrase")}
|
||||
labelRequired={_td("settings|key_export_import|phrase_cannot_be_empty")}
|
||||
labelInvalid={_td("settings|key_export_import|phrase_must_match")}
|
||||
value={this.state.passphrase2}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
this.onPassphraseChange(e, "passphrase2")
|
||||
}
|
||||
size={64}
|
||||
type="password"
|
||||
disabled={disableForm}
|
||||
autoComplete="new-password"
|
||||
fieldRef={(field) => (this.fieldPasswordConfirm = field)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input
|
||||
className="mx_Dialog_primary"
|
||||
type="submit"
|
||||
value={_t("action|export")}
|
||||
disabled={disableForm}
|
||||
/>
|
||||
<button onClick={this.onCancelClick} disabled={disableForm}>
|
||||
{_t("action|cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as MegolmExportEncryption from "../../../../utils/MegolmExportEncryption";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import Field from "../../../../components/views/elements/Field";
|
||||
|
||||
function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (e.target?.result) {
|
||||
resolve(e.target.result as ArrayBuffer);
|
||||
} else {
|
||||
reject(new Error("Failed to read file due to unknown error"));
|
||||
}
|
||||
};
|
||||
reader.onerror = reject;
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
enum Phase {
|
||||
Edit = "edit",
|
||||
Importing = "importing",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
onFinished(imported?: boolean): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
enableSubmit: boolean;
|
||||
phase: Phase;
|
||||
errStr: string | null;
|
||||
passphrase: string;
|
||||
}
|
||||
|
||||
export default class ImportE2eKeysDialog extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private file = createRef<HTMLInputElement>();
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
enableSubmit: false,
|
||||
phase: Phase.Edit,
|
||||
errStr: null,
|
||||
passphrase: "",
|
||||
};
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private onFormChange = (): void => {
|
||||
const files = this.file.current?.files;
|
||||
this.setState({
|
||||
enableSubmit: this.state.passphrase !== "" && !!files?.length,
|
||||
});
|
||||
};
|
||||
|
||||
private onPassphraseChange = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({ passphrase: ev.target.value }, this.onFormChange); // update general form state too
|
||||
};
|
||||
|
||||
private onFormSubmit = (ev: React.FormEvent): boolean => {
|
||||
ev.preventDefault();
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
const file = this.file.current?.files?.[0];
|
||||
if (file) {
|
||||
this.startImport(file, this.state.passphrase);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
private startImport(file: File, passphrase: string): Promise<void> {
|
||||
this.setState({
|
||||
errStr: null,
|
||||
phase: Phase.Importing,
|
||||
});
|
||||
|
||||
return readFileAsArrayBuffer(file)
|
||||
.then((arrayBuffer) => {
|
||||
return MegolmExportEncryption.decryptMegolmKeyFile(arrayBuffer, passphrase);
|
||||
})
|
||||
.then((keys) => {
|
||||
return this.props.matrixClient.getCrypto()!.importRoomKeysAsJson(keys);
|
||||
})
|
||||
.then(() => {
|
||||
// TODO: it would probably be nice to give some feedback about what we've imported here.
|
||||
this.props.onFinished(true);
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Error importing e2e keys:", e);
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
const msg = e.friendlyText || _t("error|unknown");
|
||||
this.setState({
|
||||
errStr: msg,
|
||||
phase: Phase.Edit,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private onCancelClick = (ev: React.MouseEvent): boolean => {
|
||||
ev.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
return false;
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const disableForm = this.state.phase !== Phase.Edit;
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_importE2eKeysDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("settings|key_export_import|import_title")}
|
||||
>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{_t("settings|key_export_import|import_description_1")}</p>
|
||||
<p>{_t("settings|key_export_import|import_description_2")}</p>
|
||||
<div className="error">{this.state.errStr}</div>
|
||||
<div className="mx_E2eKeysDialog_inputTable">
|
||||
<div className="mx_E2eKeysDialog_inputRow">
|
||||
<div className="mx_E2eKeysDialog_inputLabel">
|
||||
<label htmlFor="importFile">
|
||||
{_t("settings|key_export_import|file_to_import")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="mx_E2eKeysDialog_inputCell">
|
||||
<input
|
||||
ref={this.file}
|
||||
id="importFile"
|
||||
type="file"
|
||||
autoFocus={true}
|
||||
onChange={this.onFormChange}
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_E2eKeysDialog_inputRow">
|
||||
<Field
|
||||
label={_t("settings|key_export_import|enter_passphrase")}
|
||||
value={this.state.passphrase}
|
||||
onChange={this.onPassphraseChange}
|
||||
size={64}
|
||||
type="password"
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input
|
||||
className="mx_Dialog_primary"
|
||||
type="submit"
|
||||
value={_t("action|import")}
|
||||
disabled={!this.state.enableSubmit || disableForm}
|
||||
/>
|
||||
<button onClick={this.onCancelClick} disabled={disableForm}>
|
||||
{_t("action|cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import dis from "../../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Modal from "../../../../Modal";
|
||||
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
|
||||
import { Action } from "../../../../dispatcher/actions";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
|
||||
interface IProps {
|
||||
newVersionInfo: KeyBackupInfo;
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
export default class NewRecoveryMethodDialog extends React.PureComponent<IProps> {
|
||||
private onOkClick = (): void => {
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
private onGoToSettingsClick = (): void => {
|
||||
this.props.onFinished();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
};
|
||||
|
||||
private onSetupClick = async (): Promise<void> => {
|
||||
Modal.createDialog(
|
||||
RestoreKeyBackupDialog,
|
||||
{
|
||||
onFinished: this.props.onFinished,
|
||||
},
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
/* static = */ true,
|
||||
);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const title = (
|
||||
<span className="mx_KeyBackupFailedDialog_title">
|
||||
{_t("encryption|new_recovery_method_detected|title")}
|
||||
</span>
|
||||
);
|
||||
|
||||
const newMethodDetected = <p>{_t("encryption|new_recovery_method_detected|description_1")}</p>;
|
||||
|
||||
const hackWarning = (
|
||||
<strong className="warning">{_t("encryption|new_recovery_method_detected|warning")}</strong>
|
||||
);
|
||||
|
||||
let content: JSX.Element | undefined;
|
||||
if (MatrixClientPeg.safeGet().getKeyBackupEnabled()) {
|
||||
content = (
|
||||
<div>
|
||||
{newMethodDetected}
|
||||
<p>{_t("encryption|new_recovery_method_detected|description_2")}</p>
|
||||
{hackWarning}
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|ok")}
|
||||
onPrimaryButtonClick={this.onOkClick}
|
||||
cancelButton={_t("common|go_to_settings")}
|
||||
onCancel={this.onGoToSettingsClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
{newMethodDetected}
|
||||
{hackWarning}
|
||||
<DialogButtons
|
||||
primaryButton={_t("common|setup_secure_messages")}
|
||||
onPrimaryButtonClick={this.onSetupClick}
|
||||
cancelButton={_t("common|go_to_settings")}
|
||||
onCancel={this.onGoToSettingsClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_KeyBackupFailedDialog" onFinished={this.props.onFinished} title={title}>
|
||||
{content}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import dis from "../../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Modal, { ComponentType } from "../../../../Modal";
|
||||
import { Action } from "../../../../dispatcher/actions";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
|
||||
interface IProps {
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
export default class RecoveryMethodRemovedDialog extends React.PureComponent<IProps> {
|
||||
private onGoToSettingsClick = (): void => {
|
||||
this.props.onFinished();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
};
|
||||
|
||||
private onSetupClick = (): void => {
|
||||
this.props.onFinished();
|
||||
Modal.createDialogAsync(
|
||||
import("./CreateKeyBackupDialog") as unknown as Promise<ComponentType>,
|
||||
undefined,
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
/* static = */ true,
|
||||
);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const title = (
|
||||
<span className="mx_KeyBackupFailedDialog_title">{_t("encryption|recovery_method_removed|title")}</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_KeyBackupFailedDialog" onFinished={this.props.onFinished} title={title}>
|
||||
<div>
|
||||
<p>{_t("encryption|recovery_method_removed|description_1")}</p>
|
||||
<p>{_t("encryption|recovery_method_removed|description_2")}</p>
|
||||
<strong className="warning">{_t("encryption|recovery_method_removed|warning")}</strong>
|
||||
<DialogButtons
|
||||
primaryButton={_t("common|setup_secure_messages")}
|
||||
onPrimaryButtonClick={this.onSetupClick}
|
||||
cancelButton={_t("common|go_to_settings")}
|
||||
onCancel={this.onGoToSettingsClick}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue