Minstrel keeps credentials it has to read back: indexer, Prowlarr and
download-client keys and passwords, the AcoustID key, cover-art and tag
provider keys, the SMTP password, and each user's ListenBrainz token and
Subsonic password. They were stored in the clear; they are now sealed with
AES-256-GCM under a per-column HKDF key derived from the stream secret, each
value bound to its row.
- internal/secret: Seal/Open/Reveal, and Columns, the one list of sealed
columns (a backup will leave them out). Install takes the master key at
startup; SealStored seals any value still in the clear, so an existing
database is brought over in place on the first boot.
- Every write seals and every read opens: acquire (one read path, getService
/ usableServices), acoustid settings, SMTP, ListenBrainz, Subsonic auth and
password generation, cover-art and tag provider settings.
- A value that does not open (the stream secret changed) is logged and reads
as unset, to be entered again in Settings. docs/security.md says so.
- Tests: secret unit tests (wrong owner/purpose/key, malformed, no key), a
SealStored integration test over all eight columns plus a schema check that
each listed column exists, and the existing tests now assert sealed storage.
dbtest installs a test key for integration tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewrite writeErr(w, err) to wrap *apierror.Error via apierror.From,
preserving the existing {"error": {"code", "message"}} wire envelope.
Add writeErrWithLog helper for 500-class errors that need an operator
log line. Migrate all 13 admin_*.go handler files (~76 call sites) to
the new signature; T3 will sweep the remaining api package.
The old 4-arg writeErr is removed, so non-admin call sites in
internal/api will not compile until T3 lands. This is by design — T2
and T3 are paired.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New internal/mailer/ package:
- Sender interface with two impls: SMTPSender (production, reads
smtp_config at send time so admin edits apply without restart;
uses stdlib net/smtp + STARTTLS) and FakeSender (test/concurrent-
safe call recorder).
- Embedded text + HTML templates for the password reset email,
rendered via stdlib text/template + html/template. The HTML
uses the FabledSword forest-teal accent color.
- ErrNotConfigured surfaces when smtp_config.enabled is false or
required fields are empty; callers like the future forgot-password
handler will treat this as "log and pretend success" to avoid
user-enumeration leaks.
Three admin endpoints under RequireAdmin:
- GET /api/admin/smtp-config — returns the singleton; password
field is masked ("***" or "").
- PUT /api/admin/smtp-config — updates settings. Validates
host + from_address are non-empty when enabled=true. Empty
password in the request preserves the stored value (so the
operator doesn't have to re-enter it on every save).
- POST /api/admin/smtp-config/test — sends a real test email to
the calling admin's email. 400 if admin has no email; 500 with
the error message on send failure (so the operator can debug
config without grep-then-trace through logs).
Tests cover the password-mask, password-preservation-on-empty,
enabled-requires-host-and-from validation, and the no-email-on-file
rejection. Mailer unit tests cover the fake recorder and template
rendering. The "real SMTP send" path needs a live server and isn't
covered in CI; the FakeSender covers that role for downstream tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>