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/use_cases/crypto_only.md
# Crypto-only library mode
Use this pattern when you want KMS crypto primitives but not KMS storage, principals, sessions, bindings, migrations, or the KMS OTP application.
The application owns encryption policy and can decrypt at any time.
## What runs
Nothing KMS-specific is supervised.
You call modules directly:
- `KMS.Crypto` for AES-GCM encryption/decryption, key generation, and key derivation;
- optionally `KMS.RMK.Local` for file-backed wrapping of an application data key.
No KMS database is required.
## When to use
Good fit:
- database-only disclosure is the main threat;
- the running application is allowed to decrypt;
- no per-user KMS authorization is needed;
- secrets are app-owned, not user-owned;
- you want reviewed primitives instead of hand-rolled crypto.
Examples:
- API tokens for third-party integrations;
- webhook signing secrets;
- OAuth refresh tokens;
- small private notes where app admins may decrypt;
- tenant-level settings secrets.
Poor fit:
- app operators must not be able to decrypt;
- decrypt should require user MFA or KMS sessions;
- KMS should enforce per-user permissions;
- multiple applications need one central key authority.
Use [embedded KMS](embedded.md), [remote KMS](remote.md), or [multi-user encryption](multi_user_encryption.md) for those.
## Basic direct encryption
Generate and store a high-entropy key outside the database:
```elixir
data_key = KMS.Crypto.generate_key(32)
```
Encrypt with AAD that names where the ciphertext belongs:
```elixir
aad = "integration:#{integration_id}/api-token"
encrypted_token = KMS.Crypto.encrypt(api_token, data_key, aad)
```
Decrypt with the exact same AAD:
```elixir
api_token = KMS.Crypto.decrypt(encrypted_token, data_key, aad)
```
If the AAD differs, decrypt fails.
## Derive a field key
For an application-level secret plus per-environment salt:
```elixir
field_key =
KMS.Crypto.derive_key(
System.fetch_env!("MY_APP_ENCRYPTION_SECRET"),
System.fetch_env!("MY_APP_ENCRYPTION_SALT"),
length: 32,
info: "my-app-field-encryption-v1"
)
```
Then encrypt/decrypt with `field_key`.
## Add local RMK wrapping
If you do not want one raw data key sitting in app config, wrap it with a local RMK file:
```elixir
rmk_key_path = "/var/lib/my_app/rmk.key"
app_key_aad = "my-app:v1:application-data-key"
app_data_key = KMS.Crypto.generate_key(32)
{:ok, wrapped_app_data_key} = KMS.RMK.Local.encrypt(app_data_key, key_path: rmk_key_path, aad: app_key_aad)
```
At runtime:
```elixir
{:ok, app_data_key} =
KMS.RMK.Local.decrypt(System.fetch_env!("MY_APP_WRAPPED_DATA_KEY"),
key_path: "/var/lib/my_app/rmk.key",
aad: "my-app:v1:application-data-key"
)
```
See [Application-wide encryption](application_wide.md) for a complete module shape.
## Ecto shape
Use a virtual field and persist only ciphertext:
```elixir
schema "integrations" do
field :encrypted_api_token, :string, redact: true
field :api_token, :string, virtual: true, redact: true
end
```
Encrypt in a changeset or context:
```elixir
aad = "integration:#{integration.id}/api-token"
encrypted_api_token = KMS.Crypto.encrypt(api_token, field_key, aad)
```
Decrypt on read:
```elixir
api_token = KMS.Crypto.decrypt(integration.encrypted_api_token, field_key, aad)
```
## Security boundary
Crypto-only mode protects ciphertext when the attacker has only the application database.
It does not protect against an attacker who controls the running app process or can read both:
- the database ciphertext;
- the data key, application secret, or RMK key file needed to decrypt.
That is expected. This mode is for transparent application encryption, not user-held secrecy.