Extract the "this device" logic from DeviceListener (#32420)
* Extract logic for this device from DeviceListener * Remove a listener that I assume we forgot in DeviceListener * Use a code block for JSON in a comment Co-authored-by: Skye Elliot <actuallyori@gmail.com> * Rename to DeviceListenerCurrentDevice --------- Co-authored-by: Skye Elliot <actuallyori@gmail.com>
This commit is contained in:
@@ -7,83 +7,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
type MatrixEvent,
|
||||
ClientEvent,
|
||||
EventType,
|
||||
type MatrixClient,
|
||||
RoomStateEvent,
|
||||
type SyncState,
|
||||
ClientStoppedError,
|
||||
TypedEventEmitter,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger as baseLogger, type BaseLogger, LogSpan } from "matrix-js-sdk/src/logger";
|
||||
import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { type MatrixClient, ClientStoppedError, TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
||||
import { logger as baseLogger, LogSpan } from "matrix-js-sdk/src/logger";
|
||||
import { type CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import {
|
||||
hideToast as hideSetupEncryptionToast,
|
||||
showToast as showSetupEncryptionToast,
|
||||
} from "./toasts/SetupEncryptionToast";
|
||||
import { isSecretStorageBeingAccessed } from "./SecurityManager";
|
||||
import { type ActionPayload } from "./dispatcher/payloads";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import PlatformPeg from "./PlatformPeg";
|
||||
import { recordClientInformation, removeClientInformation } from "./utils/device/clientInformation";
|
||||
import SettingsStore, { type CallbackFn } from "./settings/SettingsStore";
|
||||
import { asyncSomeParallel } from "./utils/arrays.ts";
|
||||
import DeviceListenerOtherDevices from "./device-listener/DeviceListenerOtherDevices.ts";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Unfortunately-named account data key used by Element X to indicate that the user
|
||||
* has chosen to disable server side key backups.
|
||||
*
|
||||
* We need to set and honour this to prevent Element X from automatically turning key backup back on.
|
||||
*/
|
||||
export const BACKUP_DISABLED_ACCOUNT_DATA_KEY = "m.org.matrix.custom.backup_disabled";
|
||||
|
||||
/**
|
||||
* Account data key to indicate whether the user has chosen to enable or disable recovery.
|
||||
*/
|
||||
export const RECOVERY_ACCOUNT_DATA_KEY = "io.element.recovery";
|
||||
import DeviceListenerCurrentDevice from "./device-listener/DeviceListenerCurrentDevice.ts";
|
||||
import type DeviceState from "./device-listener/DeviceState.ts";
|
||||
|
||||
const logger = baseLogger.getChild("DeviceListener:");
|
||||
|
||||
/**
|
||||
* The state of the device and the user's account.
|
||||
*/
|
||||
export type DeviceState =
|
||||
/**
|
||||
* The device is in a good state.
|
||||
*/
|
||||
| "ok"
|
||||
/**
|
||||
* The user needs to set up recovery.
|
||||
*/
|
||||
| "set_up_recovery"
|
||||
/**
|
||||
* The device is not verified.
|
||||
*/
|
||||
| "verify_this_session"
|
||||
/**
|
||||
* Key storage is out of sync (keys are missing locally, from recovery, or both).
|
||||
*/
|
||||
| "key_storage_out_of_sync"
|
||||
/**
|
||||
* Key storage is not enabled, and has not been marked as purposely disabled.
|
||||
*/
|
||||
| "turn_on_key_storage"
|
||||
/**
|
||||
* The user's identity needs resetting, due to missing keys.
|
||||
*/
|
||||
| "identity_needs_reset";
|
||||
|
||||
/**
|
||||
* The events emitted by {@link DeviceListener}
|
||||
*/
|
||||
@@ -98,21 +40,22 @@ type EventHandlerMap = {
|
||||
export default class DeviceListener extends TypedEventEmitter<DeviceListenerEvents, EventHandlerMap> {
|
||||
private dispatcherRef?: string;
|
||||
|
||||
/** All the information about whether other devices are verified. */
|
||||
/**
|
||||
* All the information about whether other devices are verified. Only set
|
||||
* if `running` is true, otherwise undefined.
|
||||
*/
|
||||
public otherDevices?: DeviceListenerOtherDevices;
|
||||
|
||||
// has the user dismissed any of the various nag toasts to setup encryption on this device?
|
||||
private dismissedThisDeviceToast = false;
|
||||
/** Cache of the info about the current key backup on the server. */
|
||||
private keyBackupInfo: KeyBackupInfo | null = null;
|
||||
/** When `keyBackupInfo` was last updated */
|
||||
private keyBackupFetchedAt: number | null = null;
|
||||
/** All the information about whether this device's encrypytion is OK. Only
|
||||
* set if `running` is true, otherwise undefined.
|
||||
*/
|
||||
public currentDevice?: DeviceListenerCurrentDevice;
|
||||
|
||||
private running = false;
|
||||
// The client with which the instance is running. Only set if `running` is true, otherwise undefined.
|
||||
private client?: MatrixClient;
|
||||
private shouldRecordClientInformation = false;
|
||||
private deviceClientInformationSettingWatcherRef: string | undefined;
|
||||
private deviceState: DeviceState = "ok";
|
||||
|
||||
// Remember the current analytics state to avoid sending the same event multiple times.
|
||||
private analyticsVerificationState?: string;
|
||||
@@ -127,15 +70,9 @@ export default class DeviceListener extends TypedEventEmitter<DeviceListenerEven
|
||||
this.running = true;
|
||||
|
||||
this.otherDevices = new DeviceListenerOtherDevices(this, matrixClient);
|
||||
this.currentDevice = new DeviceListenerCurrentDevice(this, matrixClient, logger);
|
||||
|
||||
this.client = matrixClient;
|
||||
this.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
||||
this.client.on(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged);
|
||||
this.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChanged);
|
||||
this.client.on(ClientEvent.AccountData, this.onAccountData);
|
||||
this.client.on(ClientEvent.Sync, this.onSync);
|
||||
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
|
||||
this.shouldRecordClientInformation = SettingsStore.getValue("deviceClientInformationOptIn");
|
||||
// only configurable in config, so we don't need to watch the value
|
||||
@@ -151,22 +88,14 @@ export default class DeviceListener extends TypedEventEmitter<DeviceListenerEven
|
||||
|
||||
public stop(): void {
|
||||
this.running = false;
|
||||
if (this.client) {
|
||||
this.client.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
||||
this.client.removeListener(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged);
|
||||
this.client.removeListener(ClientEvent.AccountData, this.onAccountData);
|
||||
this.client.removeListener(ClientEvent.Sync, this.onSync);
|
||||
this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.client.removeListener(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
}
|
||||
|
||||
SettingsStore.unwatchSetting(this.deviceClientInformationSettingWatcherRef);
|
||||
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
||||
this.dispatcherRef = undefined;
|
||||
this.otherDevices?.stop();
|
||||
this.dismissedThisDeviceToast = false;
|
||||
this.keyBackupInfo = null;
|
||||
this.keyBackupFetchedAt = null;
|
||||
this.cachedKeyBackupUploadActive = undefined;
|
||||
this.currentDevice?.stop();
|
||||
this.client = undefined;
|
||||
}
|
||||
|
||||
@@ -201,22 +130,21 @@ export default class DeviceListener extends TypedEventEmitter<DeviceListenerEven
|
||||
}
|
||||
|
||||
public dismissEncryptionSetup(): void {
|
||||
this.dismissedThisDeviceToast = true;
|
||||
this.recheck();
|
||||
this.currentDevice?.dismissEncryptionSetup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the account data "m.org.matrix.custom.backup_disabled" to { "disabled": true }.
|
||||
*/
|
||||
public async recordKeyBackupDisabled(): Promise<void> {
|
||||
await this.client?.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true });
|
||||
await this.currentDevice?.recordKeyBackupDisabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the account data to indicate that recovery is disabled
|
||||
*/
|
||||
public async recordRecoveryDisabled(): Promise<void> {
|
||||
await this.client?.setAccountData(RECOVERY_ACCOUNT_DATA_KEY, { enabled: false });
|
||||
await this.currentDevice?.recordRecoveryDisabled();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,10 +193,11 @@ export default class DeviceListener extends TypedEventEmitter<DeviceListenerEven
|
||||
*/
|
||||
public async keyStorageOutOfSyncNeedsBackupReset(forgotRecovery: boolean): Promise<boolean> {
|
||||
const crypto = this.client?.getCrypto();
|
||||
if (!crypto) {
|
||||
const thisDevice = this.currentDevice;
|
||||
if (!(crypto && thisDevice)) {
|
||||
return false;
|
||||
}
|
||||
const shouldHaveBackup = !(await this.recheckBackupDisabled(this.client!));
|
||||
const shouldHaveBackup = !(await thisDevice.recheckBackupDisabled());
|
||||
const backupKeyCached = (await crypto.getSessionBackupPrivateKey()) !== null;
|
||||
const backupKeyStored = await this.client!.isKeyBackupKeyStored();
|
||||
|
||||
@@ -279,101 +208,12 @@ export default class DeviceListener extends TypedEventEmitter<DeviceListenerEven
|
||||
}
|
||||
}
|
||||
|
||||
private onUserTrustStatusChanged = (userId: string): void => {
|
||||
if (!this.client) return;
|
||||
if (userId !== this.client.getUserId()) return;
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onKeyBackupStatusChanged = (): void => {
|
||||
logger.info("Backup status changed");
|
||||
this.cachedKeyBackupUploadActive = undefined;
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onCrossSingingKeysChanged = (): void => {
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onAccountData = (ev: MatrixEvent): void => {
|
||||
// User may have:
|
||||
// * migrated SSSS to symmetric
|
||||
// * uploaded keys to secret storage
|
||||
// * completed secret storage creation
|
||||
// * disabled key backup
|
||||
// which result in account data changes affecting checks below.
|
||||
if (
|
||||
ev.getType().startsWith("m.secret_storage.") ||
|
||||
ev.getType().startsWith("m.cross_signing.") ||
|
||||
ev.getType() === "m.megolm_backup.v1" ||
|
||||
ev.getType() === BACKUP_DISABLED_ACCOUNT_DATA_KEY ||
|
||||
ev.getType() === RECOVERY_ACCOUNT_DATA_KEY
|
||||
) {
|
||||
this.recheck();
|
||||
}
|
||||
};
|
||||
|
||||
private onSync = (state: SyncState, prevState: SyncState | null): void => {
|
||||
if (state === "PREPARED" && prevState === null) {
|
||||
this.recheck();
|
||||
}
|
||||
};
|
||||
|
||||
private onRoomStateEvents = (ev: MatrixEvent): void => {
|
||||
if (ev.getType() !== EventType.RoomEncryption) return;
|
||||
|
||||
// If a room changes to encrypted, re-check as it may be our first
|
||||
// encrypted room. This also catches encrypted room creation as well.
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onAction = ({ action }: ActionPayload): void => {
|
||||
if (action !== Action.OnLoggedIn) return;
|
||||
this.recheck();
|
||||
this.updateClientInformation();
|
||||
};
|
||||
|
||||
private onToDeviceEvent = (event: MatrixEvent): void => {
|
||||
// Receiving a 4S secret can mean we are in sync where we were not before.
|
||||
if (event.getType() === EventType.SecretSend) this.recheck();
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the key backup information from the server.
|
||||
*
|
||||
* The result is cached for `KEY_BACKUP_POLL_INTERVAL` ms to avoid repeated API calls.
|
||||
*
|
||||
* @returns The key backup info from the server, or `null` if there is no key backup.
|
||||
*/
|
||||
private async getKeyBackupInfo(): Promise<KeyBackupInfo | null> {
|
||||
if (!this.client) return null;
|
||||
const now = new Date().getTime();
|
||||
const crypto = this.client.getCrypto();
|
||||
if (!crypto) return null;
|
||||
|
||||
if (
|
||||
!this.keyBackupInfo ||
|
||||
!this.keyBackupFetchedAt ||
|
||||
this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL
|
||||
) {
|
||||
this.keyBackupInfo = await crypto.getKeyBackupInfo();
|
||||
this.keyBackupFetchedAt = now;
|
||||
}
|
||||
return this.keyBackupInfo;
|
||||
}
|
||||
|
||||
private async shouldShowSetupEncryptionToast(): Promise<boolean> {
|
||||
// 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;
|
||||
// Show setup toasts once the user is in at least one encrypted room.
|
||||
const cli = this.client;
|
||||
const cryptoApi = cli?.getCrypto();
|
||||
if (!cli || !cryptoApi) return false;
|
||||
|
||||
return await asyncSomeParallel(cli.getRooms(), ({ roomId }) => cryptoApi.isEncryptionEnabledInRoom(roomId));
|
||||
}
|
||||
|
||||
public recheck(): void {
|
||||
this.doRecheck().catch((e) => {
|
||||
if (e instanceof ClientStoppedError) {
|
||||
@@ -405,127 +245,10 @@ export default class DeviceListener extends TypedEventEmitter<DeviceListenerEven
|
||||
return;
|
||||
}
|
||||
|
||||
const crossSigningReady = await crypto.isCrossSigningReady();
|
||||
const secretStorageStatus = await crypto.getSecretStorageStatus();
|
||||
const crossSigningStatus = await crypto.getCrossSigningStatus();
|
||||
const allCrossSigningSecretsCached =
|
||||
crossSigningStatus.privateKeysCachedLocally.masterKey &&
|
||||
crossSigningStatus.privateKeysCachedLocally.selfSigningKey &&
|
||||
crossSigningStatus.privateKeysCachedLocally.userSigningKey;
|
||||
|
||||
const recoveryDisabled = await this.recheckRecoveryDisabled(cli);
|
||||
|
||||
const recoveryIsOk = secretStorageStatus.ready || recoveryDisabled;
|
||||
|
||||
const isCurrentDeviceTrusted = Boolean(
|
||||
(await crypto.getDeviceVerificationStatus(cli.getSafeUserId(), cli.deviceId!))?.crossSigningVerified,
|
||||
);
|
||||
|
||||
const keyBackupUploadActive = await this.isKeyBackupUploadActive(logSpan);
|
||||
const backupDisabled = await this.recheckBackupDisabled(cli);
|
||||
|
||||
// We warn if key backup upload is turned off and we have not explicitly
|
||||
// said we are OK with that.
|
||||
const keyBackupUploadIsOk = keyBackupUploadActive || backupDisabled;
|
||||
|
||||
// We warn if key backup is set up, but we don't have the decryption
|
||||
// key, so can't fetch keys from backup.
|
||||
const keyBackupDownloadIsOk =
|
||||
!keyBackupUploadActive || backupDisabled || (await crypto.getSessionBackupPrivateKey()) !== null;
|
||||
|
||||
const allSystemsReady =
|
||||
isCurrentDeviceTrusted &&
|
||||
allCrossSigningSecretsCached &&
|
||||
keyBackupUploadIsOk &&
|
||||
recoveryIsOk &&
|
||||
keyBackupDownloadIsOk;
|
||||
await this.currentDevice?.recheck(logSpan);
|
||||
await this.otherDevices?.recheck(logSpan);
|
||||
|
||||
await this.reportCryptoSessionStateToAnalytics(cli);
|
||||
|
||||
if (allSystemsReady) {
|
||||
logSpan.info("No toast needed");
|
||||
await this.setDeviceState("ok", logSpan);
|
||||
} else {
|
||||
// make sure our keys are finished downloading
|
||||
await crypto.getUserDeviceInfo([cli.getSafeUserId()]);
|
||||
|
||||
if (!isCurrentDeviceTrusted) {
|
||||
// the current device is not trusted: prompt the user to verify
|
||||
logSpan.info("Current device not verified: setting state to VERIFY_THIS_SESSION");
|
||||
await this.setDeviceState("verify_this_session", logSpan);
|
||||
} else if (!allCrossSigningSecretsCached) {
|
||||
// cross signing ready & device trusted, but we are missing secrets from our local cache.
|
||||
// prompt the user to enter their recovery key.
|
||||
logSpan.info(
|
||||
"Some secrets not cached: setting state to KEY_STORAGE_OUT_OF_SYNC",
|
||||
crossSigningStatus.privateKeysCachedLocally,
|
||||
crossSigningStatus.privateKeysInSecretStorage,
|
||||
);
|
||||
await this.setDeviceState(
|
||||
crossSigningStatus.privateKeysInSecretStorage ? "key_storage_out_of_sync" : "identity_needs_reset",
|
||||
logSpan,
|
||||
);
|
||||
} else if (!keyBackupUploadIsOk) {
|
||||
logSpan.info("Key backup upload is unexpectedly turned off: setting state to TURN_ON_KEY_STORAGE");
|
||||
await this.setDeviceState("turn_on_key_storage", logSpan);
|
||||
} else if (secretStorageStatus.defaultKeyId === null) {
|
||||
// The user just hasn't set up 4S yet: if they have key
|
||||
// backup, prompt them to turn on recovery too. (If not, they
|
||||
// have explicitly opted out, so don't hassle them.)
|
||||
if (recoveryDisabled) {
|
||||
logSpan.info("Recovery disabled: no toast needed");
|
||||
await this.setDeviceState("ok", logSpan);
|
||||
} else if (keyBackupUploadActive) {
|
||||
logSpan.info("No default 4S key: setting state to SET_UP_RECOVERY");
|
||||
await this.setDeviceState("set_up_recovery", logSpan);
|
||||
} else {
|
||||
logSpan.info("No default 4S key but backup disabled: no toast needed");
|
||||
await this.setDeviceState("ok", logSpan);
|
||||
}
|
||||
} else {
|
||||
// If we get here, then we are verified, have key backup, and
|
||||
// 4S, but allSystemsReady is false, which means that either
|
||||
// secretStorageStatus.ready is false (which means that 4S
|
||||
// doesn't have all the secrets), or we don't have the backup
|
||||
// key cached locally. If any of the cross-signing keys are
|
||||
// missing locally, that is handled by the
|
||||
// `!allCrossSigningSecretsCached` branch above.
|
||||
logSpan.warn("4S is missing secrets or backup key not cached", {
|
||||
crossSigningReady,
|
||||
secretStorageStatus,
|
||||
allCrossSigningSecretsCached,
|
||||
isCurrentDeviceTrusted,
|
||||
keyBackupDownloadIsOk,
|
||||
});
|
||||
await this.setDeviceState("key_storage_out_of_sync", logSpan);
|
||||
}
|
||||
}
|
||||
|
||||
await this.otherDevices?.recheck(logSpan);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the account data for `backup_disabled`. If this is the first time,
|
||||
* fetch it from the server (in case the initial sync has not finished).
|
||||
* Otherwise, fetch it from the store as normal.
|
||||
*/
|
||||
private async recheckBackupDisabled(cli: MatrixClient): Promise<boolean> {
|
||||
const backupDisabled = await cli.getAccountDataFromServer(BACKUP_DISABLED_ACCOUNT_DATA_KEY);
|
||||
return !!backupDisabled?.disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the user has disabled recovery. If this is the first time,
|
||||
* fetch it from the server (in case the initial sync has not finished).
|
||||
* Otherwise, fetch it from the store as normal.
|
||||
*/
|
||||
private async recheckRecoveryDisabled(cli: MatrixClient): Promise<boolean> {
|
||||
const recoveryStatus = await cli.getAccountDataFromServer(RECOVERY_ACCOUNT_DATA_KEY);
|
||||
// Recovery is disabled only if the `enabled` flag is set to `false`.
|
||||
// If it is missing, or set to any other value, we consider it as
|
||||
// not-disabled, and will prompt the user to create recovery (if
|
||||
// missing).
|
||||
return recoveryStatus?.enabled === false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -534,23 +257,7 @@ export default class DeviceListener extends TypedEventEmitter<DeviceListenerEven
|
||||
* self-verified device that is using key backup and recovery.
|
||||
*/
|
||||
public getDeviceState(): DeviceState {
|
||||
return this.deviceState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state of the device, and perform any actions necessary in
|
||||
* response to the state changing.
|
||||
*/
|
||||
private async setDeviceState(newState: DeviceState, logSpan: LogSpan): Promise<void> {
|
||||
this.deviceState = newState;
|
||||
this.emit(DeviceListenerEvents.DeviceState, newState);
|
||||
if (newState === "ok" || this.dismissedThisDeviceToast) {
|
||||
hideSetupEncryptionToast();
|
||||
} else if (await this.shouldShowSetupEncryptionToast()) {
|
||||
showSetupEncryptionToast(newState);
|
||||
} else {
|
||||
logSpan.info("Not yet ready, but shouldShowSetupEncryptionToast==false");
|
||||
}
|
||||
return this.currentDevice?.getDeviceState() ?? "ok";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -564,7 +271,7 @@ export default class DeviceListener extends TypedEventEmitter<DeviceListenerEven
|
||||
const secretStorageStatus = await crypto.getSecretStorageStatus();
|
||||
const secretStorageReady = secretStorageStatus.ready;
|
||||
const crossSigningStatus = await crypto.getCrossSigningStatus();
|
||||
const backupInfo = await this.getKeyBackupInfo();
|
||||
const backupInfo = await this.currentDevice?.getKeyBackupInfo();
|
||||
const is4SEnabled = secretStorageStatus.defaultKeyId != null;
|
||||
const deviceVerificationStatus = await crypto.getDeviceVerificationStatus(cli.getUserId()!, cli.getDeviceId()!);
|
||||
|
||||
@@ -618,39 +325,6 @@ export default class DeviceListener extends TypedEventEmitter<DeviceListenerEven
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Is key backup enabled? Use a cached answer if we have one.
|
||||
*/
|
||||
private isKeyBackupUploadActive = async (logger: BaseLogger): Promise<boolean> => {
|
||||
if (!this.client) {
|
||||
// To preserve existing behaviour, if there is no client, we
|
||||
// pretend key backup upload is on.
|
||||
//
|
||||
// Someone looking to improve this code could try throwing an error
|
||||
// here since we don't expect client to be undefined.
|
||||
return true;
|
||||
}
|
||||
|
||||
const crypto = this.client.getCrypto();
|
||||
if (!crypto) {
|
||||
// If there is no crypto, there is no key backup
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we've already cached the answer, return it.
|
||||
if (this.cachedKeyBackupUploadActive !== undefined) {
|
||||
return this.cachedKeyBackupUploadActive;
|
||||
}
|
||||
|
||||
// Fetch the answer and cache it
|
||||
const activeKeyBackupVersion = await crypto.getActiveSessionBackupVersion();
|
||||
this.cachedKeyBackupUploadActive = !!activeKeyBackupVersion;
|
||||
logger.debug(`Key backup upload is ${this.cachedKeyBackupUploadActive ? "active" : "inactive"}`);
|
||||
|
||||
return this.cachedKeyBackupUploadActive;
|
||||
};
|
||||
private cachedKeyBackupUploadActive: boolean | undefined = undefined;
|
||||
|
||||
private onRecordClientInformationSettingChange: CallbackFn = (
|
||||
_originalSettingName,
|
||||
_roomId,
|
||||
|
||||
@@ -10,9 +10,10 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
import DeviceListener, { BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../../../DeviceListener";
|
||||
import DeviceListener from "../../../../DeviceListener";
|
||||
import { useEventEmitterAsyncState } from "../../../../hooks/useEventEmitter";
|
||||
import { resetKeyBackupAndWait } from "../../../../utils/crypto/resetKeyBackup";
|
||||
import { BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../../../device-listener/DeviceListenerCurrentDevice";
|
||||
|
||||
interface KeyStoragePanelState {
|
||||
/**
|
||||
|
||||
@@ -30,8 +30,9 @@ import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydra
|
||||
import { withSecretStorageKeyCache } from "../../../../SecurityManager";
|
||||
import { EncryptionCardButtons } from "./EncryptionCardButtons";
|
||||
import { logErrorAndShowErrorDialog } from "../../../../utils/ErrorUtils.tsx";
|
||||
import DeviceListener, { RECOVERY_ACCOUNT_DATA_KEY } from "../../../../DeviceListener";
|
||||
import DeviceListener from "../../../../DeviceListener";
|
||||
import { resetKeyBackupAndWait } from "../../../../utils/crypto/resetKeyBackup";
|
||||
import { RECOVERY_ACCOUNT_DATA_KEY } from "../../../../device-listener/DeviceListenerCurrentDevice.ts";
|
||||
|
||||
/**
|
||||
* The possible states of the component.
|
||||
|
||||
@@ -24,8 +24,9 @@ import { RecoveryPanelOutOfSync } from "../../encryption/RecoveryPanelOutOfSync"
|
||||
import { useTypedEventEmitterState } from "../../../../../hooks/useEventEmitter";
|
||||
import { KeyStoragePanel } from "../../encryption/KeyStoragePanel";
|
||||
import { DeleteKeyStoragePanel } from "../../encryption/DeleteKeyStoragePanel";
|
||||
import DeviceListener, { DeviceListenerEvents, type DeviceState } from "../../../../../DeviceListener";
|
||||
import DeviceListener, { DeviceListenerEvents } from "../../../../../DeviceListener";
|
||||
import { useKeyStoragePanelViewModel } from "../../../../viewmodels/settings/encryption/KeyStoragePanelViewModel";
|
||||
import type DeviceState from "../../../../../device-listener/DeviceState";
|
||||
|
||||
/**
|
||||
* The state in the encryption settings tab.
|
||||
|
||||
420
src/device-listener/DeviceListenerCurrentDevice.ts
Normal file
420
src/device-listener/DeviceListenerCurrentDevice.ts
Normal file
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
Copyright 2025-2026 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { type LogSpan, type BaseLogger, type Logger } from "matrix-js-sdk/src/logger";
|
||||
import {
|
||||
type MatrixEvent,
|
||||
type MatrixClient,
|
||||
EventType,
|
||||
type SyncState,
|
||||
RoomStateEvent,
|
||||
ClientEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type DeviceListener from "../DeviceListener";
|
||||
import type DeviceState from "./DeviceState";
|
||||
import { DeviceListenerEvents } from "../DeviceListener";
|
||||
import {
|
||||
hideToast as hideSetupEncryptionToast,
|
||||
showToast as showSetupEncryptionToast,
|
||||
} from "../toasts/SetupEncryptionToast";
|
||||
import { isSecretStorageBeingAccessed } from "../SecurityManager";
|
||||
import { asyncSomeParallel } from "../utils/arrays";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Unfortunately-named account data key used by Element X to indicate that the user
|
||||
* has chosen to disable server side key backups.
|
||||
*
|
||||
* We need to set and honour this to prevent Element X from automatically turning key backup back on.
|
||||
*/
|
||||
export const BACKUP_DISABLED_ACCOUNT_DATA_KEY = "m.org.matrix.custom.backup_disabled";
|
||||
|
||||
/**
|
||||
* Account data key to indicate whether the user has chosen to enable or disable recovery.
|
||||
*/
|
||||
export const RECOVERY_ACCOUNT_DATA_KEY = "io.element.recovery";
|
||||
|
||||
/**
|
||||
* Handles all of DeviceListener's work that relates to the current device.
|
||||
*/
|
||||
export default class DeviceListenerCurrentDevice {
|
||||
/**
|
||||
* The DeviceListener launching this instance.
|
||||
*/
|
||||
private deviceListener: DeviceListener;
|
||||
|
||||
/**
|
||||
* The Matrix client in use by the current user.
|
||||
*/
|
||||
private client: MatrixClient;
|
||||
|
||||
/**
|
||||
* A Logger we use to write our debug information.
|
||||
*/
|
||||
private logger: Logger;
|
||||
|
||||
/**
|
||||
* Has the user dismissed any of the various nag toasts to setup encryption
|
||||
* on this device?
|
||||
*/
|
||||
private dismissedThisDeviceToast = false;
|
||||
|
||||
/**
|
||||
* Cache of the info about the current key backup on the server.
|
||||
*/
|
||||
private keyBackupInfo: KeyBackupInfo | null = null;
|
||||
|
||||
/**
|
||||
* When `keyBackupInfo` was last updated (in ms since the epoch).
|
||||
*/
|
||||
private keyBackupFetchedAt: number | null = null;
|
||||
|
||||
/**
|
||||
* What is the current state of the device: is its crypto OK?
|
||||
*/
|
||||
private deviceState: DeviceState = "ok";
|
||||
|
||||
/**
|
||||
* Was key backup upload active last time we checked?
|
||||
*/
|
||||
private cachedKeyBackupUploadActive: boolean | undefined = undefined;
|
||||
|
||||
public constructor(deviceListener: DeviceListener, client: MatrixClient, logger: Logger) {
|
||||
this.deviceListener = deviceListener;
|
||||
this.client = client;
|
||||
this.logger = logger;
|
||||
|
||||
this.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
||||
this.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
|
||||
this.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChanged);
|
||||
this.client.on(ClientEvent.AccountData, this.onAccountData);
|
||||
this.client.on(ClientEvent.Sync, this.onSync);
|
||||
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop listening for events and clear the stored information.
|
||||
*/
|
||||
public stop(): void {
|
||||
this.dismissedThisDeviceToast = false;
|
||||
this.keyBackupInfo = null;
|
||||
this.keyBackupFetchedAt = null;
|
||||
this.cachedKeyBackupUploadActive = undefined;
|
||||
|
||||
this.client.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
||||
this.client.removeListener(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
|
||||
this.client.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChanged);
|
||||
this.client.removeListener(ClientEvent.AccountData, this.onAccountData);
|
||||
this.client.removeListener(ClientEvent.Sync, this.onSync);
|
||||
this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.client.removeListener(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user dismissed the Key Storage out of Sync toast, so we won't nag
|
||||
* them again until they refresh or restart the app.
|
||||
*/
|
||||
public dismissEncryptionSetup(): void {
|
||||
this.dismissedThisDeviceToast = true;
|
||||
this.deviceListener.recheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the account data "m.org.matrix.custom.backup_disabled" to `{ "disabled": true }`.
|
||||
*/
|
||||
public async recordKeyBackupDisabled(): Promise<void> {
|
||||
await this.client.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the account data to indicate that recovery is disabled
|
||||
*/
|
||||
public async recordRecoveryDisabled(): Promise<void> {
|
||||
await this.client.setAccountData(RECOVERY_ACCOUNT_DATA_KEY, { enabled: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a toast if our crypto is in an unexpected state, or if we want to
|
||||
* nag the user about setting up more stuff.
|
||||
*/
|
||||
public async recheck(logSpan: LogSpan): Promise<void> {
|
||||
const crypto = this.client.getCrypto();
|
||||
if (!crypto) {
|
||||
return;
|
||||
}
|
||||
|
||||
const crossSigningReady = await crypto.isCrossSigningReady();
|
||||
const secretStorageStatus = await crypto.getSecretStorageStatus();
|
||||
const crossSigningStatus = await crypto.getCrossSigningStatus();
|
||||
const allCrossSigningSecretsCached =
|
||||
crossSigningStatus.privateKeysCachedLocally.masterKey &&
|
||||
crossSigningStatus.privateKeysCachedLocally.selfSigningKey &&
|
||||
crossSigningStatus.privateKeysCachedLocally.userSigningKey;
|
||||
|
||||
const recoveryDisabled = await this.recheckRecoveryDisabled(this.client);
|
||||
|
||||
const recoveryIsOk = secretStorageStatus.ready || recoveryDisabled;
|
||||
|
||||
const isCurrentDeviceTrusted = Boolean(
|
||||
(await crypto.getDeviceVerificationStatus(this.client.getSafeUserId(), this.client.deviceId!))
|
||||
?.crossSigningVerified,
|
||||
);
|
||||
|
||||
const keyBackupUploadActive = await this.isKeyBackupUploadActive(logSpan);
|
||||
const backupDisabled = await this.recheckBackupDisabled();
|
||||
|
||||
// We warn if key backup upload is turned off and we have not explicitly
|
||||
// said we are OK with that.
|
||||
const keyBackupUploadIsOk = keyBackupUploadActive || backupDisabled;
|
||||
|
||||
// We warn if key backup is set up, but we don't have the decryption
|
||||
// key, so can't fetch keys from backup.
|
||||
const keyBackupDownloadIsOk =
|
||||
!keyBackupUploadActive || backupDisabled || (await crypto.getSessionBackupPrivateKey()) !== null;
|
||||
|
||||
const allSystemsReady =
|
||||
isCurrentDeviceTrusted &&
|
||||
allCrossSigningSecretsCached &&
|
||||
keyBackupUploadIsOk &&
|
||||
recoveryIsOk &&
|
||||
keyBackupDownloadIsOk;
|
||||
|
||||
if (allSystemsReady) {
|
||||
logSpan.info("No toast needed");
|
||||
await this.setDeviceState("ok", logSpan);
|
||||
} else {
|
||||
// make sure our keys are finished downloading
|
||||
await crypto.getUserDeviceInfo([this.client.getSafeUserId()]);
|
||||
|
||||
if (!isCurrentDeviceTrusted) {
|
||||
// the current device is not trusted: prompt the user to verify
|
||||
logSpan.info("Current device not verified: setting state to VERIFY_THIS_SESSION");
|
||||
await this.setDeviceState("verify_this_session", logSpan);
|
||||
} else if (!allCrossSigningSecretsCached) {
|
||||
// cross signing ready & device trusted, but we are missing secrets from our local cache.
|
||||
// prompt the user to enter their recovery key.
|
||||
logSpan.info(
|
||||
"Some secrets not cached: setting state to KEY_STORAGE_OUT_OF_SYNC",
|
||||
crossSigningStatus.privateKeysCachedLocally,
|
||||
crossSigningStatus.privateKeysInSecretStorage,
|
||||
);
|
||||
await this.setDeviceState(
|
||||
crossSigningStatus.privateKeysInSecretStorage ? "key_storage_out_of_sync" : "identity_needs_reset",
|
||||
logSpan,
|
||||
);
|
||||
} else if (!keyBackupUploadIsOk) {
|
||||
logSpan.info("Key backup upload is unexpectedly turned off: setting state to TURN_ON_KEY_STORAGE");
|
||||
await this.setDeviceState("turn_on_key_storage", logSpan);
|
||||
} else if (secretStorageStatus.defaultKeyId === null) {
|
||||
// The user just hasn't set up 4S yet: if they have key
|
||||
// backup, prompt them to turn on recovery too. (If not, they
|
||||
// have explicitly opted out, so don't hassle them.)
|
||||
if (recoveryDisabled) {
|
||||
logSpan.info("Recovery disabled: no toast needed");
|
||||
await this.setDeviceState("ok", logSpan);
|
||||
} else if (keyBackupUploadActive) {
|
||||
logSpan.info("No default 4S key: setting state to SET_UP_RECOVERY");
|
||||
await this.setDeviceState("set_up_recovery", logSpan);
|
||||
} else {
|
||||
logSpan.info("No default 4S key but backup disabled: no toast needed");
|
||||
await this.setDeviceState("ok", logSpan);
|
||||
}
|
||||
} else {
|
||||
// If we get here, then we are verified, have key backup, and
|
||||
// 4S, but allSystemsReady is false, which means that either
|
||||
// secretStorageStatus.ready is false (which means that 4S
|
||||
// doesn't have all the secrets), or we don't have the backup
|
||||
// key cached locally. If any of the cross-signing keys are
|
||||
// missing locally, that is handled by the
|
||||
// `!allCrossSigningSecretsCached` branch above.
|
||||
logSpan.warn("4S is missing secrets or backup key not cached", {
|
||||
crossSigningReady,
|
||||
secretStorageStatus,
|
||||
allCrossSigningSecretsCached,
|
||||
isCurrentDeviceTrusted,
|
||||
keyBackupDownloadIsOk,
|
||||
});
|
||||
await this.setDeviceState("key_storage_out_of_sync", logSpan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state of the device and the user's account. The device/account
|
||||
* state indicates what action the user must take in order to get a
|
||||
* self-verified device that is using key backup and recovery.
|
||||
*/
|
||||
public getDeviceState(): DeviceState {
|
||||
return this.deviceState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state of the device, and perform any actions necessary in
|
||||
* response to the state changing.
|
||||
*/
|
||||
private async setDeviceState(newState: DeviceState, logSpan: LogSpan): Promise<void> {
|
||||
this.deviceState = newState;
|
||||
|
||||
this.deviceListener.emit(DeviceListenerEvents.DeviceState, newState);
|
||||
|
||||
if (newState === "ok" || this.dismissedThisDeviceToast) {
|
||||
hideSetupEncryptionToast();
|
||||
} else if (await this.shouldShowSetupEncryptionToast()) {
|
||||
showSetupEncryptionToast(newState);
|
||||
} else {
|
||||
logSpan.info("Not yet ready, but shouldShowSetupEncryptionToast==false");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the account data for `backup_disabled`. If this is the first time,
|
||||
* fetch it from the server (in case the initial sync has not finished).
|
||||
* Otherwise, fetch it from the store as normal.
|
||||
*/
|
||||
public async recheckBackupDisabled(): Promise<boolean> {
|
||||
const backupDisabled = await this.client.getAccountDataFromServer(BACKUP_DISABLED_ACCOUNT_DATA_KEY);
|
||||
return !!backupDisabled?.disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the user has disabled recovery. If this is the first time,
|
||||
* fetch it from the server (in case the initial sync has not finished).
|
||||
* Otherwise, fetch it from the store as normal.
|
||||
*/
|
||||
private async recheckRecoveryDisabled(cli: MatrixClient): Promise<boolean> {
|
||||
const recoveryStatus = await cli.getAccountDataFromServer(RECOVERY_ACCOUNT_DATA_KEY);
|
||||
// Recovery is disabled only if the `enabled` flag is set to `false`.
|
||||
// If it is missing, or set to any other value, we consider it as
|
||||
// not-disabled, and will prompt the user to create recovery (if
|
||||
// missing).
|
||||
return recoveryStatus?.enabled === false;
|
||||
}
|
||||
|
||||
private onUserTrustStatusChanged = (userId: string): void => {
|
||||
if (userId !== this.client.getUserId()) return;
|
||||
this.deviceListener.recheck();
|
||||
};
|
||||
|
||||
private onKeyBackupStatusChanged = (): void => {
|
||||
this.logger.info("Backup status changed");
|
||||
this.cachedKeyBackupUploadActive = undefined;
|
||||
this.deviceListener.recheck();
|
||||
};
|
||||
|
||||
private onCrossSigningKeysChanged = (): void => {
|
||||
this.deviceListener.recheck();
|
||||
};
|
||||
|
||||
private onAccountData = (ev: MatrixEvent): void => {
|
||||
// User may have:
|
||||
// * migrated SSSS to symmetric
|
||||
// * uploaded keys to secret storage
|
||||
// * completed secret storage creation
|
||||
// * disabled key backup
|
||||
// which result in account data changes affecting checks below.
|
||||
if (
|
||||
ev.getType().startsWith("m.secret_storage.") ||
|
||||
ev.getType().startsWith("m.cross_signing.") ||
|
||||
ev.getType() === "m.megolm_backup.v1" ||
|
||||
ev.getType() === BACKUP_DISABLED_ACCOUNT_DATA_KEY ||
|
||||
ev.getType() === RECOVERY_ACCOUNT_DATA_KEY
|
||||
) {
|
||||
this.deviceListener.recheck();
|
||||
}
|
||||
};
|
||||
|
||||
private onSync = (state: SyncState, prevState: SyncState | null): void => {
|
||||
if (state === "PREPARED" && prevState === null) {
|
||||
this.deviceListener.recheck();
|
||||
}
|
||||
};
|
||||
|
||||
private onRoomStateEvents = (ev: MatrixEvent): void => {
|
||||
if (ev.getType() !== EventType.RoomEncryption) return;
|
||||
|
||||
// If a room changes to encrypted, re-check as it may be our first
|
||||
// encrypted room. This also catches encrypted room creation as well.
|
||||
this.deviceListener.recheck();
|
||||
};
|
||||
|
||||
private onToDeviceEvent = (event: MatrixEvent): void => {
|
||||
// Receiving a 4S secret can mean we are in sync where we were not before.
|
||||
if (event.getType() === EventType.SecretSend) {
|
||||
this.deviceListener.recheck();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the key backup information from the server.
|
||||
*
|
||||
* The result is cached for `KEY_BACKUP_POLL_INTERVAL` ms to avoid repeated API calls.
|
||||
*
|
||||
* @returns The key backup info from the server, or `null` if there is no key backup.
|
||||
*/
|
||||
public async getKeyBackupInfo(): Promise<KeyBackupInfo | null> {
|
||||
const now = new Date().getTime();
|
||||
const crypto = this.client.getCrypto();
|
||||
if (!crypto) return null;
|
||||
|
||||
if (
|
||||
!this.keyBackupInfo ||
|
||||
!this.keyBackupFetchedAt ||
|
||||
this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL
|
||||
) {
|
||||
this.keyBackupInfo = await crypto.getKeyBackupInfo();
|
||||
this.keyBackupFetchedAt = now;
|
||||
}
|
||||
|
||||
return this.keyBackupInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the user in at least one encrypted room?
|
||||
*/
|
||||
private async shouldShowSetupEncryptionToast(): Promise<boolean> {
|
||||
// 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;
|
||||
|
||||
// Show setup toasts once the user is in at least one encrypted room.
|
||||
const cryptoApi = this.client.getCrypto();
|
||||
if (!cryptoApi) return false;
|
||||
|
||||
return await asyncSomeParallel(this.client.getRooms(), ({ roomId }) =>
|
||||
cryptoApi.isEncryptionEnabledInRoom(roomId),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is key backup enabled? Use a cached answer if we have one.
|
||||
*/
|
||||
private isKeyBackupUploadActive = async (logger: BaseLogger): Promise<boolean> => {
|
||||
const crypto = this.client.getCrypto();
|
||||
if (!crypto) {
|
||||
// If there is no crypto, there is no key backup
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we've already cached the answer, return it.
|
||||
if (this.cachedKeyBackupUploadActive !== undefined) {
|
||||
return this.cachedKeyBackupUploadActive;
|
||||
}
|
||||
|
||||
// Fetch the answer and cache it
|
||||
const activeKeyBackupVersion = await crypto.getActiveSessionBackupVersion();
|
||||
this.cachedKeyBackupUploadActive = !!activeKeyBackupVersion;
|
||||
logger.debug(`Key backup upload is ${this.cachedKeyBackupUploadActive ? "active" : "inactive"}`);
|
||||
|
||||
return this.cachedKeyBackupUploadActive;
|
||||
};
|
||||
}
|
||||
39
src/device-listener/DeviceState.ts
Normal file
39
src/device-listener/DeviceState.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright 2025-2026 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The state of the device and the user's account.
|
||||
*/
|
||||
type DeviceState =
|
||||
/**
|
||||
* The device is in a good state.
|
||||
*/
|
||||
| "ok"
|
||||
/**
|
||||
* The user needs to set up recovery.
|
||||
*/
|
||||
| "set_up_recovery"
|
||||
/**
|
||||
* The device is not verified.
|
||||
*/
|
||||
| "verify_this_session"
|
||||
/**
|
||||
* Key storage is out of sync (keys are missing locally, from recovery, or both).
|
||||
*/
|
||||
| "key_storage_out_of_sync"
|
||||
/**
|
||||
* Key storage is not enabled, and has not been marked as purposely disabled.
|
||||
*/
|
||||
| "turn_on_key_storage"
|
||||
/**
|
||||
* The user's identity needs resetting, due to missing keys.
|
||||
*/
|
||||
| "identity_needs_reset";
|
||||
|
||||
export default DeviceState;
|
||||
@@ -14,7 +14,7 @@ import { type Interaction as InteractionEvent } from "@matrix-org/analytics-even
|
||||
|
||||
import Modal from "../Modal";
|
||||
import { _t } from "../languageHandler";
|
||||
import DeviceListener, { type DeviceState } from "../DeviceListener";
|
||||
import DeviceListener from "../DeviceListener";
|
||||
import SetupEncryptionDialog from "../components/views/dialogs/security/SetupEncryptionDialog";
|
||||
import { AccessCancelledError, accessSecretStorage } from "../SecurityManager";
|
||||
import ToastStore, { type IToast } from "../stores/ToastStore";
|
||||
@@ -30,6 +30,7 @@ import ConfirmKeyStorageOffDialog from "../components/views/dialogs/ConfirmKeySt
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { resetKeyBackupAndWait } from "../utils/crypto/resetKeyBackup";
|
||||
import { PosthogAnalytics } from "../PosthogAnalytics";
|
||||
import type DeviceState from "../device-listener/DeviceState";
|
||||
|
||||
const TOAST_KEY = "setupencryption";
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from "matrix-js-sdk/src/crypto-api";
|
||||
import { type CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange";
|
||||
|
||||
import DeviceListener, { BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../src/DeviceListener";
|
||||
import DeviceListener from "../../src/DeviceListener";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import * as SetupEncryptionToast from "../../src/toasts/SetupEncryptionToast";
|
||||
import * as UnverifiedSessionToast from "../../src/toasts/UnverifiedSessionToast";
|
||||
@@ -37,6 +37,7 @@ import { SettingLevel } from "../../src/settings/SettingLevel";
|
||||
import { getMockClientWithEventEmitter, mockPlatformPeg } from "../test-utils";
|
||||
import { isBulkUnverifiedDeviceReminderSnoozed } from "../../src/utils/device/snoozeBulkUnverifiedDeviceReminder";
|
||||
import { PosthogAnalytics } from "../../src/PosthogAnalytics";
|
||||
import { BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../src/device-listener/DeviceListenerCurrentDevice";
|
||||
|
||||
jest.mock("../../src/dispatcher/dispatcher", () => ({
|
||||
dispatch: jest.fn(),
|
||||
|
||||
Reference in New Issue
Block a user