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>
Rewrites the internal/api comment blocks to rule 32: task, milestone and
rule ids gone, the stories behind them restated as the constraint they
left, stale references corrected (the stream secret is wired from config,
the timezone handler already refreshes the scheduler, the Android app is
the native client, the design docs cited are not in the repo). Also
rewraps comment lines over 80 columns left by the earlier batches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`minstrel admin reset-password` copied the new login password into
subsonic_password, which is stored in plain text because Subsonic t/s
sign-in needs it. Every account recovered through the CLI had its login
password readable in the database, and changing the password later left
the copy behind.
- reset-password now changes only password_hash.
- Migration 0064 clears every subsonic_password, removing the copies.
- Settings gets a Subsonic password card: the server generates a random
password, shows it once, and it can be regenerated or turned off
(GET/POST/DELETE /api/me/subsonic-password, audited). Generated rather
than user-chosen so it can never be a reused password.
- docs/security.md describes the separate password instead of the known
issue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>