Stored credentials are encrypted at rest (#5927)
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build the server image (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 10s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 1m40s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 1m44s
CI & Build / Build & push image (push) Successful in 38s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m18s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m9s
Desktop (Tauri) / Update manifest (push) Successful in 4s

#5927 "Adopt the family idea secret.Box": the SMTP password and the backup
bucket's secret key were kept as plain text in the settings table, so a copy
of the database alone could read them.

- New sealing.py: AES-GCM under an HKDF-derived key, each value bound to its
  setting's name. The key is a random file, /var/inkwell/sealing.key (0600),
  made on first use. It lives on the data volume, not in the database, which
  is where the signing secret sits by default.
- set_settings seals a secret before storing it; get_setting opens it. A value
  that no longer opens (key file lost) reads as not set, so Settings asks
  for it again.
- At startup, credentials stored before this are sealed in place, so nothing
  has to be re-entered. A data folder that can't be written never stops the
  server; saving a password then answers with the reason.
- New dependency: cryptography (approved by the operator).

Also #1919 "Deprecate / remove the PWA": kept. Its service worker caches
only the offline page, so an installed copy always runs the live build. One
stale comment in sw.js is corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 14:18:41 -04:00
co-authored by Claude Opus 5.5
parent edcb5c4c11
commit c04e949c40
10 changed files with 290 additions and 10 deletions
+5 -2
View File
@@ -138,8 +138,11 @@ The public address is required because the emailed link has to point somewhere t
server can trust. Built from the request instead, a forged `Host` header would mail
someone a reset link to a site the forger controls.
The SMTP password is kept in the database and never sent back to the browser; the
field shows only that one is saved. The forgot-password page answers the same way,
The SMTP password is kept in the database encrypted, and never sent back to the
browser; the field shows only that one is saved. The key it is encrypted with is
`/var/inkwell/sealing.key`, on the data volume rather than in the database, so a copy
of the database alone can't read it. Keep the data volume: if that file is lost, the
password reads as not set and has to be entered again. The forgot-password page answers the same way,
at the same speed, whether or not an address has an account, and
`reset_emails_per_account` (Settings → Security) caps how many reset emails one
address can be sent.