Add key storage toggle to Encryption settings#29113
Conversation
| let setEnabledResolve: () => void; | ||
| const setEnabledPromise = new Promise<void>((r) => { | ||
| setEnabledResolve = r; | ||
| }); |
There was a problem hiding this comment.
FYI, you can use defer here
richvdh
left a comment
There was a problem hiding this comment.
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_disabledseparately - making the KeyStoragePanel read-only at first
| ), | ||
| })} | ||
| > | ||
| <Root className="mx_KeyBackupPanel_toggleRow"> |
There was a problem hiding this comment.
shouldn't the class here be about KeyStoragePanel rather than KeyBackupPanel ?
| 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(); | ||
| }); |
There was a problem hiding this comment.
it would be good if we confirmed that the switch actually worked, rather than just exposing some nice UI?
There was a problem hiding this comment.
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?
| * 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". |
There was a problem hiding this comment.
this comment is now apparently incorrect?
| * 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. |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
could we add a constant somewhere for this name, so that we don't need to hardcode it in 15 different places?
| } 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) |
There was a problem hiding this comment.
| // 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) |
| // 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(); | ||
| } |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
won't this reset any flow that the user is in the middle of, causing a computer->window incident?
| // 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"); | ||
| } |
There was a problem hiding this comment.
I think we need to disable dehydration and delete the dehydrated device key here too.
This whole thing could usefully become crypto.deleteSecretStorage.
| mocked(useKeyStoragePanelViewModel).mockReturnValue({ | ||
| setEnabled, | ||
| isEnabled: true, | ||
| loading: false, | ||
| busy: false, | ||
| }); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
again, please remember to restoreAllMocks
|
Closing in order to re-introduce as individual PRs. |
|
For links: this was part of #26468 |
Requires matrix-org/matrix-js-sdk#4661
Checklist
public/exportedsymbols have accurate TSDoc documentation.