Proposal: Clean up leftover .crypted files (issue #9093)

Hi everyone,

I'd like to discuss the approach for fixing #9093 — leftover .crypted files that are never cleaned up in the resource directory.

The problem

When E2EE is enabled, .crypted files are created in two scenarios:

  1. Decryption (Resource.decrypt()) — renames the encrypted blob to .crypted, decrypts it to plaintext, but never deletes the .crypted file afterward.
  2. Sync upload (Resource.fullPathForSyncUpload()) — encrypts a plaintext resource to .crypted for uploading, but never cleans it up after the sync completes.

Over time, these orphaned files accumulate and waste disk space.

Why runtime cleanup is risky

I initially submitted a PR that ran cleanup inside maintenance(), but as Laurent correctly pointed out, this creates race conditions:

  • The DecryptionWorker may be actively reading a .crypted file during decryption.
  • The Synchronizer may be about to upload a .crypted file created by fullPathForSyncUpload() — and in this case the local DB has encryption_blob_encrypted = 0, so a database check alone isn't sufficient to protect it.

Proposed approach

Run the cleanup once at app startup, before the sync and decryption services start. At that point:

  • No DecryptionWorker is running, so no .crypted file is being read.
  • No Synchronizer is running, so no .crypted file is pending upload.
  • Any .crypted file on disk is guaranteed to be a leftover from a previous session.

The only files we'd skip are those where encryption_blob_encrypted = 1 in the database, meaning the .crypted file is the primary data source (hasn't been decrypted yet).

Questions for the community

  1. Does running cleanup at startup (before sync/decryption) sound like the right timing?
  2. Should we also clean up .crypted files inline — i.e., delete them right after Resource.decrypt() finishes and after Synchronizer completes an upload — to prevent accumulation in the first place?
  3. Any edge cases I might be missing?

Thanks for any feedback!

The synchronizer and decryption worker both run automatically on application start, so in order for your solution to work you would have to defer both until .crypted deletion completes. That wouldn’t be a good idea as there isn’t a guarantee that the clean up operation would be quick, and you wouldn’t want to be holding up the sync especially

Good point — deferring sync and decryption isn't ideal.

A better approach would be inline cleanup — deleting .crypted files at the source:

  1. In Resource.decrypt(), delete the .crypted file right after decryption succeeds.

  2. In the Synchronizer, delete the .crypted file after the upload completes.

This way each process cleans up after itself — no race conditions, no startup delays.

A lightweight startup scan could handle any leftovers from crashes.

Agreed with mrjo118 that deferring sync/decryption at startup isn't great. I went ahead and implemented the inline approach with a couple extra things:

One tricky part with the sync upload path - fullPathForSyncUpload() returns an existing .crypted as primary data when encryption_blob_encrypted = 1, but creates a temp .crypted when it's 0. So you can't just blindly delete after upload. I added an isTemporary flag to the return value so the Synchronizer knows which case it's dealing with, and wraps cleanup in a finally so it catches skipped uploads and errors too.

Also caught that encryptFile() can leave a partial .crypted behind if it fails mid-write - added cleanup in the catch for that.

PR is here: All: Fixes #9093: Delete .crypted files after decryption and sync upload by keshav0479 · Pull Request #14544 · laurent22/joplin · GitHub

2 Likes