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

Command icon 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

mably created an issue. See original summary.

mably’s picture

Session 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:

  • Keep it out of the database session entirely: a memory-only store, or client-side.
  • If it must go in the session, wrap it with a client-held key (for example a random key in an HttpOnly cookie), so the session row holds only ciphertext and the unwrap key never touches the database.

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.

  • mably committed c1fb8e40 on 1.x
    task: #3594111 Add the owner-passphrase feature to the roadmap
    

mably changed the visibility of the branch 3594111-owner-passphrase to hidden.

mably’s picture

Design: 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

  • The Subject KEK is wrapped under BOTH keys, passphrase inner and Master KEK outer: 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).
  • Why both, in that order: enabling a passphrase then never LOWERS at-rest protection. Under the passphrase alone, a weak or "test" passphrase would be the only barrier on a database dump and would fall to offline guessing, so opting into more security could give less. With the Master KEK on the outside, a database thief cannot even reach the passphrase blob (and with OpenBao/HSM the Master KEK is not in the dump at all), so a weak passphrase is still shielded. The passphrase only ADDS a barrier the operator cannot pass; it never subtracts the one already there.
  • The operator (holds the Master KEK, not the passphrase) can strip the outer layer but is then stuck at the passphrase-wrapped Subject KEK. Read/unlock is Master KEK unwrap (server, as today) then passphrase unwrap (owner).
  • Master KEK rotation is unaffected: it re-keys the outer layer without the passphrase, carrying the inner blob along, so protected subjects rotate like any other.

Lifecycle

  • Enable: unwrap the Subject KEK as today, add the inner passphrase layer, store the salt and params (the outer Master KEK layer is unchanged). Optional recovery code (a second high-entropy inner unwrap path the owner keeps offline).
  • Unlock: once per session the owner enters the passphrase; the Subject KEK is unwrapped for the session.
  • Lock: on logout, timeout, or session end.
  • Change passphrase: re-make the inner layer with the new passphrase (one operation, no document re-encryption).
  • Disable: remove the inner passphrase layer, back to Master-KEK-only.

Session custody (v1): a cookie-held key, never in the database

  • On unlock the server generates a random session key K, stores the Subject KEK encrypted under K in a server-side session record (ciphertext in the database is fine), and sets K in an HttpOnly, Secure, SameSite cookie.
  • Each request reads K from the cookie, decrypts the record, uses the Subject KEK, discards it. Lock deletes the record and clears the cookie.
  • A database dump yields only ciphertext (K lives in the cookie, never stored); out of session K is gone. The passphrase and the raw Subject KEK never go in the cookie, only the random K.
  • Limit: the server still handles the key in memory per request, so a live-server compromise during an active session could capture it. Browser-side (WASM) crypto would close that and is a possible v2; the at-rest design is identical either way.

Consequences (accepted, opt-in)

  • Owner-present-only: a consumer cannot read any of that owner's items while the owner is offline (per-subject is all-or-nothing; per item was rejected as too complex for owners to manage).
  • A forgotten passphrase makes the vault unrecoverable by anyone, unless a recovery code was kept.
  • Operator-side export and dormancy handling cannot read a locked vault.
  • Unlocking needs the Master KEK available too (both layers), but the vault already depends on the Master KEK for everything else.

Invariant: the unlocked Subject KEK (and its session key) must never be written to the database. That is what the whole feature rests on.

mably’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

mably changed the visibility of the branch 3594111-owner-passphrase-gate to hidden.

  • mably committed 83a86ff0 on 1.x
    feat: #3594111 Owner passphrase: gate vault content with a user-held...

  • mably committed fa8c7fe8 on 1.x
    task: #3594111 Document the owner-passphrase feature
    

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.