Phase-3 review finding (MEDIUM/security — decision for the maintainer). The opt-in encryptCache (SQLCipher on libremail.db) does NOT cover:
Account-metadata DB (libremail-accounts.db): since #111, accounts/credentials/account_settings/signatures moved to an always-plaintext DB (AccountDatabaseModule uses FrameworkSQLiteOpenHelperFactory; AccountDataMigrator.copyAccountTables opens with empty key). So account emails, IMAP/SMTP hostnames, prefs, and signature HTML (real name/title/employer/phone) are readable at rest from a device image with no key — where pre-#111 they were SQLCipher-encrypted when the toggle was on. (Credentials' encryptedSecret remains individually Keystore-sealed — not a secret leak, a metadata/PII exposure.)
Attachment/outbox cache files (MailRepositoryImpl.ensureAttachmentFile/copyAttachments): written as plaintext Files outside the SQLCipher DB, so with encryptCache ON the bodies are encrypted at rest but attachment payloads (often the most sensitive) are not.
Decision: either extend at-rest protection (key the account DB with the passphrase; encrypt attachment cache with a Keystore-derived key when the setting is on) OR document + surface in the setting's description so users calibrate expectations. Backlog for the maintainer to choose the threat-model scope.
Phase-3 review finding (MEDIUM/security — **decision for the maintainer**). The opt-in `encryptCache` (SQLCipher on `libremail.db`) does NOT cover:
1. **Account-metadata DB** (`libremail-accounts.db`): since #111, `accounts`/`credentials`/`account_settings`/`signatures` moved to an **always-plaintext** DB (`AccountDatabaseModule` uses `FrameworkSQLiteOpenHelperFactory`; `AccountDataMigrator.copyAccountTables` opens with empty key). So account emails, IMAP/SMTP hostnames, prefs, and **signature HTML** (real name/title/employer/phone) are readable at rest from a device image with no key — where pre-#111 they were SQLCipher-encrypted when the toggle was on. (Credentials' `encryptedSecret` remains individually Keystore-sealed — not a secret leak, a metadata/PII exposure.)
2. **Attachment/outbox cache files** (`MailRepositoryImpl.ensureAttachmentFile`/`copyAttachments`): written as plaintext `File`s outside the SQLCipher DB, so with encryptCache ON the bodies are encrypted at rest but attachment payloads (often the most sensitive) are not.
**Decision:** either extend at-rest protection (key the account DB with the passphrase; encrypt attachment cache with a Keystore-derived key when the setting is on) OR document + surface in the setting's description so users calibrate expectations. Backlog for the maintainer to choose the threat-model scope.
The 2026-07-09 whole-repo review independently re-found and adversarially confirmed the attachment-payload half of this issue (severity high):
app/src/main/kotlin/org/libremail/data/repository/MailRepositoryImpl.kt:313 — Downloaded attachment and inline-image bytes are always written plaintext to cacheDir/attachments, bypassing the opt-in "Encrypt local cache" protection entirely (and surviving the key-invalidation wipe, which only deletes libremail.db).
Failure scenario: User enables settings_adv_encrypt_cache ("Encrypt cached mail stored on this device"); the DB is SQLCipher-encrypted and the app even fails closed rather than degrade (issue #359), yet ensureAttachmentFile writes every fetched attachment/inline image unencrypted via target.outputStream() (and copyAttachments stages outgoing attachment bytes plaintext under cacheDir/outbox). A device-at-rest attacker (the exact threat the opt-in addresses) reads full attachment content — documents, images — despite encryption being on. On auth-key invalidation, DatabaseFiles.clear wipes the undecryptable DB but leaves these plaintext files behind.
Verifier justification: The code does exactly what the finding claims. (1) MailRepositoryImpl.ensureAttachmentFile (line 313) writes fetched attachment/inline-image bytes straight to cacheDir/attachments/<msg>/<part>/<name> via target.outputStream().use { it.write(downloaded.bytes) } with no reference to the encryptCache setting; likewise copyAttachments (lines 534-538) stages outgoing attachment bytes under cacheDir/outbox/. A grep of the entire attachment path and of org.libremail.data.security finds zero attachment-encryption code — the opt-in encryptCache setting is only wired to SQLCipher for libremail.db (DatabaseProvisioner) and to DebugReport files (ReportEncryption/ReportStore, which explicitly FAILS CLOSED "so the user's opt-in encryption is never silently defeated by leaving a plaintext report on disk" — proving the intended threat model covers on-disk files, not just the DB). The setting's own UI copy promises "Encrypt cached mail stored on this device", and attachments are cached mail. (2) The key-invalidation wipe claim is also accurate: DatabaseFiles.clear does only context.deleteDatabase(NAME) ("Only libremail.db is wiped" per DatabaseProvisioner's comment), so plaintext attachment files survive the wipe orphaned — MailPruner deletes per-messageId dirs driven by DB rows, which no longer exist after the wipe. Trigger: enable settings_adv_encrypt_cache, open any message with an attachment or inline image — the bytes land unencrypted on disk. This matches none of the known-intentional exceptions. Mitigation is only the standard app-sandbox/FBE protection, which is exactly the baseline the opt-in exists to exceed.
Fix hint: When encryptCache is on, seal attachment/outbox files with a Keystore-backed stream cipher (reuse the KeystoreReportEncryption pattern or Jetpack Security EncryptedFile) in ensureAttachmentFile/copyAttachments and the corresponding readers, failing closed like ReportStore; additionally make the key-invalidation wipe (DatabaseProvisioner's isClearPending branch) delete cacheDir/attachments and cacheDir/outbox alongside DatabaseFiles.clear.
The 2026-07-09 whole-repo review independently re-found and **adversarially confirmed** the attachment-payload half of this issue (severity high):
`app/src/main/kotlin/org/libremail/data/repository/MailRepositoryImpl.kt:313` — Downloaded attachment and inline-image bytes are always written plaintext to cacheDir/attachments, bypassing the opt-in "Encrypt local cache" protection entirely (and surviving the key-invalidation wipe, which only deletes libremail.db).
**Failure scenario:** User enables settings_adv_encrypt_cache ("Encrypt cached mail stored on this device"); the DB is SQLCipher-encrypted and the app even fails closed rather than degrade (issue #359), yet ensureAttachmentFile writes every fetched attachment/inline image unencrypted via target.outputStream() (and copyAttachments stages outgoing attachment bytes plaintext under cacheDir/outbox). A device-at-rest attacker (the exact threat the opt-in addresses) reads full attachment content — documents, images — despite encryption being on. On auth-key invalidation, DatabaseFiles.clear wipes the undecryptable DB but leaves these plaintext files behind.
**Verifier justification:** The code does exactly what the finding claims. (1) MailRepositoryImpl.ensureAttachmentFile (line 313) writes fetched attachment/inline-image bytes straight to `cacheDir/attachments/<msg>/<part>/<name>` via `target.outputStream().use { it.write(downloaded.bytes) }` with no reference to the encryptCache setting; likewise copyAttachments (lines 534-538) stages outgoing attachment bytes under `cacheDir/outbox/`. A grep of the entire attachment path and of org.libremail.data.security finds zero attachment-encryption code — the opt-in encryptCache setting is only wired to SQLCipher for libremail.db (DatabaseProvisioner) and to DebugReport files (ReportEncryption/ReportStore, which explicitly FAILS CLOSED "so the user's opt-in encryption is never silently defeated by leaving a plaintext report on disk" — proving the intended threat model covers on-disk files, not just the DB). The setting's own UI copy promises "Encrypt cached mail stored on this device", and attachments are cached mail. (2) The key-invalidation wipe claim is also accurate: DatabaseFiles.clear does only `context.deleteDatabase(NAME)` ("Only libremail.db is wiped" per DatabaseProvisioner's comment), so plaintext attachment files survive the wipe orphaned — MailPruner deletes per-messageId dirs driven by DB rows, which no longer exist after the wipe. Trigger: enable settings_adv_encrypt_cache, open any message with an attachment or inline image — the bytes land unencrypted on disk. This matches none of the known-intentional exceptions. Mitigation is only the standard app-sandbox/FBE protection, which is exactly the baseline the opt-in exists to exceed.
**Fix hint:** When encryptCache is on, seal attachment/outbox files with a Keystore-backed stream cipher (reuse the KeystoreReportEncryption pattern or Jetpack Security EncryptedFile) in ensureAttachmentFile/copyAttachments and the corresponding readers, failing closed like ReportStore; additionally make the key-invalidation wipe (DatabaseProvisioner's isClearPending branch) delete `cacheDir/attachments` and `cacheDir/outbox` alongside DatabaseFiles.clear.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Phase-3 review finding (MEDIUM/security — decision for the maintainer). The opt-in
encryptCache(SQLCipher onlibremail.db) does NOT cover:libremail-accounts.db): since #111,accounts/credentials/account_settings/signaturesmoved to an always-plaintext DB (AccountDatabaseModuleusesFrameworkSQLiteOpenHelperFactory;AccountDataMigrator.copyAccountTablesopens with empty key). So account emails, IMAP/SMTP hostnames, prefs, and signature HTML (real name/title/employer/phone) are readable at rest from a device image with no key — where pre-#111 they were SQLCipher-encrypted when the toggle was on. (Credentials'encryptedSecretremains individually Keystore-sealed — not a secret leak, a metadata/PII exposure.)MailRepositoryImpl.ensureAttachmentFile/copyAttachments): written as plaintextFiles outside the SQLCipher DB, so with encryptCache ON the bodies are encrypted at rest but attachment payloads (often the most sensitive) are not.Decision: either extend at-rest protection (key the account DB with the passphrase; encrypt attachment cache with a Keystore-derived key when the setting is on) OR document + surface in the setting's description so users calibrate expectations. Backlog for the maintainer to choose the threat-model scope.
The 2026-07-09 whole-repo review independently re-found and adversarially confirmed the attachment-payload half of this issue (severity high):
app/src/main/kotlin/org/libremail/data/repository/MailRepositoryImpl.kt:313— Downloaded attachment and inline-image bytes are always written plaintext to cacheDir/attachments, bypassing the opt-in "Encrypt local cache" protection entirely (and surviving the key-invalidation wipe, which only deletes libremail.db).Failure scenario: User enables settings_adv_encrypt_cache ("Encrypt cached mail stored on this device"); the DB is SQLCipher-encrypted and the app even fails closed rather than degrade (issue #359), yet ensureAttachmentFile writes every fetched attachment/inline image unencrypted via target.outputStream() (and copyAttachments stages outgoing attachment bytes plaintext under cacheDir/outbox). A device-at-rest attacker (the exact threat the opt-in addresses) reads full attachment content — documents, images — despite encryption being on. On auth-key invalidation, DatabaseFiles.clear wipes the undecryptable DB but leaves these plaintext files behind.
Verifier justification: The code does exactly what the finding claims. (1) MailRepositoryImpl.ensureAttachmentFile (line 313) writes fetched attachment/inline-image bytes straight to
cacheDir/attachments/<msg>/<part>/<name>viatarget.outputStream().use { it.write(downloaded.bytes) }with no reference to the encryptCache setting; likewise copyAttachments (lines 534-538) stages outgoing attachment bytes undercacheDir/outbox/. A grep of the entire attachment path and of org.libremail.data.security finds zero attachment-encryption code — the opt-in encryptCache setting is only wired to SQLCipher for libremail.db (DatabaseProvisioner) and to DebugReport files (ReportEncryption/ReportStore, which explicitly FAILS CLOSED "so the user's opt-in encryption is never silently defeated by leaving a plaintext report on disk" — proving the intended threat model covers on-disk files, not just the DB). The setting's own UI copy promises "Encrypt cached mail stored on this device", and attachments are cached mail. (2) The key-invalidation wipe claim is also accurate: DatabaseFiles.clear does onlycontext.deleteDatabase(NAME)("Only libremail.db is wiped" per DatabaseProvisioner's comment), so plaintext attachment files survive the wipe orphaned — MailPruner deletes per-messageId dirs driven by DB rows, which no longer exist after the wipe. Trigger: enable settings_adv_encrypt_cache, open any message with an attachment or inline image — the bytes land unencrypted on disk. This matches none of the known-intentional exceptions. Mitigation is only the standard app-sandbox/FBE protection, which is exactly the baseline the opt-in exists to exceed.Fix hint: When encryptCache is on, seal attachment/outbox files with a Keystore-backed stream cipher (reuse the KeystoreReportEncryption pattern or Jetpack Security EncryptedFile) in ensureAttachmentFile/copyAttachments and the corresponding readers, failing closed like ReportStore; additionally make the key-invalidation wipe (DatabaseProvisioner's isClearPending branch) delete
cacheDir/attachmentsandcacheDir/outboxalongside DatabaseFiles.clear.