Merge pull request #5130 from matrix-org/jryans/secure-backup-required

Enforce Secure Backup completion when requested by HS
This commit is contained in:
J. Ryan Stinnett 2020-08-24 17:32:58 +01:00 committed by GitHub
commit 8e0742b9fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 125 additions and 19 deletions

View file

@ -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>;

View file

@ -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) {

View file

@ -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);
}
}
}
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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>&nbsp;&nbsp;&nbsp;
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
{_t("Delete Backup")}
</AccessibleButton>
{deleteBackupButton}
</div>
);

View file

@ -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;
}

View file

@ -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",

View 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;
}