diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 86ee995a13..84340d8219 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -27,10 +27,12 @@ import {ModalManager} from "../Modal"; import SettingsStore from "../settings/SettingsStore"; import {ActiveRoomObserver} from "../ActiveRoomObserver"; import {Notifier} from "../Notifier"; +import type {Renderer} from "react-dom"; declare global { interface Window { Modernizr: ModernizrStatic; + matrixChat: ReturnType; mxMatrixClientPeg: IMatrixClientPeg; Olm: { init: () => Promise; diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 676c41d7d7..da09a436e9 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -21,6 +21,7 @@ import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from './languageHandler'; import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; +import { isSecureBackupRequired } from './utils/WellKnownUtils'; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -34,6 +35,17 @@ function isCachingAllowed() { return secretStorageBeingAccessed; } +/** + * This can be used by other components to check if secret storage access is in + * progress, so that we can e.g. avoid intermittently showing toasts during + * secret storage setup. + * + * @returns {bool} + */ +export function isSecretStorageBeingAccessed() { + return secretStorageBeingAccessed; +} + export class AccessCancelledError extends Error { constructor() { super("Secret storage access canceled"); @@ -208,7 +220,18 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f { force: forceReset, }, - null, /* priority = */ false, /* static = */ true, + null, + /* priority = */ false, + /* static = */ true, + /* options = */ { + onBeforeClose(reason) { + // If Secure Backup is required, you cannot leave the modal. + if (reason === "backgroundClick") { + return !isSecureBackupRequired(); + } + return true; + }, + }, ); const [confirmed] = await finished; if (!confirmed) { diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index a37521118f..6b667ae54d 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -15,6 +15,7 @@ limitations under the License. */ import {MatrixClientPeg} from './MatrixClientPeg'; +import dis from "./dispatcher/dispatcher"; import { hideToast as hideBulkUnverifiedSessionsToast, showToast as showBulkUnverifiedSessionsToast, @@ -28,11 +29,16 @@ import { hideToast as hideUnverifiedSessionsToast, showToast as showUnverifiedSessionsToast, } from "./toasts/UnverifiedSessionToast"; -import {privateShouldBeEncrypted} from "./createRoom"; +import { privateShouldBeEncrypted } from "./createRoom"; +import { isSecretStorageBeingAccessed, accessSecretStorage } from "./CrossSigningManager"; +import { isSecureBackupRequired } from './utils/WellKnownUtils'; +import { isLoggedIn } from './components/structures/MatrixChat'; + const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; export default class DeviceListener { + private dispatcherRef: string; // device IDs for which the user has dismissed the verify toast ('Later') private dismissed = new Set(); // has the user dismissed any of the various nag toasts to setup encryption on this device? @@ -60,6 +66,7 @@ export default class DeviceListener { MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged); MatrixClientPeg.get().on('accountData', this._onAccountData); MatrixClientPeg.get().on('sync', this._onSync); + this.dispatcherRef = dis.register(this._onAction); this._recheck(); } @@ -73,6 +80,10 @@ export default class DeviceListener { MatrixClientPeg.get().removeListener('accountData', this._onAccountData); MatrixClientPeg.get().removeListener('sync', this._onSync); } + if (this.dispatcherRef) { + dis.unregister(this.dispatcherRef); + this.dispatcherRef = null; + } this.dismissed.clear(); this.dismissedThisDeviceToast = false; this.keyBackupInfo = null; @@ -158,6 +169,11 @@ export default class DeviceListener { if (state === 'PREPARED' && prevState === null) this._recheck(); }; + _onAction = ({ action }) => { + if (action !== "on_logged_in") return; + this._recheck(); + }; + // The server doesn't tell us when key backup is set up, so we poll // & cache the result async _getKeyBackupInfo() { @@ -170,6 +186,9 @@ export default class DeviceListener { } private shouldShowSetupEncryptionToast() { + // If we're in the middle of a secret storage operation, we're likely + // modifying the state involved here, so don't add new toasts to setup. + if (isSecretStorageBeingAccessed()) return false; // In a default configuration, show the toasts. If the well-known config causes e2ee default to be false // then do not show the toasts until user is in at least one encrypted room. if (privateShouldBeEncrypted()) return true; @@ -207,7 +226,15 @@ export default class DeviceListener { showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION); } else { // No cross-signing or key backup on account (set up encryption) - showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + await cli.waitForClientWellKnown(); + if (isSecureBackupRequired() && isLoggedIn()) { + // If we're meant to set up, and Secure Backup is required, + // trigger the flow directly without a toast once logged in. + hideSetupEncryptionToast(); + accessSecretStorage(); + } else { + showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + } } } } diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 53b3033330..47faa35df4 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -30,6 +30,7 @@ import StyledRadioButton from '../../../../components/views/elements/StyledRadio import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; +import { isSecureBackupRequired } from '../../../../utils/WellKnownUtils'; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; @@ -85,8 +86,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { canUploadKeysWithPasswordOnly: null, accountPassword: props.accountPassword || "", accountPasswordCorrect: null, - passPhraseKeySelected: CREATE_STORAGE_OPTION_KEY, + canSkip: !isSecureBackupRequired(), }; this._passphraseField = createRef(); @@ -470,7 +471,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { primaryButton={_t("Continue")} onPrimaryButtonClick={this._onChooseKeyPassphraseFormSubmit} onCancel={this._onCancelClick} - hasCancel={true} + hasCancel={this.state.canSkip} /> ; } @@ -687,7 +688,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
@@ -714,7 +715,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _titleForPhase(phase) { switch (phase) { case PHASE_CHOOSE_KEY_PASSPHRASE: - return _t('Set up Secure backup'); + return _t('Set up Secure Backup'); case PHASE_MIGRATE: return _t('Upgrade your encryption'); case PHASE_PASSPHRASE: @@ -742,7 +743,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 460148045c..a10af429b9 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -2049,3 +2049,12 @@ export default class MatrixChat extends React.PureComponent { ; } } + +export function isLoggedIn(): boolean { + // JRS: Maybe we should move the step that writes this to the window out of + // `element-web` and into this file? Better yet, we should probably create a + // store to hold this state. + // See also https://github.com/vector-im/element-web/issues/15034. + const app = window.matrixChat; + return app && (app as MatrixChat).state.view === Views.LOGGED_IN; +} diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index a7a2c768db..8a74276f58 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -21,6 +21,7 @@ import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; +import { isSecureBackupRequired } from '../../../utils/WellKnownUtils'; export default class KeyBackupPanel extends React.PureComponent { constructor(props) { @@ -315,14 +316,19 @@ export default class KeyBackupPanel extends React.PureComponent { trustedLocally = _t("This backup is trusted because it has been restored on this session"); } + let deleteBackupButton; + if (!isSecureBackupRequired()) { + deleteBackupButton = + {_t("Delete Backup")} + ; + } + const buttonRow = (
{restoreButtonCaption}     - - {_t("Delete Backup")} - + {deleteBackupButton}
); diff --git a/src/createRoom.ts b/src/createRoom.ts index 23a664a4c4..78d0cf1356 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -26,8 +26,7 @@ import dis from "./dispatcher/dispatcher"; import * as Rooms from "./Rooms"; import DMRoomMap from "./utils/DMRoomMap"; import {getAddressType} from "./UserAddress"; - -const E2EE_WK_KEY = "im.vector.riot.e2ee"; +import { getE2EEWellKnown } from "./utils/WellKnownUtils"; // we define a number of interfaces which take their names from the js-sdk /* eslint-disable camelcase */ @@ -294,12 +293,11 @@ export async function ensureDMExists(client: MatrixClient, userId: string): Prom return roomId; } -export function privateShouldBeEncrypted() { - const clientWellKnown = MatrixClientPeg.get().getClientWellKnown(); - if (clientWellKnown && clientWellKnown[E2EE_WK_KEY]) { - const defaultDisabled = clientWellKnown[E2EE_WK_KEY]["default"] === false; +export function privateShouldBeEncrypted(): boolean { + const e2eeWellKnown = getE2EEWellKnown(); + if (e2eeWellKnown) { + const defaultDisabled = e2eeWellKnown["default"] === false; return !defaultDisabled; } - return true; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d0627efcc4..c12b57c033 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2238,7 +2238,7 @@ "Retry": "Retry", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", "You can also set up Secure Backup & manage your keys in Settings.": "You can also set up Secure Backup & manage your keys in Settings.", - "Set up Secure backup": "Set up Secure backup", + "Set up Secure Backup": "Set up Secure Backup", "Upgrade your encryption": "Upgrade your encryption", "Set a Security Phrase": "Set a Security Phrase", "Confirm Security Phrase": "Confirm Security Phrase", diff --git a/src/utils/WellKnownUtils.ts b/src/utils/WellKnownUtils.ts new file mode 100644 index 0000000000..46d9638ecd --- /dev/null +++ b/src/utils/WellKnownUtils.ts @@ -0,0 +1,40 @@ +/* +Copyright 2020 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {MatrixClientPeg} from '../MatrixClientPeg'; + +const E2EE_WK_KEY = "io.element.e2ee"; +const E2EE_WK_KEY_DEPRECATED = "im.vector.riot.e2ee"; + +export interface IE2EEWellKnown { + default?: boolean; +} + +export function getE2EEWellKnown(): IE2EEWellKnown { + const clientWellKnown = MatrixClientPeg.get().getClientWellKnown(); + if (clientWellKnown && clientWellKnown[E2EE_WK_KEY]) { + return clientWellKnown[E2EE_WK_KEY]; + } + if (clientWellKnown && clientWellKnown[E2EE_WK_KEY_DEPRECATED]) { + return clientWellKnown[E2EE_WK_KEY_DEPRECATED] + } + return null; +} + +export function isSecureBackupRequired(): boolean { + const wellKnown = getE2EEWellKnown(); + return wellKnown && wellKnown["secure_backup_required"] === true; +}