Use this pattern when multiple users or service identities should encrypt/decrypt through KMS authorization.
This example uses RMK-only aliases and authorization bindings, not per-recipient key wrapping. If a user-held secret must be cryptographically required, use the complete Factor-bound aliases workflow instead.
KMS features:
- principals identify users/services;
- bindings grant permissions on scopes;
- sessions prove the principal and carry authorization;
- KEK aliases isolate encrypted data classes.
When to use
Good fit:
- teams sharing encrypted records;
- service-to-service scoped decrypt access;
- admin-managed authorization for document classes;
- separate encrypt and decrypt permissions;
- later MFA with TOTP or WebAuthn.
Poor fit:
- anonymous shared links;
- low-friction single shared password workflows;
- cases where KMS should not know user identities.
Use shared passphrase documents for shared-secret workflows.
Model documents by KEK alias
Create one alias per data class, tenant, or purpose:
{:ok, _kek} =
KMS.create_alias("tenant/acme/documents", tags: ["tenant:acme", "documents"])Encrypt with AAD bound to the document id:
aad = "my-app:v1:tenant:acme:document:#{document_id}:field:body"
{:ok, encrypted_body} =
KMS.encrypt_alias("document body", "tenant/acme/documents", aad: aad)Create principals
alice_password = Base.encode64(:crypto.strong_rand_bytes(32))
bob_password = Base.encode64(:crypto.strong_rand_bytes(32))
{:ok, alice} =
KMS.create_principal(%{
kind: "users",
ref: "alice",
username: "alice@example.com",
password: alice_password,
tags: ["tenant:acme"]
})
{:ok, bob} =
KMS.create_principal(%{
kind: "users",
ref: "bob",
username: "bob@example.com",
password: bob_password,
tags: ["tenant:acme"]
})Grant scoped permissions
Let Alice encrypt and decrypt tenant documents:
for permission <- ["kek.encrypt", "kek.decrypt"] do
{:ok, _binding} =
KMS.create_binding(alice.id, %{
permission: permission,
scope: "kek.alias:tenant/acme/documents"
})
endLet Bob decrypt only:
{:ok, _binding} =
KMS.create_binding(bob.id, %{
permission: "kek.decrypt",
scope: "kek.alias:tenant/acme/documents"
})Use sessions
Create sessions from passwords:
{:ok, alice_session} = KMS.create_session("users", "alice", %{password: alice_password})
{:ok, bob_session} = KMS.create_session("users", "bob", %{password: bob_password})Alice can encrypt:
{:ok, encrypted_body} =
KMS.encrypt_alias(
"document body",
"tenant/acme/documents",
session: alice_session.secret,
aad: aad
)Bob can decrypt:
{:ok, "document body"} =
KMS.decrypt_alias(
encrypted_body,
"tenant/acme/documents",
session: bob_session.secret,
aad: aad
)Bob cannot encrypt unless granted kek.encrypt:
{:error, :not_allowed} =
KMS.encrypt_alias(
"new body",
"tenant/acme/documents",
session: bob_session.secret,
aad: aad
)Ecto integration
In Phoenix, keep KMS calls in the context, not controllers and not schema callbacks.
Good shape:
def create_document(attrs, session_secret) do
id = Ecto.UUID.generate()
aad = document_aad(attrs.tenant_id, id)
with {:ok, encrypted_body} <-
KMS.encrypt_alias(attrs.body, alias_for(attrs.tenant_id), session: session_secret, aad: aad) do
insert_document(%{id: id, tenant_id: attrs.tenant_id, encrypted_body: encrypted_body})
end
end
def get_document!(id, session_secret) do
document = Repo.get!(Document, id)
aad = document_aad(document.tenant_id, document.id)
{:ok, body} =
KMS.decrypt_alias(document.encrypted_body, alias_for(document.tenant_id),
session: session_secret,
aad: aad
)
%{document | body: body}
end
defp alias_for(tenant_id), do: "tenant/#{tenant_id}/documents"
defp document_aad(tenant_id, id), do: "my-app:v1:tenant:#{tenant_id}:document:#{id}:field:body"Adding MFA
After principals exist, add factors:
Sessions can require factor advancement before full authentication.
Security notes
- Local KMS calls without
session:are admin/trusted. Always passsession:when you want KMS authorization enforced. - AAD must be stable and reproducible. Do not include secrets or mutable display names.
- Session secrets are bearer credentials. Store and transmit them like tokens.
- Bindings protect KMS operations. Your Phoenix app still needs normal application authorization for routes and records.