2020-01-17 11:43:35 +00: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-25 16:52:12 +00:00
|
|
|
|
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
|
|
|
|
const THIS_DEVICE_TOAST_KEY = 'setupencryption';
|
2020-04-27 17:33:54 +00:00
|
|
|
|
const OTHER_DEVICES_TOAST_KEY = 'reviewsessions';
|
2020-01-25 16:52:12 +00:00
|
|
|
|
|
2020-04-28 17:35:16 +00:00
|
|
|
|
function toastKey(deviceId) {
|
|
|
|
|
return "unverified_session_" + deviceId;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-17 11:43:35 +00:00
|
|
|
|
export default class DeviceListener {
|
2020-05-22 11:54:03 +00:00
|
|
|
|
// 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?
|
|
|
|
|
private dismissedThisDeviceToast = false;
|
|
|
|
|
// cache of the key backup info
|
|
|
|
|
private keyBackupInfo: object = null;
|
|
|
|
|
private keyBackupFetchedAt: number = null;
|
|
|
|
|
// We keep a list of our own device IDs so we can batch ones that were already
|
|
|
|
|
// there the last time the app launched into a single toast, but display new
|
|
|
|
|
// ones in their own toasts.
|
|
|
|
|
private ourDeviceIdsAtStart: Set<string> = null;
|
|
|
|
|
// The set of device IDs we're currently displaying toasts for
|
|
|
|
|
private displayingToastsForDeviceIds = new Set<string>();
|
2020-01-25 16:52:12 +00:00
|
|
|
|
|
2020-05-22 11:54:03 +00:00
|
|
|
|
static sharedInstance() {
|
|
|
|
|
if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener();
|
|
|
|
|
return window.mx_DeviceListener;
|
2020-01-17 11:43:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
start() {
|
2020-04-28 17:35:16 +00:00
|
|
|
|
MatrixClientPeg.get().on('crypto.willUpdateDevices', this._onWillUpdateDevices);
|
2020-01-17 11:43:35 +00:00
|
|
|
|
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
|
|
|
|
|
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
2020-01-25 16:52:12 +00:00
|
|
|
|
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
2020-04-09 11:43:51 +00:00
|
|
|
|
MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
2020-03-20 18:53:31 +00:00
|
|
|
|
MatrixClientPeg.get().on('accountData', this._onAccountData);
|
2020-04-20 13:36:15 +00:00
|
|
|
|
MatrixClientPeg.get().on('sync', this._onSync);
|
2020-01-25 16:52:12 +00:00
|
|
|
|
this._recheck();
|
2020-01-17 11:43:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
|
if (MatrixClientPeg.get()) {
|
2020-04-28 17:35:16 +00:00
|
|
|
|
MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this._onWillUpdateDevices);
|
2020-01-17 11:43:35 +00:00
|
|
|
|
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
|
|
|
|
|
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
2020-01-25 16:52:12 +00:00
|
|
|
|
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
2020-04-09 11:43:51 +00:00
|
|
|
|
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
2020-03-20 18:53:31 +00:00
|
|
|
|
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
|
2020-04-20 13:36:15 +00:00
|
|
|
|
MatrixClientPeg.get().removeListener('sync', this._onSync);
|
2020-01-17 11:43:35 +00:00
|
|
|
|
}
|
2020-05-22 11:54:03 +00:00
|
|
|
|
this.dismissed.clear();
|
|
|
|
|
this.dismissedThisDeviceToast = false;
|
|
|
|
|
this.keyBackupInfo = null;
|
|
|
|
|
this.keyBackupFetchedAt = null;
|
|
|
|
|
this.ourDeviceIdsAtStart = null;
|
|
|
|
|
this.displayingToastsForDeviceIds = new Set();
|
2020-01-17 11:43:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-28 17:35:16 +00:00
|
|
|
|
/**
|
|
|
|
|
* Dismiss notifications about our own unverified devices
|
|
|
|
|
*
|
|
|
|
|
* @param {String[]} deviceIds List of device IDs to dismiss notifications for
|
|
|
|
|
*/
|
2020-05-22 11:54:03 +00:00
|
|
|
|
async dismissUnverifiedSessions(deviceIds: string[]) {
|
2020-04-28 17:35:16 +00:00
|
|
|
|
for (const d of deviceIds) {
|
2020-05-22 11:54:03 +00:00
|
|
|
|
this.dismissed.add(d);
|
2020-04-28 17:35:16 +00:00
|
|
|
|
}
|
2020-04-27 17:33:54 +00:00
|
|
|
|
|
2020-01-25 16:52:12 +00:00
|
|
|
|
this._recheck();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dismissEncryptionSetup() {
|
2020-05-22 11:54:03 +00:00
|
|
|
|
this.dismissedThisDeviceToast = true;
|
2020-01-25 16:52:12 +00:00
|
|
|
|
this._recheck();
|
2020-01-17 11:43:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-28 17:35:16 +00:00
|
|
|
|
_ensureDeviceIdsAtStartPopulated() {
|
2020-05-22 11:54:03 +00:00
|
|
|
|
if (this.ourDeviceIdsAtStart === null) {
|
2020-04-28 17:35:16 +00:00
|
|
|
|
const cli = MatrixClientPeg.get();
|
2020-05-22 11:54:03 +00:00
|
|
|
|
this.ourDeviceIdsAtStart = new Set(
|
2020-04-28 17:35:16 +00:00
|
|
|
|
cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 11:54:03 +00:00
|
|
|
|
_onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
|
2020-04-29 16:33:18 +00:00
|
|
|
|
// If we didn't know about *any* devices before (ie. it's fresh login),
|
2020-04-29 16:16:04 +00:00
|
|
|
|
// then they are all pre-existing devices, so ignore this and set the
|
|
|
|
|
// devicesAtStart list to the devices that we see after the fetch.
|
|
|
|
|
if (initialFetch) return;
|
|
|
|
|
|
2020-04-28 17:35:16 +00:00
|
|
|
|
const myUserId = MatrixClientPeg.get().getUserId();
|
|
|
|
|
if (users.includes(myUserId)) this._ensureDeviceIdsAtStartPopulated();
|
2020-04-29 09:55:44 +00:00
|
|
|
|
|
|
|
|
|
// No need to do a recheck here: we just need to get a snapshot of our devices
|
2020-04-29 10:25:18 +00:00
|
|
|
|
// before we download any new ones.
|
2020-04-28 17:35:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 11:54:03 +00:00
|
|
|
|
_onDevicesUpdated = (users: string[]) => {
|
2020-01-17 11:43:35 +00:00
|
|
|
|
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
2020-01-25 16:52:12 +00:00
|
|
|
|
this._recheck();
|
2020-01-17 11:43:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 11:54:03 +00:00
|
|
|
|
_onDeviceVerificationChanged = (userId: string) => {
|
2020-01-29 21:55:27 +00:00
|
|
|
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
2020-01-25 16:52:12 +00:00
|
|
|
|
this._recheck();
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 11:54:03 +00:00
|
|
|
|
_onUserTrustStatusChanged = (userId: string) => {
|
2020-01-25 16:52:12 +00:00
|
|
|
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
|
|
|
|
this._recheck();
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-09 11:43:51 +00:00
|
|
|
|
_onCrossSingingKeysChanged = () => {
|
|
|
|
|
this._recheck();
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-20 18:53:31 +00:00
|
|
|
|
_onAccountData = (ev) => {
|
2020-04-07 09:57:10 +00:00
|
|
|
|
// 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.')
|
|
|
|
|
) {
|
2020-03-20 18:53:31 +00:00
|
|
|
|
this._recheck();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-20 13:36:15 +00:00
|
|
|
|
_onSync = (state, prevState) => {
|
|
|
|
|
if (state === 'PREPARED' && prevState === null) this._recheck();
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-25 16:52:12 +00: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();
|
2020-05-22 11:54:03 +00:00
|
|
|
|
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
|
|
|
|
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
|
|
|
|
this.keyBackupFetchedAt = now;
|
2020-01-25 16:52:12 +00:00
|
|
|
|
}
|
2020-05-22 11:54:03 +00:00
|
|
|
|
return this.keyBackupInfo;
|
2020-01-17 11:43:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-25 16:52:12 +00:00
|
|
|
|
async _recheck() {
|
2020-01-17 11:43:35 +00:00
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
|
2020-03-12 18:03:18 +00:00
|
|
|
|
if (
|
2020-04-15 19:18:42 +00:00
|
|
|
|
!SettingsStore.getValue("feature_cross_signing") ||
|
2020-03-12 18:03:18 +00:00
|
|
|
|
!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
|
|
|
|
) return;
|
|
|
|
|
|
2020-01-25 16:52:12 +00:00
|
|
|
|
if (!cli.isCryptoEnabled()) return;
|
2020-04-20 13:36:15 +00:00
|
|
|
|
// 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;
|
2020-03-23 18:36:37 +00:00
|
|
|
|
|
2020-03-24 13:03:07 +00:00
|
|
|
|
const crossSigningReady = await cli.isCrossSigningReady();
|
2020-03-23 18:36:37 +00:00
|
|
|
|
|
2020-05-22 11:54:03 +00:00
|
|
|
|
if (this.dismissedThisDeviceToast) {
|
2020-03-27 15:45:46 +00:00
|
|
|
|
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
|
|
|
|
} else {
|
|
|
|
|
if (!crossSigningReady) {
|
2020-04-24 14:58:28 +00:00
|
|
|
|
// make sure our keys are finished downlaoding
|
|
|
|
|
await cli.downloadKeys([cli.getUserId()]);
|
2020-03-27 15:45:46 +00:00
|
|
|
|
// 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)
|
2020-01-25 16:52:12 +00:00
|
|
|
|
ToastStore.sharedInstance().addOrReplaceToast({
|
|
|
|
|
key: THIS_DEVICE_TOAST_KEY,
|
2020-03-27 15:45:46 +00:00
|
|
|
|
title: _t("Verify this session"),
|
2020-01-25 16:52:12 +00:00
|
|
|
|
icon: "verification_warning",
|
2020-03-27 15:45:46 +00:00
|
|
|
|
props: {kind: 'verify_this_session'},
|
2020-01-25 16:52:12 +00:00
|
|
|
|
component: sdk.getComponent("toasts.SetupEncryptionToast"),
|
|
|
|
|
});
|
|
|
|
|
} else {
|
2020-03-27 15:45:46 +00:00
|
|
|
|
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"),
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-01-25 16:52:12 +00:00
|
|
|
|
}
|
2020-04-02 14:07:55 +00:00
|
|
|
|
} else {
|
|
|
|
|
// cross-signing is ready, and we don't need to upgrade encryption
|
|
|
|
|
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
2020-03-20 19:01:26 +00:00
|
|
|
|
}
|
2020-01-25 16:52:12 +00:00
|
|
|
|
}
|
2020-01-17 14:08:37 +00:00
|
|
|
|
|
2020-04-29 16:16:04 +00:00
|
|
|
|
// This needs to be done after awaiting on downloadKeys() above, so
|
|
|
|
|
// we make sure we get the devices after the fetch is done.
|
|
|
|
|
this._ensureDeviceIdsAtStartPopulated();
|
|
|
|
|
|
2020-04-28 17:35:16 +00:00
|
|
|
|
// Unverified devices that were there last time the app ran
|
|
|
|
|
// (technically could just be a boolean: we don't actually
|
|
|
|
|
// need to remember the device IDs, but for the sake of
|
|
|
|
|
// symmetry...).
|
2020-05-22 11:54:03 +00:00
|
|
|
|
const oldUnverifiedDeviceIds = new Set<string>();
|
2020-04-28 17:35:16 +00:00
|
|
|
|
// Unverified devices that have appeared since then
|
2020-05-22 11:54:03 +00:00
|
|
|
|
const newUnverifiedDeviceIds = new Set<string>();
|
2020-04-28 17:35:16 +00:00
|
|
|
|
|
2020-03-27 15:45:46 +00:00
|
|
|
|
// as long as cross-signing isn't ready,
|
|
|
|
|
// you can't see or dismiss any device toasts
|
|
|
|
|
if (crossSigningReady) {
|
2020-04-28 17:35:16 +00:00
|
|
|
|
const devices = cli.getStoredDevicesForUser(cli.getUserId());
|
2020-03-27 15:45:46 +00:00
|
|
|
|
for (const device of devices) {
|
2020-05-22 11:54:03 +00:00
|
|
|
|
if (device.deviceId === cli.deviceId) continue;
|
2020-01-17 11:43:35 +00:00
|
|
|
|
|
2020-03-27 15:45:46 +00:00
|
|
|
|
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
|
2020-05-22 11:54:03 +00:00
|
|
|
|
if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) {
|
|
|
|
|
if (this.ourDeviceIdsAtStart.has(device.deviceId)) {
|
2020-04-28 17:35:16 +00:00
|
|
|
|
oldUnverifiedDeviceIds.add(device.deviceId);
|
|
|
|
|
} else {
|
|
|
|
|
newUnverifiedDeviceIds.add(device.deviceId);
|
|
|
|
|
}
|
2020-03-27 15:45:46 +00:00
|
|
|
|
}
|
2020-01-17 11:43:35 +00:00
|
|
|
|
}
|
2020-04-28 17:35:16 +00:00
|
|
|
|
}
|
2020-01-25 17:08:31 +00:00
|
|
|
|
|
2020-04-28 17:35:16 +00:00
|
|
|
|
// Display or hide the batch toast for old unverified sessions
|
|
|
|
|
if (oldUnverifiedDeviceIds.size > 0) {
|
|
|
|
|
ToastStore.sharedInstance().addOrReplaceToast({
|
|
|
|
|
key: OTHER_DEVICES_TOAST_KEY,
|
|
|
|
|
title: _t("Review where you’re logged in"),
|
|
|
|
|
icon: "verification_warning",
|
2020-04-29 13:49:30 +00:00
|
|
|
|
priority: ToastStore.PRIORITY_LOW,
|
2020-04-28 17:35:16 +00:00
|
|
|
|
props: {
|
|
|
|
|
deviceIds: oldUnverifiedDeviceIds,
|
|
|
|
|
},
|
|
|
|
|
component: sdk.getComponent("toasts.BulkUnverifiedSessionsToast"),
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show toasts for new unverified devices if they aren't already there
|
|
|
|
|
for (const deviceId of newUnverifiedDeviceIds) {
|
|
|
|
|
ToastStore.sharedInstance().addOrReplaceToast({
|
|
|
|
|
key: toastKey(deviceId),
|
2020-04-29 09:53:36 +00:00
|
|
|
|
title: _t("New login. Was this you?"),
|
2020-04-28 17:35:16 +00:00
|
|
|
|
icon: "verification_warning",
|
|
|
|
|
props: { deviceId },
|
|
|
|
|
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ...and hide any we don't need any more
|
2020-05-22 11:54:03 +00:00
|
|
|
|
for (const deviceId of this.displayingToastsForDeviceIds) {
|
2020-04-28 17:35:16 +00:00
|
|
|
|
if (!newUnverifiedDeviceIds.has(deviceId)) {
|
|
|
|
|
ToastStore.sharedInstance().dismissToast(toastKey(deviceId));
|
2020-03-27 15:45:46 +00:00
|
|
|
|
}
|
2020-01-25 17:08:31 +00:00
|
|
|
|
}
|
2020-04-28 17:35:16 +00:00
|
|
|
|
|
2020-05-22 11:54:03 +00:00
|
|
|
|
this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
|
2020-01-17 11:43:35 +00:00
|
|
|
|
}
|
|
|
|
|
}
|