Use this pattern when an application should encrypt and decrypt its own data at any time, without principals, sessions, bindings, KMS tables, or the KMS OTP application.
You still use KMS modules for secure primitives:
KMS.Crypto.generate_key/1KMS.Crypto.derive_key/3KMS.Crypto.encrypt/3KMS.Crypto.decrypt/3- optional local RMK provider encrypt/decrypt calls to wrap the application data key with a local RMK file
This is application-level symmetric encryption. It is simpler than full KMS authority mode.
When to use
Good fit:
- one app owns both encrypt and decrypt;
- data only needs protection at rest from database-only disclosure;
- no per-user KMS authorization is needed;
- no sessions, MFA, principals, or bindings are needed;
- you want clean, reviewed crypto primitives instead of hand-rolling AES-GCM.
Examples:
- encrypting API tokens stored in app tables;
- encrypting webhook secrets;
- encrypting integration credentials;
- encrypting private notes where the app may decrypt at any time;
- small Phoenix apps that do not need a dedicated KMS service.
Poor fit:
- users should hold separate decrypt rights;
- app operators must not be able to decrypt;
- decryption should require MFA/session authorization;
- you need remote KMS audit/authorization;
- you need break-glass principal/KEK recovery.
Use multi-user encryption or a full KMS deployment for those.
Key model
Use two pieces:
- Local RMK key file: host-owned root key material.
- Application secret: high-entropy env/secret-manager value.
Generate one random application data key, wrap it with the local RMK, and store only the wrapped form in config or an application table.
At runtime:
- unwrap application data key with
KMS.RMK.Local; - derive a field encryption key from the application secret + unwrapped data key;
- encrypt/decrypt values with
KMS.Cryptoand AAD.
This does not start the KMS OTP application, a KMS repo, or any KMS worker.
One-time setup
Generate a wrapped application data key:
rmk_key_path = "priv/kms_rmk.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: aad)Store wrapped_app_data_key in your app config, secret manager, or a protected table. Do not store app_data_key directly.
Set an application secret separately:
export MY_APP_ENCRYPTION_SECRET=<32-plus-random-bytes-base64url>
Runtime module
defmodule MyApp.Encryption do
@rmk_key_path "priv/kms_rmk.key"
@app_key_aad "my-app:v1:application-data-key"
def encrypt(plaintext, aad) when is_binary(plaintext) and is_binary(aad) do
plaintext
|> KMS.Crypto.encrypt(field_key!(), aad)
|> then(&{:ok, &1})
rescue
_error -> {:error, :encryption_failed}
end
def decrypt(ciphertext, aad) when is_binary(ciphertext) and is_binary(aad) do
case KMS.Crypto.decrypt(ciphertext, field_key!(), aad) do
plaintext when is_binary(plaintext) -> {:ok, plaintext}
:error -> {:error, :decryption_failed}
end
rescue
_error -> {:error, :decryption_failed}
end
defp field_key! do
{:ok, app_data_key} =
KMS.RMK.Local.decrypt(System.fetch_env!("MY_APP_WRAPPED_DATA_KEY"),
key_path: @rmk_key_path,
aad: @app_key_aad
)
KMS.Crypto.derive_key(
System.fetch_env!("MY_APP_ENCRYPTION_SECRET"),
app_data_key,
length: 32,
info: "my-app-field-encryption-v1"
)
end
endUse it from a changeset or context:
aad = "document:#{document_id}/body"
{:ok, encrypted_body} = MyApp.Encryption.encrypt(body, aad)
{:ok, body} = MyApp.Encryption.decrypt(encrypted_body, aad)AAD guidance
AAD should identify where ciphertext is valid:
"document:#{document_id}/body"
"integration:#{integration_id}/api-token"
"webhook-endpoint:#{endpoint_id}/secret"AAD is not secret. It must be stable and exactly reproducible during decrypt.
What this protects
This pattern protects against database-only disclosure when the attacker does not also have:
- the local RMK key file;
- the wrapped application data key;
- the application encryption secret.
It does not protect against a fully compromised running application process. That process can decrypt by design.
Why still use KMS primitives?
Even without the KMS OTP application, KMS gives you:
- AES-GCM encryption/decryption with authenticated data;
- base64 key generation with supported AES key sizes;
- HKDF-style key derivation helper;
- file-backed local RMK wrapping;
- consistent failure behavior and examples shared with the full KMS path.
This is safer than copying crypto snippets from random sources.
Rotation
To rotate the application secret:
- decrypt old ciphertext with old secret;
- re-encrypt with new secret;
- deploy new secret after re-encryption completes.
To rotate the local RMK key:
- decrypt
wrapped_app_data_keywith old RMK; - write/choose new RMK key file;
- re-wrap the same app data key with new RMK;
- deploy new wrapped key + RMK path together.