Add a button to reset personal encryption state during login

This commit is contained in:
Travis Ralston 2021-03-30 15:37:06 -06:00
parent b68fabb44b
commit 262475f96e
4 changed files with 123 additions and 7 deletions

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018, 2019, 2021 The Matrix.org Foundation C.I.C.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,6 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { .mx_AccessSecretStorageDialog_titleWithIcon::before {
content: ''; content: '';
display: inline-block; display: inline-block;
@ -26,6 +46,13 @@ limitations under the License.
background-color: $primary-fg-color; 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 { .mx_AccessSecretStorageDialog_secureBackupTitle::before {
mask-image: url('$(res)/img/feather-customised/secure-backup.svg'); mask-image: url('$(res)/img/feather-customised/secure-backup.svg');
} }

View file

@ -36,6 +36,7 @@ export interface IModal<T extends any[]> {
onBeforeClose?(reason?: string): Promise<boolean>; onBeforeClose?(reason?: string): Promise<boolean>;
onFinished(...args: T): void; onFinished(...args: T): void;
close(...args: T): void; close(...args: T): void;
hidden?: boolean;
} }
export interface IHandle<T extends any[]> { export interface IHandle<T extends any[]> {
@ -93,6 +94,12 @@ export class ModalManager {
return container; return container;
} }
public toggleCurrentDialogVisibility() {
const modal = this.getCurrentModal();
if (!modal) return;
modal.hidden = !modal.hidden;
}
public hasDialogs() { public hasDialogs() {
return this.priorityModal || this.staticModal || this.modals.length > 0; return this.priorityModal || this.staticModal || this.modals.length > 0;
} }
@ -364,7 +371,7 @@ export class ModalManager {
} }
const modal = this.getCurrentModal(); const modal = this.getCurrentModal();
if (modal !== this.staticModal) { if (modal !== this.staticModal && !modal.hidden) {
const classes = classNames("mx_Dialog_wrapper", modal.className, { const classes = classNames("mx_Dialog_wrapper", modal.className, {
mx_Dialog_wrapperWithStaticUnder: this.staticModal, mx_Dialog_wrapperWithStaticUnder: this.staticModal,
}); });

View file

@ -25,6 +25,8 @@ import Field from '../../elements/Field';
import AccessibleButton from '../../elements/AccessibleButton'; import AccessibleButton from '../../elements/AccessibleButton';
import {_t} from '../../../../languageHandler'; import {_t} from '../../../../languageHandler';
import {IDialogProps} from "../IDialogProps"; 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, // 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 // so this should be plenty and allow for people putting extra whitespace in the file because
@ -47,6 +49,7 @@ interface IState {
forceRecoveryKey: boolean; forceRecoveryKey: boolean;
passPhrase: string; passPhrase: string;
keyMatches: boolean | null; keyMatches: boolean | null;
resetting: boolean;
} }
/* /*
@ -66,10 +69,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
forceRecoveryKey: false, forceRecoveryKey: false,
passPhrase: '', passPhrase: '',
keyMatches: null, keyMatches: null,
resetting: false,
}; };
} }
private onCancel = () => { private onCancel = () => {
if (this.state.resetting) {
this.setState({resetting: false});
}
this.props.onFinished(false); this.props.onFinished(false);
}; };
@ -201,6 +208,50 @@ 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();
// 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);
};
private getKeyValidationText(): string { private getKeyValidationText(): string {
if (this.state.recoveryKeyFileError) { if (this.state.recoveryKeyFileError) {
return _t("Wrong file type"); return _t("Wrong file type");
@ -216,8 +267,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
} }
render() { 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 BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const hasPassphrase = ( const hasPassphrase = (
this.props.keyInfo && this.props.keyInfo &&
@ -226,11 +278,36 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
this.props.keyInfo.passphrase.iterations 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 content;
let title; let title;
let titleClass; let titleClass;
if (hasPassphrase && !this.state.forceRecoveryKey) { if (this.state.resetting) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); 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 devices, 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'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Security Phrase"); title = _t("Security Phrase");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle']; titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle'];
@ -278,13 +355,13 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
onCancel={this.onCancel} onCancel={this.onCancel}
focus={false} focus={false}
primaryDisabled={this.state.passPhrase.length === 0} primaryDisabled={this.state.passPhrase.length === 0}
additive={resetButton}
/> />
</form> </form>
</div>; </div>;
} else { } else {
title = _t("Security Key"); title = _t("Security Key");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle']; titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle'];
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const feedbackClasses = classNames({ const feedbackClasses = classNames({
'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true, 'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true,
@ -339,6 +416,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
onCancel={this.onCancel} onCancel={this.onCancel}
focus={false} focus={false}
primaryDisabled={!this.state.recoveryKeyValid} primaryDisabled={!this.state.recoveryKeyValid}
additive={resetButton}
/> />
</form> </form>
</div>; </div>;

View file

@ -2369,6 +2369,10 @@
"Looks good!": "Looks good!", "Looks good!": "Looks good!",
"Wrong Security Key": "Wrong Security Key", "Wrong Security Key": "Wrong Security Key",
"Invalid Security Key": "Invalid 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 devices, no trusted users, and might not be able to see past messages.": "If you reset everything, you will restart with no trusted devices, no trusted users, and might not be able to see past messages.",
"Security Phrase": "Security Phrase", "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.", "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.", "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.",