diff --git a/src/DeviceListener.js b/src/DeviceListener.js index a4c5785db4..32024d1d87 100644 --- a/src/DeviceListener.js +++ b/src/DeviceListener.js @@ -24,6 +24,9 @@ function toastKey(device) { return 'newsession_' + device.deviceId; } +const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; +const THIS_DEVICE_TOAST_KEY = 'setupencryption'; + export default class DeviceListener { static sharedInstance() { if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener(); @@ -33,42 +36,114 @@ export default class DeviceListener { constructor() { // device IDs for which the user has dismissed the verify toast ('Later') this._dismissed = new Set(); + // has the user dismissed any of the various nag toasts to setup encryption on this device? + this._dismissedThisDeviceToast = false; + + // cache of the key backup info + this._keyBackupInfo = null; + this._keyBackupFetchedAt = null; } start() { MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated); MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged); - this.recheck(); + MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); + this._recheck(); } stop() { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated); MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged); + MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); } this._dismissed.clear(); } dismissVerification(deviceId) { this._dismissed.add(deviceId); - this.recheck(); + this._recheck(); + } + + dismissEncryptionSetup() { + this._dismissedThisDeviceToast = true; + this._recheck(); } _onDevicesUpdated = (users) => { if (!users.includes(MatrixClientPeg.get().getUserId())) return; - this.recheck(); + this._recheck(); } _onDeviceVerificationChanged = (users) => { if (!users.includes(MatrixClientPeg.get().getUserId())) return; - this.recheck(); + this._recheck(); } - async recheck() { + _onUserTrustStatusChanged = (userId, trustLevel) => { + if (userId !== MatrixClientPeg.get().getUserId()) return; + this._recheck(); + } + + // The server doesn't tell us when key backup is set up, so we poll + // & cache the result + async _getKeyBackupInfo() { + const now = (new Date()).getTime(); + if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { + this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + this._keyBackupFetchedAt = now; + } + return this._keyBackupInfo; + } + + async _recheck() { if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) return; const cli = MatrixClientPeg.get(); - if (!cli.isCryptoEnabled()) return false; + if (!cli.isCryptoEnabled()) return; + if (!cli.getCrossSigningId()) { + if (this._dismissedThisDeviceToast) { + ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); + return; + } + + // cross signing isn't enabled - nag to enable it + // There are 3 different toasts for: + if (cli.getStoredCrossSigningForUser(cli.getUserId())) { + // Cross-signing on account but this device doesn't trust the master key (verify this session) + ToastStore.sharedInstance().addOrReplaceToast({ + key: THIS_DEVICE_TOAST_KEY, + title: _t("Verify this session"), + icon: "verification_warning", + props: {kind: 'verify_this_session'}, + component: sdk.getComponent("toasts.SetupEncryptionToast"), + }); + } else { + const backupInfo = await this._getKeyBackupInfo(); + if (backupInfo) { + // No cross-signing on account but key backup available (upgrade encryption) + ToastStore.sharedInstance().addOrReplaceToast({ + key: THIS_DEVICE_TOAST_KEY, + title: _t("Encryption upgrade available"), + icon: "verification_warning", + props: {kind: 'upgrade_encryption'}, + component: sdk.getComponent("toasts.SetupEncryptionToast"), + }); + } else { + // No cross-signing or key backup on account (set up encryption) + ToastStore.sharedInstance().addOrReplaceToast({ + key: THIS_DEVICE_TOAST_KEY, + title: _t("Set up encryption"), + icon: "verification_warning", + props: {kind: 'set_up_encryption'}, + component: sdk.getComponent("toasts.SetupEncryptionToast"), + }); + } + } + return; + } else { + ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); + } const devices = await cli.getStoredDevicesForUser(cli.getUserId()); for (const device of devices) { diff --git a/src/components/views/toasts/NewSessionToast.js b/src/components/views/toasts/NewSessionToast.js index f83326121b..3b60f59131 100644 --- a/src/components/views/toasts/NewSessionToast.js +++ b/src/components/views/toasts/NewSessionToast.js @@ -32,7 +32,7 @@ export default class VerifySessionToast extends React.PureComponent { DeviceListener.sharedInstance().dismissVerification(this.props.deviceId); }; - _onVerifyClick = async () => { + _onReviewClick = async () => { const cli = MatrixClientPeg.get(); const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); @@ -47,10 +47,10 @@ export default class VerifySessionToast extends React.PureComponent { render() { const FormButton = sdk.getComponent("elements.FormButton"); return (
-
{_t("Other users may not trust it")}
+
{_t("Review & verify your new session")}
- +
); } diff --git a/src/components/views/toasts/SetupEncryptionToast.js b/src/components/views/toasts/SetupEncryptionToast.js new file mode 100644 index 0000000000..841ee66ac7 --- /dev/null +++ b/src/components/views/toasts/SetupEncryptionToast.js @@ -0,0 +1,68 @@ +/* +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 React from 'react'; +import PropTypes from 'prop-types'; +import * as sdk from "../../../index"; +import { _t } from '../../../languageHandler'; +import DeviceListener from '../../../DeviceListener'; +import { accessSecretStorage } from '../../../CrossSigningManager'; + +export default class SetupEncryptionToast extends React.PureComponent { + static propTypes = { + toastKey: PropTypes.string.isRequired, + kind: PropTypes.oneOf(['set_up_encryption', 'verify_this_session', 'upgrade_encryption']).isRequired, + }; + + _onLaterClick = () => { + DeviceListener.sharedInstance().dismissEncryptionSetup(); + }; + + _onSetupClick = async () => { + accessSecretStorage(); + }; + + getDescription() { + switch (this.props.kind) { + case 'set_up_encryption': + case 'upgrade_encryption': + return _t('Verify your other devices easier'); + case 'verify_this_session': + return _t('Other users may not trust it'); + } + } + + getSetupCaption() { + switch (this.props.kind) { + case 'set_up_encryption': + case 'upgrade_encryption': + return _t('Upgrade'); + case 'verify_this_session': + return _t('Verify'); + } + } + + render() { + const FormButton = sdk.getComponent("elements.FormButton"); + return (
+
{this.getDescription()}
+
+ + +
+
); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 79f5731aed..5c2f427c6a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -88,6 +88,9 @@ "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", + "Verify this session": "Verify this session", + "Encryption upgrade available": "Encryption upgrade available", + "Set up encryption": "Set up encryption", "New Session": "New Session", "Who would you like to add to this community?": "Who would you like to add to this community?", "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID", @@ -509,8 +512,12 @@ "Headphones": "Headphones", "Folder": "Folder", "Pin": "Pin", - "Other users may not trust it": "Other users may not trust it", + "Review & verify your new session": "Review & verify your new session", "Later": "Later", + "Review": "Review", + "Verify your other devices easier": "Verify your other devices easier", + "Other users may not trust it": "Other users may not trust it", + "Upgrade": "Upgrade", "Verify": "Verify", "Decline (%(counter)s)": "Decline (%(counter)s)", "Accept to continue:": "Accept to continue:", @@ -1514,7 +1521,6 @@ "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.", "This usually only affects how the room is processed on the server. If you're having problems with your Riot, please report a bug.": "This usually only affects how the room is processed on the server. If you're having problems with your Riot, please report a bug.", "You'll upgrade this room from to .": "You'll upgrade this room from to .", - "Upgrade": "Upgrade", "Sign out and remove encryption keys?": "Sign out and remove encryption keys?", "Clear Storage and Sign Out": "Clear Storage and Sign Out", "Send Logs": "Send Logs", @@ -2008,7 +2014,6 @@ "Set up secret storage": "Set up secret storage", "Restore your Key Backup": "Restore your Key Backup", "Upgrade your encryption": "Upgrade your encryption", - "Set up encryption": "Set up encryption", "Recovery key": "Recovery key", "Keep it safe": "Keep it safe", "Storing secrets...": "Storing secrets...",