From 262475f96e1b6527325ec19f31220d04c76c7a97 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Mar 2021 15:37:06 -0600 Subject: [PATCH 1/5] Add a button to reset personal encryption state during login --- .../security/_AccessSecretStorageDialog.scss | 31 ++++++- src/Modal.tsx | 9 +- .../security/AccessSecretStorageDialog.tsx | 86 ++++++++++++++++++- src/i18n/strings/en_EN.json | 4 + 4 files changed, 123 insertions(+), 7 deletions(-) diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss index 63d0ca555d..30b79c1a9a 100644 --- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss @@ -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. @@ -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; @@ -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'); } diff --git a/src/Modal.tsx b/src/Modal.tsx index ab582b9b22..ce11c571b6 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -36,6 +36,7 @@ export interface IModal { onBeforeClose?(reason?: string): Promise; onFinished(...args: T): void; close(...args: T): void; + hidden?: boolean; } export interface IHandle { @@ -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; } @@ -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, }); diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 3c09470b39..283e4208a6 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -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 @@ -47,6 +49,7 @@ interface IState { forceRecoveryKey: boolean; passPhrase: string; keyMatches: boolean | null; + resetting: boolean; } /* @@ -66,10 +69,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent { + if (this.state.resetting) { + this.setState({resetting: false}); + } this.props.onFinished(false); }; @@ -201,6 +208,50 @@ export default class AccessSecretStorageDialog extends React.PureComponent) => { + 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 { if (this.state.recoveryKeyFileError) { return _t("Wrong file type"); @@ -216,8 +267,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent + {_t("Forgotten or lost all recovery methods? Reset all", null, { + a: (sub) => {sub}, + })} + + ); + 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 =
+

{_t("Only do this if you have no other device to complete verification with.")}

+

{_t("If you reset everything, you will restart with no trusted devices, no trusted users, and " + + "might not be able to see past messages.")}

+ +
; + } else if (hasPassphrase && !this.state.forceRecoveryKey) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); title = _t("Security Phrase"); titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle']; @@ -278,13 +355,13 @@ export default class AccessSecretStorageDialog extends React.PureComponent ; } 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, @@ -339,6 +416,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0668f54822..9592d1360d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2369,6 +2369,10 @@ "Looks good!": "Looks good!", "Wrong Security Key": "Wrong Security Key", "Invalid Security Key": "Invalid Security Key", + "Forgotten or lost all recovery methods? Reset all": "Forgotten or lost all recovery methods? Reset all", + "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", "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 to continue.": "Enter your Security Phrase or to continue.", From f86e090b8a9377a70f3f256e65854eab6ec20fac Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 1 Apr 2021 08:23:32 -0600 Subject: [PATCH 2/5] Update src/components/views/dialogs/security/AccessSecretStorageDialog.tsx Co-authored-by: J. Ryan Stinnett --- .../views/dialogs/security/AccessSecretStorageDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 283e4208a6..0f2e9aa637 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -296,7 +296,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent

{_t("Only do this if you have no other device to complete verification with.")}

-

{_t("If you reset everything, you will restart with no trusted devices, no trusted users, and " +

{_t("If you reset everything, you will restart with no trusted sessions, no trusted users, and " + "might not be able to see past messages.")}

Date: Fri, 2 Apr 2021 19:36:28 -0600 Subject: [PATCH 3/5] Update i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9592d1360d..bc79a1c18b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2372,7 +2372,7 @@ "Forgotten or lost all recovery methods? Reset all": "Forgotten or lost all recovery methods? Reset all", "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.", + "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 to continue.": "Enter your Security Phrase or to continue.", From f2e2f1699bdc10dcb520b26b348851ae93024199 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 8 Apr 2021 18:09:41 -0600 Subject: [PATCH 4/5] Add some catches --- .../security/AccessSecretStorageDialog.tsx | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 0f2e9aa637..7bc1e77459 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -222,34 +222,44 @@ export default class AccessSecretStorageDialog extends React.PureComponent { - // 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, + try { + // Force reset secret storage (which resets the key backup) + await accessSecretStorage(async () => { + try { + // 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"); + } }, - ); - const [confirmed] = await finished; - if (!confirmed) { - throw new Error("Cross-signing key upload auth canceled"); - } - }, - setupNewCrossSigning: true, - }); + 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); + // 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); + } catch (e) { + console.error(e); + this.props.onFinished(false); + } + }, true); + } catch (e) { + console.error(e); + this.props.onFinished(false); + } }; private getKeyValidationText(): string { From 72a9bda3b7d7a4a2c009a61a62518a8924c3a509 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Apr 2021 14:57:21 -0600 Subject: [PATCH 5/5] One less try/catch --- .../security/AccessSecretStorageDialog.tsx | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 7bc1e77459..ffe513581b 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -225,36 +225,31 @@ export default class AccessSecretStorageDialog extends React.PureComponent { - try { - // 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 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); - } catch (e) { - console.error(e); - this.props.onFinished(false); - } + // 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);