2546e23a3e
Each recheck caused a GET to account data to see if the master key exists if done before the initial sync, which is unnecessary here. This just makes it wait until the initial sync is done to recheck. Fixes https://github.com/vector-im/riot-web/issues/13279
229 lines
9.6 KiB
JavaScript
229 lines
9.6 KiB
JavaScript
/*
|
|
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';
|
|
|
|
function toastKey(deviceId) {
|
|
return 'unverified_session_' + 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();
|
|
return global.mx_DeviceListener;
|
|
}
|
|
|
|
constructor() {
|
|
// set of device IDs we're currently showing toasts for
|
|
this._activeNagToasts = new Set();
|
|
// 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);
|
|
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
|
MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
|
MatrixClientPeg.get().on('accountData', this._onAccountData);
|
|
MatrixClientPeg.get().on('sync', this._onSync);
|
|
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);
|
|
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
|
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
|
|
MatrixClientPeg.get().removeListener('sync', this._onSync);
|
|
}
|
|
this._dismissed.clear();
|
|
}
|
|
|
|
dismissVerification(deviceId) {
|
|
this._dismissed.add(deviceId);
|
|
this._recheck();
|
|
}
|
|
|
|
dismissEncryptionSetup() {
|
|
this._dismissedThisDeviceToast = true;
|
|
this._recheck();
|
|
}
|
|
|
|
_onDevicesUpdated = (users) => {
|
|
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
|
this._recheck();
|
|
}
|
|
|
|
_onDeviceVerificationChanged = (userId) => {
|
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
|
this._recheck();
|
|
}
|
|
|
|
_onUserTrustStatusChanged = (userId, trustLevel) => {
|
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
|
this._recheck();
|
|
}
|
|
|
|
_onCrossSingingKeysChanged = () => {
|
|
this._recheck();
|
|
}
|
|
|
|
_onAccountData = (ev) => {
|
|
// User may have:
|
|
// * migrated SSSS to symmetric
|
|
// * uploaded keys to secret storage
|
|
// * completed secret storage creation
|
|
// which result in account data changes affecting checks below.
|
|
if (
|
|
ev.getType().startsWith('m.secret_storage.') ||
|
|
ev.getType().startsWith('m.cross_signing.')
|
|
) {
|
|
this._recheck();
|
|
}
|
|
}
|
|
|
|
_onSync = (state, prevState) => {
|
|
if (state === 'PREPARED' && prevState === null) 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() {
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
if (
|
|
!SettingsStore.getValue("feature_cross_signing") ||
|
|
!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
|
) return;
|
|
|
|
if (!cli.isCryptoEnabled()) return;
|
|
// don't recheck until the initial sync is complete: lots of account data events will fire
|
|
// while the initial sync is processing and we don't need to recheck on each one of them
|
|
// (we add a listener on sync to do once check after the initial sync is done)
|
|
if (!cli.isInitialSyncComplete()) return;
|
|
|
|
const crossSigningReady = await cli.isCrossSigningReady();
|
|
|
|
if (this._dismissedThisDeviceToast) {
|
|
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
|
} else {
|
|
if (!crossSigningReady) {
|
|
// 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 if (await cli.secretStorageKeyNeedsUpgrade()) {
|
|
ToastStore.sharedInstance().addOrReplaceToast({
|
|
key: THIS_DEVICE_TOAST_KEY,
|
|
title: _t("Encryption upgrade available"),
|
|
icon: "verification_warning",
|
|
props: {kind: 'upgrade_ssss'},
|
|
component: sdk.getComponent("toasts.SetupEncryptionToast"),
|
|
});
|
|
} else {
|
|
// cross-signing is ready, and we don't need to upgrade encryption
|
|
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
|
}
|
|
}
|
|
|
|
// as long as cross-signing isn't ready,
|
|
// you can't see or dismiss any device toasts
|
|
if (crossSigningReady) {
|
|
const newActiveToasts = new Set();
|
|
|
|
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);
|
|
if (deviceTrust.isCrossSigningVerified() || this._dismissed.has(device.deviceId)) {
|
|
ToastStore.sharedInstance().dismissToast(toastKey(device.deviceId));
|
|
} else {
|
|
this._activeNagToasts.add(device.deviceId);
|
|
ToastStore.sharedInstance().addOrReplaceToast({
|
|
key: toastKey(device.deviceId),
|
|
title: _t("Unverified login. Was this you?"),
|
|
icon: "verification_warning",
|
|
props: { device },
|
|
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
|
|
});
|
|
newActiveToasts.add(device.deviceId);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|