Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Add a button to reset personal encryption state during login #5819

Merged
merged 7 commits into from
Apr 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions res/css/views/dialogs/security/_AccessSecretStorageDialog.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2018, 2019, 2021 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -15,6 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

.mx_AccessSecretStorageDialog_reset {
position: relative;
padding-left: 24px; // 16px icon + 8px padding
margin-top: 7px; // vertical alignment to buttons

&::before {
content: "";
display: inline-block;
position: absolute;
height: 16px;
width: 16px;
left: 0;
top: 2px; // alignment
background-image: url("$(res)/img/element-icons/warning-badge.svg");
}

.mx_AccessSecretStorageDialog_reset_link {
color: $warning-color;
}
}

.mx_AccessSecretStorageDialog_titleWithIcon::before {
content: '';
display: inline-block;
Expand All @@ -26,6 +46,13 @@ limitations under the License.
background-color: $primary-fg-color;
}

.mx_AccessSecretStorageDialog_resetBadge::before {
// The image isn't capable of masking, so we use a background instead.
background-image: url("$(res)/img/element-icons/warning-badge.svg");
background-size: 24px;
background-color: transparent;
}

.mx_AccessSecretStorageDialog_secureBackupTitle::before {
mask-image: url('$(res)/img/feather-customised/secure-backup.svg');
}
Expand Down
9 changes: 8 additions & 1 deletion src/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface IModal<T extends any[]> {
onBeforeClose?(reason?: string): Promise<boolean>;
onFinished(...args: T): void;
close(...args: T): void;
hidden?: boolean;
}

export interface IHandle<T extends any[]> {
Expand Down Expand Up @@ -93,6 +94,12 @@ export class ModalManager {
return container;
}

public toggleCurrentDialogVisibility() {
const modal = this.getCurrentModal();
if (!modal) return;
modal.hidden = !modal.hidden;
}

public hasDialogs() {
return this.priorityModal || this.staticModal || this.modals.length > 0;
}
Expand Down Expand Up @@ -364,7 +371,7 @@ export class ModalManager {
}

const modal = this.getCurrentModal();
if (modal !== this.staticModal) {
if (modal !== this.staticModal && !modal.hidden) {
const classes = classNames("mx_Dialog_wrapper", modal.className, {
mx_Dialog_wrapperWithStaticUnder: this.staticModal,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import Field from '../../elements/Field';
import AccessibleButton from '../../elements/AccessibleButton';
import {_t} from '../../../../languageHandler';
import {IDialogProps} from "../IDialogProps";
import {accessSecretStorage} from "../../../../SecurityManager";
import Modal from "../../../../Modal";

// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
// so this should be plenty and allow for people putting extra whitespace in the file because
Expand All @@ -47,6 +49,7 @@ interface IState {
forceRecoveryKey: boolean;
passPhrase: string;
keyMatches: boolean | null;
resetting: boolean;
}

/*
Expand All @@ -66,10 +69,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
forceRecoveryKey: false,
passPhrase: '',
keyMatches: null,
resetting: false,
};
}

private onCancel = () => {
if (this.state.resetting) {
this.setState({resetting: false});
}
this.props.onFinished(false);
};

Expand Down Expand Up @@ -201,6 +208,55 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
});
};

private onResetAllClick = (ev: React.MouseEvent<HTMLAnchorElement>) => {
ev.preventDefault();
this.setState({resetting: true});
};

private onConfirmResetAllClick = async () => {
// Hide ourselves so the user can interact with the reset dialogs.
// We don't conclude the promise chain (onFinished) yet to avoid confusing
// any upstream code flows.
//
// Note: this will unmount us, so don't call `setState` or anything in the
// rest of this function.
Modal.toggleCurrentDialogVisibility();

try {
// Force reset secret storage (which resets the key backup)
await accessSecretStorage(async () => {
// Now reset cross-signing so everything Just Works™ again.
const cli = MatrixClientPeg.get();
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => {
// XXX: Making this an import breaks the app.
const InteractiveAuthDialog = sdk.getComponent("views.dialogs.InteractiveAuthDialog");
const {finished} = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
title: _t("Setting up keys"),
matrixClient: cli,
makeRequest,
},
);
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
},
setupNewCrossSigning: true,
});

// Now we can indicate that the user is done pressing buttons, finally.
// Upstream flows will detect the new secret storage, key backup, etc and use it.
this.props.onFinished(true);
}, true);
} catch (e) {
console.error(e);
this.props.onFinished(false);
}
};

private getKeyValidationText(): string {
if (this.state.recoveryKeyFileError) {
return _t("Wrong file type");
Expand All @@ -216,8 +272,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
}

render() {
// Caution: Making this an import will break tests.
// Caution: Making these an import will break tests.
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");

const hasPassphrase = (
this.props.keyInfo &&
Expand All @@ -226,11 +283,36 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
this.props.keyInfo.passphrase.iterations
);

const resetButton = (
<div className="mx_AccessSecretStorageDialog_reset">
{_t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
a: (sub) => <a
href="" onClick={this.onResetAllClick}
className="mx_AccessSecretStorageDialog_reset_link">{sub}</a>,
})}
</div>
);

let content;
let title;
let titleClass;
if (hasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
if (this.state.resetting) {
title = _t("Reset everything");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_resetBadge'];
content = <div>
<p>{_t("Only do this if you have no other device to complete verification with.")}</p>
<p>{_t("If you reset everything, you will restart with no trusted sessions, no trusted users, and "
+ "might not be able to see past messages.")}</p>
<DialogButtons
primaryButton={_t('Reset')}
onPrimaryButtonClick={this.onConfirmResetAllClick}
hasCancel={true}
onCancel={this.onCancel}
focus={false}
primaryButtonClass="danger"
/>
</div>;
} else if (hasPassphrase && !this.state.forceRecoveryKey) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Security Phrase");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle'];
Expand Down Expand Up @@ -278,13 +360,13 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
onCancel={this.onCancel}
focus={false}
primaryDisabled={this.state.passPhrase.length === 0}
additive={resetButton}
/>
</form>
</div>;
} else {
title = _t("Security Key");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle'];
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');

const feedbackClasses = classNames({
'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true,
Expand Down Expand Up @@ -339,6 +421,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
onCancel={this.onCancel}
focus={false}
primaryDisabled={!this.state.recoveryKeyValid}
additive={resetButton}
/>
</form>
</div>;
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2377,6 +2377,10 @@
"Looks good!": "Looks good!",
"Wrong Security Key": "Wrong Security Key",
"Invalid Security Key": "Invalid Security Key",
"Forgotten or lost all recovery methods? <a>Reset all</a>": "Forgotten or lost all recovery methods? <a>Reset all</a>",
"Reset everything": "Reset everything",
"Only do this if you have no other device to complete verification with.": "Only do this if you have no other device to complete verification with.",
"If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.",
"Security Phrase": "Security Phrase",
"Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "Unable to access secret storage. Please verify that you entered the correct Security Phrase.",
"Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Enter your Security Phrase or <button>Use your Security Key</button> to continue.",
Expand Down