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:
@@ -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],
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user