Key storage out of sync: reset key backup when needed (#31279)

* add function to pause device listener

* add function to check if key backup key missing both locally and in 4s

* reset backup if backup key missing both locally and in 4s

* fixup! add function to check if key backup key missing both locally and in 4s

* Drop KEY_STORAGE_OUT_OF_SYNC_STORE in favour of checking cross-signing

Check if cross-signing needs resetting, because that seems to be what
KEY_STORAGE_OUT_OF_SYNC_STORE is actually trying to do.

* add a function for resetting key backup and waiting until it's ready

* trigger key storage out of sync toast when missing backup key locally

and fetch it when user enters their recovery key

* reset backup when needed if user forgets recovery key

* rename function as suggested in code review
This commit is contained in:
Hubert Chathi
2025-11-20 15:25:31 -05:00
committed by GitHub
parent 1285b73be6
commit aee24be1b4
9 changed files with 512 additions and 120 deletions

View File

@@ -12,6 +12,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
import DeviceListener, { BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../../../DeviceListener";
import { useEventEmitterAsyncState } from "../../../../hooks/useEventEmitter";
import { resetKeyBackupAndWait } from "../../../../utils/crypto/resetKeyBackup";
interface KeyStoragePanelState {
/**
@@ -75,63 +76,58 @@ export function useKeyStoragePanelViewModel(): KeyStoragePanelState {
async (enable: boolean) => {
setPendingValue(enable);
try {
// stop the device listener since enabling or (especially) disabling key storage must be
// pause the device listener since enabling or (especially) disabling key storage must be
// done with a sequence of API calls that will put the account in a slightly different
// state each time, so suppress any warning toasts until the process is finished (when
// we'll turn it back on again.)
DeviceListener.sharedInstance().stop();
const crypto = matrixClient.getCrypto();
if (!crypto) {
logger.error("Can't change key backup status: no crypto module available");
return;
}
if (enable) {
const childLogger = logger.getChild("[enable key storage]");
childLogger.info("User requested enabling key storage");
let currentKeyBackup = await crypto.checkKeyBackupAndEnable();
if (currentKeyBackup) {
logger.info(
`Existing key backup is present. version: ${currentKeyBackup.backupInfo.version}`,
currentKeyBackup.trustInfo,
);
// Check if the current key backup can be used. Either of these properties causes the key backup to be used.
if (currentKeyBackup.trustInfo.trusted || currentKeyBackup.trustInfo.matchesDecryptionKey) {
logger.info("Existing key backup can be used");
// state each time, so suppress any warning toasts until the process is finished
await DeviceListener.sharedInstance().whilePaused(async () => {
const crypto = matrixClient.getCrypto();
if (!crypto) {
logger.error("Can't change key backup status: no crypto module available");
return;
}
if (enable) {
const childLogger = logger.getChild("[enable key storage]");
childLogger.info("User requested enabling key storage");
let currentKeyBackup = await crypto.checkKeyBackupAndEnable();
if (currentKeyBackup) {
logger.info(
`Existing key backup is present. version: ${currentKeyBackup.backupInfo.version}`,
currentKeyBackup.trustInfo,
);
// Check if the current key backup can be used. Either of these properties causes the key backup to be used.
if (currentKeyBackup.trustInfo.trusted || currentKeyBackup.trustInfo.matchesDecryptionKey) {
logger.info("Existing key backup can be used");
} else {
logger.warn("Existing key backup cannot be used, creating new backup");
// There aren't any *usable* backups, so we need to create a new one.
currentKeyBackup = null;
}
} else {
logger.warn("Existing key backup cannot be used, creating new backup");
// There aren't any *usable* backups, so we need to create a new one.
currentKeyBackup = null;
logger.info("No existing key backup versions are present, creating new backup");
}
// If there is no usable key backup on the server, create one.
// `resetKeyBackup` will delete any existing backup, so we only do this if there is no usable backup.
if (currentKeyBackup === null) {
await resetKeyBackupAndWait(crypto);
}
// Set the flag so that EX no longer thinks the user wants backup disabled
await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: false });
} else {
logger.info("No existing key backup versions are present, creating new backup");
logger.info("User requested disabling key backup");
// This method will delete the key backup as well as server side recovery keys and other
// server-side crypto data.
await crypto.disableKeyStorage();
// Set a flag to say that the user doesn't want key backup.
// Element X uses this to determine whether to set up automatically,
// so this will stop EX turning it back on spontaneously.
await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true });
}
// If there is no usable key backup on the server, create one.
// `resetKeyBackup` will delete any existing backup, so we only do this if there is no usable backup.
if (currentKeyBackup === null) {
await crypto.resetKeyBackup();
// resetKeyBackup fires this off in the background without waiting, so we need to do it
// explicitly and wait for it, otherwise it won't be enabled yet when we check again.
await crypto.checkKeyBackupAndEnable();
}
// Set the flag so that EX no longer thinks the user wants backup disabled
await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: false });
} else {
logger.info("User requested disabling key backup");
// This method will delete the key backup as well as server side recovery keys and other
// server-side crypto data.
await crypto.disableKeyStorage();
// Set a flag to say that the user doesn't want key backup.
// Element X uses this to determine whether to set up automatically,
// so this will stop EX turning it back on spontaneously.
await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true });
}
});
} finally {
setPendingValue(undefined);
DeviceListener.sharedInstance().start(matrixClient);
}
},
[setPendingValue, matrixClient],

View File

@@ -1,4 +1,5 @@
/*
* Copyright 2025 Element Creations Ltd.
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
@@ -29,7 +30,8 @@ import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydra
import { withSecretStorageKeyCache } from "../../../../SecurityManager";
import { EncryptionCardButtons } from "./EncryptionCardButtons";
import { logErrorAndShowErrorDialog } from "../../../../utils/ErrorUtils.tsx";
import { RECOVERY_ACCOUNT_DATA_KEY } from "../../../../DeviceListener";
import DeviceListener, { RECOVERY_ACCOUNT_DATA_KEY } from "../../../../DeviceListener";
import { resetKeyBackupAndWait } from "../../../../utils/crypto/resetKeyBackup";
/**
* The possible states of the component.
@@ -123,14 +125,27 @@ export function ChangeRecoveryKey({
if (!crypto) return onFinish();
try {
// We need to enable the cache to avoid to prompt the user to enter the new key
// when we will try to access the secret storage during the bootstrap
await withSecretStorageKeyCache(async () => {
await crypto.bootstrapSecretStorage({
setupNewSecretStorage: true,
createSecretStorageKey: async () => recoveryKey,
const deviceListener = DeviceListener.sharedInstance();
// we need to call keyStorageOutOfSyncNeedsBackupReset here because
// deviceListener.whilePaused() sets its client to undefined, so
// keyStorageOutOfSyncNeedsBackupReset won't be able to check
// the backup state.
const needsBackupReset = await deviceListener.keyStorageOutOfSyncNeedsBackupReset(true);
await deviceListener.whilePaused(async () => {
// We need to enable the cache to avoid to prompt the user to enter the new key
// when we will try to access the secret storage during the bootstrap
await withSecretStorageKeyCache(async () => {
await crypto.bootstrapSecretStorage({
setupNewSecretStorage: true,
createSecretStorageKey: async () => recoveryKey,
});
// Reset the key backup if needed
if (needsBackupReset) {
await resetKeyBackupAndWait(crypto);
}
await initialiseDehydrationIfEnabled(matrixClient, { createNewKey: true });
});
await initialiseDehydrationIfEnabled(matrixClient, { createNewKey: true });
});
// Record the fact that the user explicitly enabled recovery.