The KMS module is the public facade. Calls route through the configured client:

config :kms, :client, KMS.Client.Local
# or
config :kms, :client, KMS.Client.Remote

Most return values are {:ok, value} or {:error, reason}. Signing-key operations are embedded-only and access the configured Ecto repository and RMK provider directly; they are not exposed by KMS.Client.Remote or KMS.API.

Signing keys

Install an RMK-wrapped PKCS#8 P-256 private key with its DER public key:

version = "2"
aad = KMS.signing_key_aad(version)
{:ok, encrypted_private_key} = KMS.RMK.encrypt(private_key_der, aad: aad)

{:ok, key} =
  KMS.install_signing_key_once(%{
    version: version,
    public_key_der: public_key_der,
    encrypted_private_key: encrypted_private_key,
    aad: aad
  })

Sign and verify an already-computed SHA-256 digest without hashing it again:

digest = :crypto.hash(:sha256, payload)
{:ok, signature_der} = KMS.sign_digest(digest, version)
:ok = KMS.verify_digest(digest, signature_der, version)

KMS.validate_signing_key/1 unwraps, decodes, validates, and caches the configured key. Signing then runs directly and concurrently in caller processes. No signer process or signing HTTP route exists.

KEK aliases and encryption

Create an alias:

{:ok, kek} = KMS.create_alias("owner:alice")

Encrypt/decrypt:

aad = "document:123/body"

{:ok, encrypted} = KMS.encrypt_alias("secret", "owner:alice", aad: aad)
{:ok, "secret"} = KMS.decrypt_alias(encrypted, "owner:alice", aad: aad)

Bang helpers raise on error:

encrypted = KMS.encrypt_alias!("secret", "owner:alice", aad: aad)
"secret" = KMS.decrypt_alias!(encrypted, "owner:alice", aad: aad)

Tag helpers:

{:ok, tags} = KMS.add_kek_tags("owner:alice", ["tenant:acme"])
{:ok, tags} = KMS.remove_kek_tags("owner:alice", ["tenant:acme"])
tags = KMS.list_kek_tags("owner:alice")
aliases = KMS.list_kek_aliases_by_tag("tenant:acme")

Tags are optional. Prefer aliases and AAD for the main encryption boundary.

RMK PDK encryption

Use when a caller supplies a high-entropy secret:

secret = KMS.Passphrase.generate()
{:ok, encrypted} = KMS.encrypt_rmk_pdk("document body", secret, aad: "document:123/body")
{:ok, "document body"} = KMS.decrypt_rmk_pdk(encrypted, secret, aad: "document:123/body")

For machine-only flows, random bytes encoded for transport also work:

secret = Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false)

See Shared passphrase documents.

Principals

Create a principal:

{:ok, principal} =
  KMS.create_principal(%{
    kind: "users",
    ref: "alice",
    username: "alice@example.com",
    password: password,
    tags: ["tenant:acme"]
  })

Read/update/delete:

principal = KMS.get_principal("users", "alice")
{:ok, principal} = KMS.fetch_principal("users", "alice")
principals = KMS.list_principals()
{:ok, principal} = KMS.update_principal("users", "alice", %{username: "new@example.com"})
{:ok, principal} = KMS.delete_principal("users", "alice")

get_principal/2 and the other get_* helpers return nil only when the resource is absent. A remote transport, HTTP, or malformed-response failure returns {:error, reason}. Use fetch_* when a uniform {:ok, value} | {:error, reason} contract is easier to handle.

Sessions

Password session:

{:ok, session} = KMS.create_session("users", "alice", %{password: password})

Pass a session to authorized operations:

{:ok, encrypted} =
  KMS.encrypt_alias("secret", "tenant/acme/docs",
    session: session.secret,
    aad: "document:123/body"
  )

Advance MFA factors:

{:ok, session} = KMS.advance_session(session.secret, %{code: "123456"})

Refresh/nudge/delete:

{:ok, refreshed} = KMS.refresh_session(session)
{:ok, nudged} = KMS.nudge_session(refreshed.secret)
{:ok, deleted} = KMS.delete_session(nudged.secret)

See Sessions.

Factors

{:ok, factor} =
  KMS.create_principal_factor("users", "alice", %{
    provider: "totp",
    label: "authenticator",
    position: 1,
    config: config
  })

factors = KMS.list_principal_factors("users", "alice")
{:ok, factor} = KMS.update_principal_factor(factor.id, %{enabled: false})
{:ok, factor} = KMS.delete_principal_factor(factor.id)

See Authentication factors.

Bindings

Bindings grant principal permissions:

{:ok, binding} =
  KMS.create_binding(principal.id, %{
    permission: "kek.decrypt",
    scope: "kek.alias:tenant/acme/docs"
  })

Manage bindings:

bindings = KMS.list_bindings(principal.id)
{:ok, binding} = KMS.fetch_binding(binding.id)
{:ok, binding} = KMS.update_binding(binding.id, %{permission: "kek.encrypt"})
{:ok, binding} = KMS.delete_binding(binding.id)

See Authorization and bindings.

Fallback recovery

Fallback functions work only through KMS.Client.Local. KMS.Client.Remote returns {:error, :not_implemented}, and the HTTP router contains no fallback routes.

Public key registry:

keys = KMS.list_fallback_public_keys()
{:ok, key} = KMS.create_fallback_public_key(%{name: "ops", curve: "X25519", public_key: public_key})
{:ok, key} = KMS.fetch_fallback_public_key_by_name("ops")

Local trusted unwrap helpers:

fallbacks = KMS.list_kek_fallbacks(kek.id)
{:ok, master_key} = KMS.unwrap_kek(kek.id, "ops", private_key)

See Fallback recovery.

WebAuthn

{:ok, challenge} = KMS.begin_webauthn_registration("users", "alice")
{:ok, credential} = KMS.finish_webauthn_registration(challenge.id, response)

{:ok, challenge} = KMS.begin_webauthn_authentication("users", "alice")
{:ok, session} = KMS.finish_webauthn_authentication(challenge.id, response)

See WebAuthn.

Crypto primitives

Use KMS.Crypto directly for crypto-only mode:

key = KMS.Crypto.generate_key(32)
encrypted = KMS.Crypto.encrypt("secret", key, "aad")
"secret" = KMS.Crypto.decrypt(encrypted, key, "aad")

See Crypto-only library mode.