Add a button to reset personal encryption state during login
This commit is contained in:
parent
b68fabb44b
commit
262475f96e
4 changed files with 123 additions and 7 deletions
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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.",
|
||||||
|
|
Loading…
Reference in a new issue