Use this pattern when a document should be decryptable by anyone who knows the same high-entropy shared secret.
This is the direct payload-encryption model. For alias-based sharing through sessions, with passphrase replacement/revocation without re-encrypting document ciphertext, use Factor-bound aliases.
KMS API:
PDK means password-derived key. KMS derives an inner data key from the caller-supplied secret and protects the payload with the configured RMK provider.
When to use
Good fit:
- shared vault documents;
- export bundles protected by a customer-held secret;
- invite links where the link carries or derives a high-entropy secret;
- data that should not decrypt from KMS/database alone.
Poor fit:
- low-entropy human passwords;
- secrets users commonly forget;
- data that needs per-user revocation without re-encryption;
- public web forms where the passphrase may be logged by proxies or analytics.
encrypt_rmk_pdk/3 enforces KMS secret policy for encryption. You can use a generated human-friendly passphrase:
shared_secret = KMS.Passphrase.generate()
# => "ba-la-na-pa-ra ..."KMS.Passphrase.generate/1 uses Diceware-style five-die rolls with 10 words by default, enough to satisfy KMS' high-entropy secret policy.
For machine-only flows, 32 random bytes encoded for transport also work:
shared_secret = Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false)Encrypt a document
document_id = Ecto.UUID.generate()
aad = "my-app:v1:document:#{document_id}:field:body"
{:ok, encrypted_body} =
KMS.encrypt_rmk_pdk(
"shared secret document body",
shared_secret,
aad: aad
)Store:
document_idencrypted_body- enough stable metadata to reconstruct
aad
Do not store shared_secret next to the document.
Decrypt a document
aad = "my-app:v1:document:#{document_id}:field:body"
{:ok, body} = KMS.decrypt_rmk_pdk(encrypted_body, shared_secret, aad: aad)Wrong secret or wrong AAD fails:
{:error, :decryption_failed} =
KMS.decrypt_rmk_pdk(encrypted_body, shared_secret, aad: "my-app:v1:document:other")Phoenix schema shape
schema "shared_documents" do
field :title, :string
field :encrypted_body, :string, redact: true
field :body, :string, virtual: true, redact: true
timestamps(type: :utc_datetime)
endContext outline:
def create_shared_document(attrs, shared_secret) do
id = Ecto.UUID.generate()
aad = shared_document_aad(id)
with {:ok, encrypted_body} <- KMS.encrypt_rmk_pdk(attrs.body, shared_secret, aad: aad) do
insert_document(%{id: id, title: attrs.title, encrypted_body: encrypted_body})
end
end
def open_shared_document(document, shared_secret) do
with {:ok, body} <- KMS.decrypt_rmk_pdk(document.encrypted_body, shared_secret, aad: shared_document_aad(document.id)) do
{:ok, %{document | body: body}}
end
end
defp shared_document_aad(id), do: "my-app:v1:shared-document:#{id}:field:body"Security notes
- AAD is not secret; it is context that must match on decrypt.
- The shared secret is a credential. Do not log it.
- Low-entropy passwords are not enough for this API.
- Rotating the shared secret requires decrypting with the old secret and re-encrypting with the new one.
- Anyone with the shared secret can decrypt. KMS bindings do not identify or revoke individual holders of that secret.
Use multi-user encryption instead when per-user authorization, audit, or revocation matters.