2020-01-17 19:43:35 +08:00
|
|
|
/*
|
|
|
|
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';
|
|
|
|
import SettingsStore from './settings/SettingsStore';
|
|
|
|
import * as sdk from './index';
|
|
|
|
import { _t } from './languageHandler';
|
|
|
|
import ToastStore from './stores/ToastStore';
|
|
|
|
|
2020-01-26 01:08:31 +08:00
|
|
|
function toastKey(deviceId) {
|
2020-01-29 23:00:01 +08:00
|
|
|
return 'unverified_session_' + deviceId;
|
2020-01-17 19:43:35 +08:00
|
|
|
}
|
|
|
|
|
2020-01-26 00:52:12 +08:00
|
|
|
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
|
|
|
const THIS_DEVICE_TOAST_KEY = 'setupencryption';
|
|
|
|
|
2020-01-17 19:43:35 +08:00
|
|
|
export default class DeviceListener {
|
|
|
|
static sharedInstance() {
|
|
|
|
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener();
|
|
|
|
return global.mx_DeviceListener;
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor() {
|
2020-01-27 18:17:16 +08:00
|
|
|
// set of device IDs we're currently showing toasts for
|
2020-01-26 01:08:31 +08:00
|
|
|
this._activeNagToasts = new Set();
|
2020-01-17 19:43:35 +08:00
|
|
|
// device IDs for which the user has dismissed the verify toast ('Later')
|
|
|
|
this._dismissed = new Set();
|
2020-01-26 00:52:12 +08:00
|
|
|
// 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;
|
2020-01-17 19:43:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
start() {
|
|
|
|
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
|
|
|
|
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
2020-01-26 00:52:12 +08:00
|
|
|
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
2020-03-21 02:53:31 +08:00
|
|
|
MatrixClientPeg.get().on('accountData', this._onAccountData);
|
2020-01-26 00:52:12 +08:00
|
|
|
this._recheck();
|
2020-01-17 19:43:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
if (MatrixClientPeg.get()) {
|
|
|
|
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
|
|
|
|
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
2020-01-26 00:52:12 +08:00
|
|
|
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
2020-03-21 02:53:31 +08:00
|
|
|
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
|
2020-01-17 19:43:35 +08:00
|
|
|
}
|
|
|
|
this._dismissed.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
dismissVerification(deviceId) {
|
|
|
|
this._dismissed.add(deviceId);
|
2020-01-26 00:52:12 +08:00
|
|
|
this._recheck();
|
|
|
|
}
|
|
|
|
|
|
|
|
dismissEncryptionSetup() {
|
|
|
|
this._dismissedThisDeviceToast = true;
|
|
|
|
this._recheck();
|
2020-01-17 19:43:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
_onDevicesUpdated = (users) => {
|
|
|
|
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
2020-01-26 00:52:12 +08:00
|
|
|
this._recheck();
|
2020-01-17 19:43:35 +08:00
|
|
|
}
|
|
|
|
|
2020-01-30 05:55:27 +08:00
|
|
|
_onDeviceVerificationChanged = (userId) => {
|
|
|
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
2020-01-26 00:52:12 +08:00
|
|
|
this._recheck();
|
|
|
|
}
|
|
|
|
|
|
|
|
_onUserTrustStatusChanged = (userId, trustLevel) => {
|
|
|
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
|
|
|
this._recheck();
|
|
|
|
}
|
|
|
|
|
2020-03-21 02:53:31 +08:00
|
|
|
_onAccountData = (ev) => {
|
|
|
|
// User may have migrated SSSS to symmetric, in which case we can dismiss that toast
|
|
|
|
if (ev.getType().startsWith('m.secret_storage.key.')) {
|
|
|
|
this._recheck();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-26 00:52:12 +08:00
|
|
|
// 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;
|
2020-01-17 19:43:35 +08:00
|
|
|
}
|
|
|
|
|
2020-01-26 00:52:12 +08:00
|
|
|
async _recheck() {
|
2020-01-17 19:43:35 +08:00
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
2020-03-13 02:03:18 +08:00
|
|
|
if (
|
|
|
|
!SettingsStore.isFeatureEnabled("feature_cross_signing") ||
|
|
|
|
!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
|
|
|
) return;
|
|
|
|
|
2020-01-26 00:52:12 +08:00
|
|
|
if (!cli.isCryptoEnabled()) return;
|
2020-03-24 02:36:37 +08:00
|
|
|
|
2020-03-24 21:03:07 +08:00
|
|
|
const crossSigningReady = await cli.isCrossSigningReady();
|
2020-03-24 02:36:37 +08:00
|
|
|
|
|
|
|
if (!crossSigningReady) {
|
2020-01-26 00:52:12 +08:00
|
|
|
if (this._dismissedThisDeviceToast) {
|
|
|
|
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// cross signing isn't enabled - nag to enable it
|
2020-01-27 17:44:12 +08:00
|
|
|
// There are 3 different toasts for:
|
2020-01-26 00:52:12 +08:00
|
|
|
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,
|
2020-01-27 17:44:39 +08:00
|
|
|
title: _t("Verify this session"),
|
2020-01-26 00:52:12 +08:00
|
|
|
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;
|
2020-03-17 05:31:26 +08:00
|
|
|
} else if (await cli.secretStorageKeyNeedsUpgrade()) {
|
2020-03-21 03:01:26 +08:00
|
|
|
if (this._dismissedThisDeviceToast) {
|
|
|
|
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-17 05:31:26 +08:00
|
|
|
ToastStore.sharedInstance().addOrReplaceToast({
|
|
|
|
key: THIS_DEVICE_TOAST_KEY,
|
|
|
|
title: _t("Encryption upgrade available"),
|
|
|
|
icon: "verification_warning",
|
2020-03-25 03:02:57 +08:00
|
|
|
props: {kind: 'upgrade_ssss'},
|
2020-03-17 05:31:26 +08:00
|
|
|
component: sdk.getComponent("toasts.SetupEncryptionToast"),
|
|
|
|
});
|
2020-01-26 00:52:12 +08:00
|
|
|
} else {
|
|
|
|
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
|
|
|
}
|
2020-01-17 22:08:37 +08:00
|
|
|
|
2020-01-26 01:08:31 +08:00
|
|
|
const newActiveToasts = new Set();
|
|
|
|
|
2020-01-17 19:43:35 +08:00
|
|
|
const devices = await cli.getStoredDevicesForUser(cli.getUserId());
|
|
|
|
for (const device of devices) {
|
|
|
|
if (device.deviceId == cli.deviceId) continue;
|
|
|
|
|
|
|
|
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
|
2020-01-21 19:33:09 +08:00
|
|
|
if (deviceTrust.isCrossSigningVerified() || this._dismissed.has(device.deviceId)) {
|
2020-01-26 01:08:31 +08:00
|
|
|
ToastStore.sharedInstance().dismissToast(toastKey(device.deviceId));
|
2020-01-17 19:43:35 +08:00
|
|
|
} else {
|
2020-01-26 01:08:31 +08:00
|
|
|
this._activeNagToasts.add(device.deviceId);
|
2020-01-17 19:43:35 +08:00
|
|
|
ToastStore.sharedInstance().addOrReplaceToast({
|
2020-01-26 01:08:31 +08:00
|
|
|
key: toastKey(device.deviceId),
|
2020-03-27 00:37:44 +08:00
|
|
|
title: _t("Unverified login. Was this you?"),
|
2020-01-17 19:43:35 +08:00
|
|
|
icon: "verification_warning",
|
2020-01-29 23:00:01 +08:00
|
|
|
props: { device },
|
|
|
|
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
|
2020-01-17 19:43:35 +08:00
|
|
|
});
|
2020-01-26 01:08:31 +08:00
|
|
|
newActiveToasts.add(device.deviceId);
|
2020-01-17 19:43:35 +08:00
|
|
|
}
|
|
|
|
}
|
2020-01-26 01:08:31 +08:00
|
|
|
|
|
|
|
// clear any other outstanding toasts (eg. logged out devices)
|
|
|
|
for (const deviceId of this._activeNagToasts) {
|
|
|
|
if (!newActiveToasts.has(deviceId)) ToastStore.sharedInstance().dismissToast(toastKey(deviceId));
|
|
|
|
}
|
|
|
|
this._activeNagToasts = newActiveToasts;
|
2020-01-17 19:43:35 +08:00
|
|
|
}
|
|
|
|
}
|