mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 17:56:01 +03:00
Merge pull request #5130 from matrix-org/jryans/secure-backup-required
Enforce Secure Backup completion when requested by HS
This commit is contained in:
commit
8e0742b9fe
9 changed files with 125 additions and 19 deletions
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
|
@ -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<Renderer>;
|
||||
mxMatrixClientPeg: IMatrixClientPeg;
|
||||
Olm: {
|
||||
init: () => Promise<void>;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<string>();
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</form>;
|
||||
}
|
||||
|
@ -687,7 +688,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._onLoadRetryClick}
|
||||
hasCancel={true}
|
||||
hasCancel={this.state.canSkip}
|
||||
onCancel={this._onCancel}
|
||||
/>
|
||||
</div>
|
||||
|
@ -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 {
|
|||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._bootstrapSecretStorage}
|
||||
hasCancel={true}
|
||||
hasCancel={this.state.canSkip}
|
||||
onCancel={this._onCancel}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -2049,3 +2049,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
</ErrorBoundary>;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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 = <AccessibleButton kind="danger" onClick={this._deleteBackup}>
|
||||
{_t("Delete Backup")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const buttonRow = (
|
||||
<div className="mx_KeyBackupPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
|
||||
{restoreButtonCaption}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
|
||||
{_t("Delete Backup")}
|
||||
</AccessibleButton>
|
||||
{deleteBackupButton}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
40
src/utils/WellKnownUtils.ts
Normal file
40
src/utils/WellKnownUtils.ts
Normal file
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue