Skip to content

feat: preserve E2EE key sets during setup - #4270

Open
marinofaggiana wants to merge 17 commits into
masterfrom
8-handle-keys-that-were-removed-or-changed-on-another-device-in-the-client
Open

feat: preserve E2EE key sets during setup#4270
marinofaggiana wants to merge 17 commits into
masterfrom
8-handle-keys-that-were-removed-or-changed-on-another-device-in-the-client

Conversation

@marinofaggiana

@marinofaggiana marinofaggiana commented Aug 20, 2026

Copy link
Copy Markdown
Member

Summary

This PR allows the iOS client to safely handle E2EE account keys that are removed or replaced on another device.

Previously, the client assumed that an account had a single permanent key set. When the server-side user keys changed, storage encrypted with the previous keys could become inaccessible, while the client could continue treating its local key as active.

The client now preserves previous key sets as immutable Keychain snapshots. Storage that can only be decrypted with an archived key remains accessible as read-only, while the current server key set is used for new read/write storage.

Changes

  • Archive the current certificate, private key, server public key and passphrase before replacing or clearing active E2EE credentials.
  • Preserve multiple key snapshots for successive key rotations.
  • Resolve metadata using the active key set first, then the newest matching archived key set.
  • Support archived-key resolution for E2EE metadata versions 1, 1.2 and 2, including inherited root metadata keys for V2 child folders.
  • Classify resolved key sets as:
    • active: read/write;
    • archived: read-only;
    • unavailable: cannot decrypt.
  • Prevent archived key sets from being used for uploads, renames, deletions, folder creation, metadata changes, sharing changes, E2EE enable/disable operations and other write paths.
  • Disable the + button inside E2EE folders resolved with an archived key.
  • Refresh the + menu when the local E2EE configuration changes.
  • Validate the user certificate once per foreground cycle, avoiding repeated requests and HTTP 429 responses.
  • Compare the public keys embedded in the local and remote user certificates instead of comparing the server CA key.
  • Verify remote certificates using the server public key.
  • Verify that a downloaded and decrypted private key belongs to the remote user certificate before activating it.
  • Prompt for the new passphrase when another device replaces the account key pair.
  • Keep the previous key set available as read-only when the new passphrase is cancelled or invalid.
  • Start a new E2EE setup when the user certificate and private key have been removed from the server.
  • Remove active and archived E2EE credentials when the account is explicitly removed locally.
  • Require the account password where necessary for E2EE certificate renewal and server-side key deletion.

User-visible behavior

When account keys change from K1 to K2 on another device:

  • the client detects the new user certificate;
  • K1 is archived before any local key is replaced;
  • the user is asked for the K2 passphrase;
  • after successful verification, K2 becomes active;
  • storage encrypted with K1 remains readable but read-only;
  • storage encrypted with K2 is read/write.

If the passphrase is incorrect or the update is cancelled, K2 is not stored and the previous storage remains accessible through the archived K1 key set.

Testing

Manual key-rotation test

  1. Configured E2EE with key set K1.
  2. Created an E2EE folder containing a valid file.
  3. Confirmed that the folder and file were initially readable and writable.
  4. On a second device, deleted the user certificate/public key and encrypted private key from the server.
  5. Generated a new key set K2 on the second device.
  6. Restarted/reactivated the first device to begin a new validation cycle.
  7. Opened the existing K1 E2EE folder on the first device.
  8. Confirmed that the client detected the changed certificate and requested the K2 passphrase.
  9. Entered an incorrect passphrase and confirmed that:
    • the new key set was not installed;
    • the existing K1 file remained readable;
    • the folder was read-only;
    • the + button was disabled.
  10. Entered the correct K2 passphrase and confirmed that:
    • K2 became the active key set;
    • the existing K1 folder remained readable and read-only;
    • new E2EE storage using K2 remained read/write;
    • the + button reflected the active/read-only state correctly.
  11. Confirmed that returning to Files after enabling E2EE refreshes the + menu without requiring a manual reload.

Automated coverage

Added unit coverage for:

  • preserving active credentials while archiving;
  • restoring archived credentials after active credentials are cleared;
  • avoiding duplicate snapshots;
  • preserving usable partial key material;
  • clearing stale state during explicit local removal;
  • active versus archived key selection;
  • excluding stale credentials from active write access;
  • selecting the newest matching archived key set;
  • unavailable-key behavior;
  • PEM public-key normalization;
  • V2 child-folder root metadata-key inheritance.

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

Archive active E2EE credentials in the Keychain before setup resets or certificate renewal, while deduplicating snapshots and preserving explicit full cleanup.

Add unit coverage for complete, duplicate, and partial key-set archives, and update resolved package versions.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
@marinofaggiana
marinofaggiana marked this pull request as draft August 20, 2026 07:17
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Reuse the encrypted root key only for child metadata and verify empty-user signatures with the current certificate.

Update Firebase dependencies to 12.18.0.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Block metadata recovery and destructive operations when only archived or unavailable keys can decrypt the folder.

Allow explicit initial metadata creation for newly encrypted folders.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Pin Firebase and GoogleAppMeasurement to 12.17.0.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Detect and replace stale server key sets while preserving archived keys for read-only access. Validate write operations and route encrypted media folder deletion through the E2EE flow.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
…were-removed-or-changed-on-another-device-in-the-client
Prompt for the account password before deleting E2EE keys or renewing certificates.

Update NextcloudKit to the password-parameter branch.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Update NextcloudKit and Firebase package revisions.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Use the server public key only to authenticate certificates, and compare embedded user keys when detecting key changes.

Verify decrypted private keys match the user certificate before storing or uploading them.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
@marinofaggiana
marinofaggiana marked this pull request as ready for review August 25, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant