Summary
Let a vault owner protect their content with a personal passphrase, so it is unreadable to anyone holding only the Master KEK, the operator included. Today the hierarchy is Master KEK (operator or secret store) wrapping a per-owner Subject KEK wrapping each document DEK, so whoever controls the Master KEK can decrypt any vault. A passphrase adds a user-held factor, so at-rest content cannot be read without the owner.
Proposed mechanism
Derive a key from the owner passphrase with a memory-hard KDF (Argon2id) and wrap the Subject KEK with that derived key in addition to the Master KEK, so an unwrap needs both. The passphrase and derived key live only for the duration of an owner session and are zeroized after use, like the other key material in the module.
Threat model
This lifts protection from "an attacker who steals the database" to "the operator who runs the site". It is a meaningful shift and should be opt-in per owner, off by default.
Design tensions to resolve
- Unrecoverable by design. A forgotten passphrase leaves the vault unreadable by anyone, with no operator backdoor. Needs a clear warning and an optional recovery code (a high-entropy secret the owner stores out of band that wraps the same Subject KEK as a second path).
- Consumer access. A core feature is that a consumer reads and writes a vault on the owner behalf over the API, often while the owner is offline; a passphrase only the owner knows blocks that. This suggests the gate applies per item (an owner-only "private" flag) rather than to the whole vault, so consumer-shared items stay on the existing Master KEK path while private items require the owner present.
- GDPR and lifecycle. Crypto-erase still works (drop the Subject KEK), but operator-side export and dormancy handling cannot read private items; export of private content must happen in an owner session.
- Server-side vs client-side KDF. Deriving server-side protects the at-rest database but the server sees the passphrase transiently; deriving in the browser keeps the passphrase off the server entirely (true zero-knowledge) at the cost of a JavaScript crypto path. The first is the smaller step, the second the stronger end state.
Scope
Roadmap item, design first. Listed on the roadmap under "Owner passphrase".
Issue fork pdv-3594111
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
mably commentedSession handling of the derived key
The passphrase-derived key encrypts and decrypts the owner's private documents and is held only for the session, re-derived from the passphrase, which the server never stores. The property to preserve: once that key is out of session, nothing on the server can decrypt the data, the operator included.
The derived key should not sit in the session as plaintext. Encrypting it is only worthwhile if the unwrapping key lives outside the database. Wrapping it with a server-reachable key such as the Subject KEK (which the Master KEK can unwrap) is cosmetic, because anyone who can read the session can also obtain that key. Two approaches that are genuine:
For "even the operator cannot decrypt" to hold during a live session and not only between sessions, the key must not live in the database-backed session at all; client-held is the clean answer.
Comment #8
mably commentedDesign: per-subject passphrase (v1)
Opt-in, per subject (per tenant + owner), not per item. When an owner enables it, their whole vault becomes readable only after they unlock it with their passphrase; once locked (logout, timeout, session end) nothing on the server can read it, the operator included.
Key flow: two layers, Master KEK outside the passphrase
MasterKEK_wrap(passphrase_wrap(Subject KEK)). The passphrase key is derived with Argon2id (per-subject salt); the AEAD is the existing EnvelopeCrypto, bound to (tenant, owner).Lifecycle
Session custody (v1): a cookie-held key, never in the database
Consequences (accepted, opt-in)
Invariant: the unlocked Subject KEK (and its session key) must never be written to the database. That is what the whole feature rests on.
Comment #10
mably commented