A community based topic aggregation platform built on atproto

feat(security): seal stored credentials app-side with AES-256-GCM; drop the in-database key master

Community PDS passwords and session tokens, and aggregator OAuth tokens and DPoP private keys, were encrypted with pgcrypto using a key stored in the encryption_keys table of the same database. Anyone with a database read or a backup file had the key beside the ciphertext. Credentials are now sealed in the AppView process with a key from ENCRYPTION_KEY that never enters the database, so a read or a pg_dump yields only ciphertext. Changes: - New package internal/crypto/credentialcipher: AES-256-GCM, wire format Version byte 0x01 || 12-byte nonce || ciphertext || GCM tag, with the version byte and a "<table>.<column>:<did>" context as authenticated data so a value cannot be moved between rows or columns. Sentinel errors ErrInvalidKey, ErrInvalidCiphertext, ErrUnsupportedVersion (the latter wraps the former); a 0xC3 first byte is reported as pgcrypto legacy data. - Repositories encrypt before the parameterized write and decrypt after the scan; every decrypt error names the DID and nothing is skipped or returned empty. Absent credentials stay NULL; zero-length values read as absent. - Startup conversion pass postgres.ReencryptLegacyCredentials runs before goose: one transaction with rows locked, legacy key read inside the SQL statement, per-table gating on column existence (pre-025 databases get a second boot instead of a crash), zero-length values rewritten to NULL, ephemeral dev keys refused while legacy rows exist, and a loud failure if legacy rows remain after the key table is already gone. - Migration 046 verifies every non-NULL credential starts with the version byte, updates column comments, and drops encryption_keys. Down is schema-only and documented as such. - Config: ENCRYPTION_KEY (base64, 32 bytes) required in production and validated; generated per boot in dev when unset, with a warning. rematerialize-posts refuses a generated key. Dev/CI/example env files pin a dev-only key. - Tests: T0 cipher and config tests; T1 acceptance, per-method repository, legacy-value classification, and conversion/migration tests including the pre-025 schema, zero-length values and the dropped-table case. Existing credential tests now run against the application cipher via a fixed test cipher helper. - docs/CREDENTIAL_ENCRYPTION.md: mechanism, configuration, conversion, threat model and accepted tradeoffs, production cutover and rollback caveats. PRD references to pgcrypto updated. Deployment: set ENCRYPTION_KEY in the production env before deploying, take a backup first (046 Down does not reverse the conversion), and treat older backups as containing plaintext-equivalent credentials. Reviewed with /second-opinion; Important findings fixed test-first. Deferred follow-ups are filed in the central issue backlog. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTb1aVWaZCk5xa1Mf2ej5E


+2791 -358
87 changed files