Crypto-only library mode

Copy Markdown View Source

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, remote KMS, or multi-user encryption for those.

Basic direct encryption

Generate and store a high-entropy key outside the database:

data_key = KMS.Crypto.generate_key(32)

Encrypt with AAD that names where the ciphertext belongs:

aad = "integration:#{integration_id}/api-token"
encrypted_token = KMS.Crypto.encrypt(api_token, data_key, aad)

Decrypt with the exact same AAD:

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:

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:

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:

{: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 for a complete module shape.

Ecto shape

Use a virtual field and persist only ciphertext:

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:

aad = "integration:#{integration.id}/api-token"
encrypted_api_token = KMS.Crypto.encrypt(api_token, field_key, aad)

Decrypt on read:

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.