Summary
The pdv_client module stores each linked user's opaque vault handle in users.data in plaintext (HandleStore, under the vault_handle:CONNECTION name). Every vault read is handle-addressed, and handles are only minted through the owner's consent ceremony, so the handle store is effectively the second factor alongside the cached OAuth token. A read of the consumer site's database today yields both the handles and the access token, which is enough to replay every linked user's handle and read their vault data within the consumer's grants. The vault-side property "a stolen handle is useless without the OAuth secret" does not help here, because on the consumer site the token is present.
Proposed change
Add an optional per-connection encryption Key (a key module Key, mirroring the existing client-secret key) and have HandleStore AEAD-encrypt handles at rest under a key derived from it. With the wrapping key held outside the database (an environment, file, or secrets-manager key provider), a database dump alone yields a short-lived token but no usable handles, and a token with no handles reads nothing.
Design
- Self-contained crypto (libsodium XChaCha20-Poly1305); no dependency on the main pdv module, since pdv_client runs on a site with no vault of its own.
- Per-connection key derived with HKDF-SHA256, domain-separated by connection id; the additional authenticated data binds each ciphertext to its (connection, uid) so a stored row cannot be lifted to another user or connection.
- Opt-in: with no key configured the handle is stored as today. Fail-closed on write (a configured but unreadable key raises an error at consent rather than silently storing plaintext); on read, an undecryptable row is treated as "not linked", so the user simply re-consents.
Scope and trade-offs
This is defense-in-depth against an at-rest or database-dump compromise of the consumer site, not an active hole: it takes a full database read to matter, and exposure is already bounded by token lifetime and the owner's revocable grants. The cached OAuth access token in State is the companion exposure and is worth moving out of plaintext State for the same reason; the handle store is the higher-leverage asset because reads are handle-addressed.
Issue fork pdv-3594165
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 #4
mably commented