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/elixir-api.md
# Elixir API
The `KMS` module is the public facade. Calls route through the configured client:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
{:ok, kek} = KMS.create_alias("owner:alice")
```
Encrypt/decrypt:
```elixir
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:
```elixir
encrypted = KMS.encrypt_alias!("secret", "owner:alice", aad: aad)
"secret" = KMS.decrypt_alias!(encrypted, "owner:alice", aad: aad)
```
Tag helpers:
```elixir
{: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:
```elixir
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:
```elixir
secret = Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false)
```
See [Shared passphrase documents](use_cases/passphrase.md).
## Principals
Create a principal:
```elixir
{:ok, principal} =
KMS.create_principal(%{
kind: "users",
ref: "alice",
username: "alice@example.com",
password: password,
tags: ["tenant:acme"]
})
```
Read/update/delete:
```elixir
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:
```elixir
{:ok, session} = KMS.create_session("users", "alice", %{password: password})
```
Pass a session to authorized operations:
```elixir
{:ok, encrypted} =
KMS.encrypt_alias("secret", "tenant/acme/docs",
session: session.secret,
aad: "document:123/body"
)
```
Advance MFA factors:
```elixir
{:ok, session} = KMS.advance_session(session.secret, %{code: "123456"})
```
Refresh/nudge/delete:
```elixir
{:ok, refreshed} = KMS.refresh_session(session)
{:ok, nudged} = KMS.nudge_session(refreshed.secret)
{:ok, deleted} = KMS.delete_session(nudged.secret)
```
See [Sessions](sessions.md).
## Factors
```elixir
{: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](factors.md).
## Bindings
Bindings grant principal permissions:
```elixir
{:ok, binding} =
KMS.create_binding(principal.id, %{
permission: "kek.decrypt",
scope: "kek.alias:tenant/acme/docs"
})
```
Manage bindings:
```elixir
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](authz-bindings.md).
## 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:
```elixir
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:
```elixir
fallbacks = KMS.list_kek_fallbacks(kek.id)
{:ok, master_key} = KMS.unwrap_kek(kek.id, "ops", private_key)
```
See [Fallback recovery](fallback-recovery.md).
## WebAuthn
```elixir
{: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](factors/webauthn.md).
## Crypto primitives
Use `KMS.Crypto` directly for crypto-only mode:
```elixir
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](use_cases/crypto_only.md).