Skip to content

Handle cross-signing keys missing locally and/or from secret storage#31367

Merged
uhoreg merged 24 commits intoelement-hq:developfrom
uhoreg:key_storage_out_of_sync_cross_signing
Dec 19, 2025
Merged

Handle cross-signing keys missing locally and/or from secret storage#31367
uhoreg merged 24 commits intoelement-hq:developfrom
uhoreg:key_storage_out_of_sync_cross_signing

Conversation

@uhoreg
Copy link
Member

@uhoreg uhoreg commented Nov 28, 2025

Fixes #31187 and parts of #30438

Best to review this commit-by-commit

Checklist

uhoreg and others added 7 commits November 21, 2025 17:00
If cross-signing keys are missing both locally and in 4S, show a new toast
saying that identity needs resetting, rather than saying that the device
needs to be verified.
- move enum from SetupEncryptionToast to DeviceListener
- DeviceListener has public method to get device state
- DeviceListener emits events to update device state
brings RecoveryPanelOutOfSync in line with SetupEncryptionToast behaviour
Co-authored-by: Skye Elliot <actuallyori@gmail.com>
Copy link
Contributor

@kaylendog kaylendog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonderful, I think this is fine now!

Comment on lines 68 to 96
/**
* The state of the device and the user's account.
*/
export enum DeviceState {
/**
* The device is in a good state.
*/
OK = "ok",
/**
* The user needs to set up recovery.
*/
SET_UP_RECOVERY = "set_up_recovery",
/**
* The device is not verified.
*/
VERIFY_THIS_SESSION = "verify_this_session",
/**
* Key storage is out of sync (keys are missing locally, from recovery, or both).
*/
KEY_STORAGE_OUT_OF_SYNC = "key_storage_out_of_sync",
/**
* Key storage is not enabled, and has not been marked as purposely disabled.
*/
TURN_ON_KEY_STORAGE = "turn_on_key_storage",
/**
* The user's identity needs resetting, due to missing keys.
*/
IDENTITY_NEEDS_RESET = "identity_needs_reset",
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use string union? It allows more flexibility

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally like enums better (and this was just copied from SetupEncryptionToast), but if string unions are the currently-preferred style, then I can switch it.

Comment on lines 76 to 85
await deviceListener.whilePaused(async () => {
await accessSecretStorage(async () => {
// Reset backup if needed.
if (needsBackupReset) {
await resetKeyBackupAndWait(crypto);
} else if (await matrixClient.isKeyBackupKeyStored()) {
await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
}
});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is happening if this promises are rejected?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed it so that now it does the same thing as SetupEncryptionToast in the same situation

Comment on lines 27 to 31
return render(
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>
<RecoveryPanelOutOfSync onFinish={onFinish} onForgotRecoveryKey={onForgotRecoveryKey} />
</MatrixClientContext.Provider>,
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return render(
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>
<RecoveryPanelOutOfSync onFinish={onFinish} onForgotRecoveryKey={onForgotRecoveryKey} />
</MatrixClientContext.Provider>,
);
return render(<RecoveryPanelOutOfSync onFinish={onFinish} onForgotRecoveryKey={onForgotRecoveryKey} />, withClientContextRenderOptions(matrixClient));

describe("<RecoveyPanelOutOfSync />", () => {
function renderComponent(onFinish = jest.fn(), onForgotRecoveryKey = jest.fn()) {
return render(<RecoveryPanelOutOfSync onFinish={onFinish} onForgotRecoveryKey={onForgotRecoveryKey} />);
stubClient();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using MatrixClientPeg.safeGet(), prefer to create the matrix client and put it an a context

Suggested change
stubClient();
createTestClient();

Comment on lines 59 to 61
.mockImplementation(async (func = async (): Promise<void> => {}) => {
return await func();
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to call the function in mockImplementation? Or returning a promise for accessSecretStorage is enough?

Suggested change
.mockImplementation(async (func = async (): Promise<void> => {}) => {
return await func();
});
.mockResolvedValue(undefined)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do need to call the function in mockImplementation because we're checking whether the function calls resetKeyBackup or not. The test will pass without calling the function, but then it makes the expect(matrixClient.getCrypto()!.resetKeyBackup).not.toHaveBeenCalled(); useless.

}

afterEach(() => {
jest.restoreAllMocks();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
jest.restoreAllMocks();
jest.clearAllMocks()


const user = userEvent.setup();
mocked(accessSecretStorage)
.mockClear()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With jest.clearAllMocks() in afteEach, this not necessary

expect(accessSecretStorage).toHaveBeenCalled();
expect(onFinish).toHaveBeenCalled();

expect(MatrixClientPeg.safeGet().getCrypto()!.resetKeyBackup).toHaveBeenCalled();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto matrix client

return this.deviceState;
}

private async setDeviceState(newState: DeviceState, logSpan: LogSpan): Promise<void> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing tsdoc

@uhoreg uhoreg requested a review from florianduros December 17, 2025 04:03
@uhoreg
Copy link
Member Author

uhoreg commented Dec 17, 2025

@florianduros I think that I've addressed all your concerns

subHeading={
<SettingsSubheader state="error" stateMessage={_t("encryption|identity_needs_reset_description")} />
}
data-testid="recoveryPanel"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming seems incorrect

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, copy-and-paste error

@uhoreg uhoreg enabled auto-merge December 19, 2025 17:00
@uhoreg uhoreg added this pull request to the merge queue Dec 19, 2025
Merged via the queue into element-hq:develop with commit ebd5df6 Dec 19, 2025
32 checks passed
@uhoreg uhoreg deleted the key_storage_out_of_sync_cross_signing branch December 19, 2025 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SetupEncryptionToast and EncryptionSettingsUserTab say the device is unverified when it actually is

3 participants