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.RemoteMost 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)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")