Multi-user encryption

Copy Markdown View Source

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"
    })
end

Let 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 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.