0009. Privacy model: hashes by default, keyed¶
Status: proposed (Phase 2, 2026-11-10) ยท Requirements: FR-09, FR-50, FR-57, NFR-11, NFR-12
Context¶
A state is often a customer's message, a payment or a prompt. The brief says raw state is never stored unless a project opts in, and only hashes and tags are kept. Phase 1 open question 2: a plain SHA-256 of a short or guessable state (an email address, an order number) can be reversed by hashing guesses.
Options¶
- Plain SHA-256. Simple, and the same everywhere. Reversible by guessing short states.
- HMAC-SHA-256 with a per-project key stored in the database. Safe against outsiders, but anyone with a database dump has both the key and the hashes.
- HMAC-SHA-256 with a per-project key derived from a server secret held outside the database.
Decision¶
State hash (option 3):
- state_hash = HMAC-SHA-256(k_project, RFC 8785 canonical JSON of the state), where
k_project = HMAC-SHA-256(TRIMSET_SECRET, project_id).
- The record calls the field state_hash, with state_hash_alg: "hmac-sha256/1". This
replaces the brief's state_sha256.
- The SDK receives k_project in the policy bundle, which only the project's keys can
read. Rotating TRIMSET_SECRET changes every future hash; this is documented.
Raw state:
- Read only to forward it and hash it. Never written to logs, traces, errors or alerts.
- If the project opts in, it goes to decision_states, with its own retention (30 days
by default).
- Only admin keys can read it. Every read, and every change to the setting, goes in the
audit log.
Tags:
- Strings only: at most 20 keys, keys up to 64 characters, values up to 256.
- There is an optional allow-list of keys per project.
- The docs say to put no personal data in tags or in external_ref.
Secrets are never in the database, and never pass through the API. Upstream keys and alert-channel secrets live only in the server environment, inside operator-defined upstreams and channels that projects pick by name (ADR 0011).
Deletion and telemetry: - Deletion (FR-57) reaches decisions, outcomes, review items and raw state. - Fits keep parameters, bin counts and anonymous (score, label) pairs for the simulator, with no ids. - trimset collects no telemetry.
Consequences¶
- A database leak alone doesn't let anyone reverse state hashes. The server secret has to leak too.
- Hashes from different projects can't be matched with each other.
- Reviewers get context from the link template (FR-50), not from stored state.