Packages

Key management service with embedded OTP runtime, optional HTTP API, SQLite/PostgreSQL storage, and external RMK providers

Current section

Files

Jump to
kms docs use_cases crypto_only.md
Raw

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.