Packages
Key management service with embedded OTP runtime, optional HTTP API, SQLite/PostgreSQL storage, and external RMK providers
Current section
Files
Jump to
Current section
Files
docs/use_cases/multi_user_encryption.md
# Multi-user encryption
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](factor_bound_aliases.md) 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](passphrase.md) for shared-secret workflows.
## Model documents by KEK alias
Create one alias per data class, tenant, or purpose:
```elixir
{:ok, _kek} =
KMS.create_alias("tenant/acme/documents", tags: ["tenant:acme", "documents"])
```
Encrypt with AAD bound to the document id:
```elixir
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
```elixir
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:
```elixir
for permission <- ["kek.encrypt", "kek.decrypt"] do
{:ok, _binding} =
KMS.create_binding(alice.id, %{
permission: permission,
scope: "kek.alias:tenant/acme/documents"
})
end
```
Let Bob decrypt only:
```elixir
{:ok, _binding} =
KMS.create_binding(bob.id, %{
permission: "kek.decrypt",
scope: "kek.alias:tenant/acme/documents"
})
```
## Use sessions
Create sessions from passwords:
```elixir
{: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:
```elixir
{:ok, encrypted_body} =
KMS.encrypt_alias(
"document body",
"tenant/acme/documents",
session: alice_session.secret,
aad: aad
)
```
Bob can decrypt:
```elixir
{:ok, "document body"} =
KMS.decrypt_alias(
encrypted_body,
"tenant/acme/documents",
session: bob_session.secret,
aad: aad
)
```
Bob cannot encrypt unless granted `kek.encrypt`:
```elixir
{: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:
```elixir
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:
- [TOTP](../factors/totp.md)
- [WebAuthn/passkeys](../factors/webauthn.md)
Sessions can require factor advancement before full authentication.
## Security notes
- Local KMS calls without `session:` are admin/trusted. Always pass `session:` 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.