feat(crypto): Phase C — build-time registry ↔ Dexie audit

Before: adding a new Dexie table left the encryption decision implicit.
If you forgot to register it, the table silently shipped in plaintext
forever — no error, no warning, no footprint anywhere. The architecture
audit flagged this as the root of Concern 1.

- `scripts/audit-crypto-registry.mjs` parses database.ts's `.stores()`
  blocks and registry.ts's entries, then enforces three invariants:
    1. Every Dexie table is either in the encryption registry OR in the
       new `plaintext-allowlist.ts` — one conscious classification per
       table.
    2. No dead registry entries (referring to tables that no longer
       exist in Dexie).
    3. No table appears in both — single authoritative source.
- `plaintext-allowlist.ts` auto-seeded from current state. 105 entries,
  each tagged `// TODO: audit` as an invitation to review whether the
  table truly holds nothing sensitive. The allowlist is intentionally
  a separate file so additions are reviewable on their own (not buried
  inside database.ts schema bumps).
- Wired into `pnpm run check:crypto` + CI validate job — a new table
  now fails the PR check instead of slipping past review.
- `check:crypto:seed` regenerates the allowlist if ever needed.

Verified: drift simulation (removing aiMissions from the allowlist)
fails the audit with a clear message pointing at the missing
classification. Current state passes: 187 Dexie tables, 82 encrypted,
105 explicit plaintext.

Concern 1 is now fully closed (A: typed registry entries, B: dev-mode
runtime drift check, C: build-time audit enforcing coverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-20 14:36:32 +02:00
parent a2598b9c57
commit c7af693c6d
5 changed files with 406 additions and 3 deletions

View file

@ -0,0 +1,125 @@
/**
* Plaintext allowlist Dexie tables that are intentionally NOT encrypted.
*
* Counterpart to ENCRYPTION_REGISTRY in crypto/registry.ts. The audit script
* (`scripts/audit-crypto-registry.mjs`, wired as `pnpm run check:crypto`)
* fails if a Dexie table is in neither list.
*
* Why a separate file: adding a table here is a conscious security decision
* ("this genuinely holds no user-sensitive data") and should be reviewable
* as its own diff, not buried inside database.ts.
*
* Auto-seeded from current state on 2026-04-20 every entry below was
* introduced before the audit script existed. The `// TODO: audit` markers
* are an invitation to review each one: does this table really hold nothing
* that would embarrass the user if it leaked? If not, move it to the
* encryption registry.
*/
export const PLAINTEXT_ALLOWLIST: readonly string[] = [
'achievements', // TODO: audit
'activities', // TODO: audit
'aiMissions', // TODO: audit
'albumItems', // TODO: audit
'albums', // TODO: audit
'automations', // TODO: audit
'boardViews', // TODO: audit
'budgets', // TODO: audit
'calculations', // TODO: audit
'calendars', // TODO: audit
'ccFavorites', // TODO: audit
'ccLocationTags', // TODO: audit
'ccLocations', // TODO: audit
'cities', // TODO: audit
'companionConversations', // TODO: audit
'companionGoals', // TODO: audit
'companionMessages', // TODO: audit
'contactTags', // TODO: audit
'contextSpaces', // TODO: audit
'conversationTags', // TODO: audit
'customQuotes', // TODO: audit
'dashboardConfigs', // TODO: audit
'deckTags', // TODO: audit
'documentTags', // TODO: audit
'dreamTags', // TODO: audit
'entryTags', // TODO: audit
'eventInvitations', // TODO: audit
'eventItems', // TODO: audit
'eventTags', // TODO: audit
'fileTags', // TODO: audit
'financeCategories', // TODO: audit
'foodFavorites', // TODO: audit
'globalTags', // TODO: audit
'goals', // TODO: audit
'guideCollections', // TODO: audit
'guideTags', // TODO: audit
'habitLogs', // TODO: audit
'habits', // TODO: audit
'imageTags', // TODO: audit
'invCategories', // TODO: audit
'invCollections', // TODO: audit
'invItemTags', // TODO: audit
'invLocations', // TODO: audit
'linkTags', // TODO: audit
'manaLinks', // TODO: audit
'markers', // TODO: audit
'mealTags', // TODO: audit
'memoSpaces', // TODO: audit
'memoTags', // TODO: audit
'memoroSpaces', // TODO: audit
'moodTags', // TODO: audit
'moods', // TODO: audit
'mukkeProjects', // TODO: audit
'newsCachedFeed', // TODO: audit
'noteTags', // TODO: audit
'pendingProposals', // TODO: audit
'periodSymptoms', // TODO: audit
'photoFavorites', // TODO: audit
'photoMediaTags', // TODO: audit
'placeTags', // TODO: audit
'plantPhotos', // TODO: audit
'plantTags', // TODO: audit
'playlistSongs', // TODO: audit
'presiDeckTags', // TODO: audit
'qCollections', // TODO: audit
'questionTags', // TODO: audit
'quizAttempts', // TODO: audit
'quotesFavorites', // TODO: audit
'quotesListTags', // TODO: audit
'quotesLists', // TODO: audit
'reminders', // TODO: audit
'ritualLogs', // TODO: audit
'ritualSteps', // TODO: audit
'rituals', // TODO: audit
'runs', // TODO: audit
'savedFormulas', // TODO: audit
'sequences', // TODO: audit
'skillTags', // TODO: audit
'skills', // TODO: audit
'songTags', // TODO: audit
'spaceMembers', // TODO: audit
'storageFolders', // TODO: audit
'tagGroups', // TODO: audit
'taskLabels', // TODO: audit
'timeAlarms', // TODO: audit
'timeBlockTags', // TODO: audit
'timeClients', // TODO: audit
'timeCountdownTimers', // TODO: audit
'timeEntries', // TODO: audit
'timeProjects', // TODO: audit
'timeSettings', // TODO: audit
'timeTemplates', // TODO: audit
'timeWorldClocks', // TODO: audit
'todoProjects', // TODO: audit
'uloadFolders', // TODO: audit
'uloadTags', // TODO: audit
'userSettings', // TODO: audit
'wateringLogs', // TODO: audit
'wateringSchedules', // TODO: audit
'wetterLocations', // TODO: audit
'wetterSettings', // TODO: audit
'wishesItems', // TODO: audit
'wishesLists', // TODO: audit
'wishesPriceChecks', // TODO: audit
'workbenchScenes', // TODO: audit
];

View file

@ -3,9 +3,11 @@
* tables get encrypted.
*
* Strict allowlist semantics: anything not listed here stays plaintext.
* Adding a new module = adding an entry here. Forgetting to add a field
* means it ships in plaintext, which is the safer failure mode than the
* inverse (a typo'd field name silently failing to decrypt).
* Adding a new module = adding an entry here OR an entry in
* `plaintext-allowlist.ts` (explicit "this table genuinely holds no
* sensitive data"). The `pnpm run check:crypto` audit script enforces
* that every Dexie table appears in exactly one of the two forgetting
* a new table now fails CI instead of silently shipping plaintext.
*
* Why a central registry instead of per-module config?
* - One pull request to audit ahead of a release: "what is encrypted?"