Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add key storage toggle to Encryption settings #29113

Closed
wants to merge 45 commits into from

Conversation

dbkr
Copy link
Member

@dbkr dbkr commented Jan 27, 2025

Requires matrix-org/matrix-js-sdk#4661

Checklist

  • Tests written for new code (and old code if feasible).
  • New or updated public/exported symbols have accurate TSDoc documentation.
  • Linter and other CI checks pass.
  • I have licensed the changes to Element by completing the Contributor License Agreement (CLA)

Copy link
Member

@florianduros florianduros left a comment

Choose a reason for hiding this comment

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

Thanks!

Comment on lines 73 to 76
let setEnabledResolve: () => void;
const setEnabledPromise = new Promise<void>((r) => {
setEnabledResolve = r;
});
Copy link
Member

Choose a reason for hiding this comment

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

FYI, you can use defer here

Copy link
Member

Choose a reason for hiding this comment

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

Or Promise.withResolvers

Copy link
Member

@richvdh richvdh left a comment

Choose a reason for hiding this comment

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

I don't really think this is ready to land as it is.

Although I don't have concerns from the crypto safety PoV, I do have concerns about the usability of the feature, and also the fact that this is code the crypto team will end up maintaining, and I think it needs work.

I'm particularly concerned about the fuzziness around what "enabled" means as regards key storage -- I think the code is making assumptions that do not hold.

Generally though, I'm concerned that this PR is too large to give a meaningful review to. I would urge you to find ways to shave bits off of it so that we can reduce the amount of state we need to keep in our heads at once.

Some random ideas might include:

  • introducing DestructiveComponent separately
  • making DeviceListener dependent on m.org.matrix.custom.backup_disabled separately
  • making the KeyStoragePanel read-only at first

),
})}
>
<Root className="mx_KeyBackupPanel_toggleRow">
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't the class here be about KeyStoragePanel rather than KeyBackupPanel ?

Comment on lines +115 to +130
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
await util.openEncryptionTab();

await page.getByRole("checkbox", { name: "Allow key storage" }).click();

await expect(
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
).toBeVisible();

await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");

await page.getByRole("button", { name: "Delete key storage" }).click();

await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
});
Copy link
Member

Choose a reason for hiding this comment

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

it would be good if we confirmed that the switch actually worked, rather than just exposing some nice UI?

Copy link
Member

Choose a reason for hiding this comment

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

I don't have a strong opinion on this, but having a .pcss file which doesn't correspond to a specific component seems like a deviation from our normal way of doing things.

Wouldn't the normal way be to have a DestructiveEncryptionCard or something?

Comment on lines 149 to 150
* If the user needs to set up the encryption, the state will be set to "set_up_encryption".
* If the user secrets are not cached, the state will be set to "secrets_not_cached".
Copy link
Member

Choose a reason for hiding this comment

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

this comment is now apparently incorrect?

@@ -124,6 +144,7 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Encrypti
* Hook to check if the user needs:
* - to go through the SetupEncryption flow.
* - to enter their recovery key, if the secrets are not cached locally.
* ...and also whether key backup is enabled.
Copy link
Member

Choose a reason for hiding this comment

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

what, exactly, does "enabled" mean here? Is it the same or different to m.org.matrix.custom.backup_disabled ?

And are you sure that checking backupInfo?.version matches that definition?

// the user just hasn't set up 4S yet: prompt them to do so
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
// the user just hasn't set up 4S yet: prompt them to do so (unless they've explicitly said no to backups)
const disabledEvent = cli.getAccountData("m.org.matrix.custom.backup_disabled");
Copy link
Member

Choose a reason for hiding this comment

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

could we add a constant somewhere for this name, so that we don't need to hardcode it in 15 different places?

@@ -318,8 +318,11 @@ export default class DeviceListener {
// prompt the user to enter their recovery key.
showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC);
} else if (defaultKeyId === null) {
// the user just hasn't set up 4S yet: prompt them to do so
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
// the user just hasn't set up 4S yet: prompt them to do so (unless they've explicitly said no to backups)
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
// the user just hasn't set up 4S yet: prompt them to do so (unless they've explicitly said no to backups)
// the user just hasn't set up 4S yet: prompt them to do so (unless they've explicitly said no to key storage)

Comment on lines +188 to +191
// Recheck the status if this account data has been updated as this implies it has changed
if (type === "m.org.matrix.custom.backup_disabled") {
checkEncryptionState();
}
Copy link
Member

Choose a reason for hiding this comment

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

why specifically this, rather than any of the other things that could cause the initial checks to be invalidated?

const type = event.getType();
// Recheck the status if this account data has been updated as this implies it has changed
if (type === "m.org.matrix.custom.backup_disabled") {
checkEncryptionState();
Copy link
Member

Choose a reason for hiding this comment

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

won't this reset any flow that the user is in the middle of, causing a computer->window incident?

Comment on lines +93 to +108
// also turn off 4S, since this is also storing keys on the server.
// Delete the cross signing keys from secret storage
await matrixClient.deleteAccountData("m.cross_signing.master");
await matrixClient.deleteAccountData("m.cross_signing.self_signing");
await matrixClient.deleteAccountData("m.cross_signing.user_signing");
// and the key backup key (we just turned it off anyway)
await matrixClient.deleteAccountData("m.megolm_backup.v1");

// Delete the key information
const defaultKey = await matrixClient.secretStorage.getDefaultKeyId();
if (defaultKey) {
await matrixClient.deleteAccountData(`m.secret_storage.key.${defaultKey}`);

// ...and the default key pointer
await matrixClient.deleteAccountData("m.secret_storage.default_key");
}
Copy link
Member

Choose a reason for hiding this comment

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

I think we need to disable dehydration and delete the dehydrated device key here too.

This whole thing could usefully become crypto.deleteSecretStorage.

Comment on lines +56 to +61
mocked(useKeyStoragePanelViewModel).mockReturnValue({
setEnabled,
isEnabled: true,
loading: false,
busy: false,
});
Copy link
Member

Choose a reason for hiding this comment

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

this will carry over between tests: please can you jest.restoreAllMocks in an afterEach?

});

it("should call resetKeyBackup if there is no backup currently", async () => {
mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue(null);
Copy link
Member

Choose a reason for hiding this comment

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

again, please remember to restoreAllMocks

@dbkr
Copy link
Member Author

dbkr commented Feb 14, 2025

Closing in order to re-introduce as individual PRs.

@dbkr dbkr closed this Feb 14, 2025
@richvdh
Copy link
Member

richvdh commented Feb 14, 2025

For links: this was part of #26468

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants