Packages

Key management service with embedded OTP runtime, optional HTTP API, SQLite/PostgreSQL storage, and external RMK providers

Current section

Files

Jump to
kms docs use_cases multi_user_encryption.md
Raw

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.