LC-77-SMTP-SEAL: migrate SMTP password to AES-256-GCM-sealed storage #200
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Background
LC-77 email ingress (shipped across PRs #194-#199) stores the IMAP-poll password AES-256-GCM-sealed at rest via
crate::db::imap_config, modeled on the VAPID precedent (crate::db::vapid). The sealed password is decrypted only at startup, into memory owned by the poll-loop task, and never logged.The pre-existing SMTP password (used for outbound digest / password reset / email verification) is stored plaintext in the
settingskey-value table undersmtp_pass, written byroutes::admin::post_settings. Asettings.dbleak yields a usable SMTP credential without needingLETS_CHAT_SECRET_KEY.This inconsistency was flagged during the LC-77 plan and explicitly scoped out of v1 (see PR #195 commit body,
docs/email-ingress.md"Security notes" section).Scope
Migrate SMTP password to the same VAPID-sealed pattern LC-77's IMAP password uses.
Two-commit shape, mirroring the LC-77 commit-2 + commit-3 ordering:
Schema migration under
server/migrations/settings/: addsmtp_password_encrypted BLOB+smtp_password_nonce BLOBcolumns (either on a new dedicatedsmtp_configsingleton table mirroringimap_inbox_config, or alongside the existingsettingsrows). One-time backfill at startup: if the legacysmtp_passrow is populated and the new columns are NULL, seal the plaintext into the new columns, then NULL the legacy row (or drop it in a follow-up migration).Code change: extend
routes::admin::post_settingsto callcrypto::seal(via a newdb::smtp_config::writehelper) before writing the password. Updatemail::Mailer::from_env(or whichever loader reads SMTP creds) to callcrypto::openat startup. Empty password input preserves the existing sealed value (matching both the LC-77 IMAP form UX and the existing SMTP write-only UX).Test requirements
/admin/settingsnever appears in the on-disk sealed BLOB and never echoes from the GET render. Mirrortests/routes_admin_imap_settings.rs::first_save_seals_password_and_round_trips_via_read.settings.smtp_passrows get backfilled into the sealed columns at startup.Anti-scope
SMTP_PASSWORDenv var stays the operator's way to bootstrap creds without going through the admin UI).Why now
settings.dbis the most likely backup-target leak surface (small, often shipped in restore archives). Sealing brings SMTP up to the bar VAPID and IMAP-poll already meet.docs/email-ingress.md"Security notes" calls out this inconsistency. Closing the gap removes a documented known weakness.References
crate::db::vapidfor the seal/open pattern.crate::db::imap_configfor the LC-77 v1 mirror.docs/email-ingress.md"Security notes" section.