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 aad.md
Raw

docs/aad.md

# Additional authenticated data (AAD)

Additional authenticated data (AAD) is context that is authenticated with ciphertext but is not encrypted and is not stored inside the ciphertext by KMS.

AAD must match during decrypt. If the encrypt-side AAD and decrypt-side AAD differ, decrypt fails with an authentication error such as `:decryption_failed`.

Use AAD to bind ciphertext to the place where it is valid.

## What AAD protects

AAD helps prevent valid ciphertext from being replayed in the wrong context.

Examples:

- ciphertext for tenant A copied into tenant B;
- ciphertext for one database row copied into another row;
- ciphertext for one purpose reused for another purpose;
- ciphertext decrypted through the wrong API path or key alias by mistake.

AAD does **not** hide data. Treat AAD as non-secret metadata. It may appear in logs, cloud KMS audit trails, traces, or request bodies depending on the caller and provider.

AAD also does **not** replace authorization. It is a cryptographic context check, not a policy check.

## What to put in AAD

Prefer stable, deterministic, low-cardinality context that identifies the intended use of the ciphertext.

Good AAD examples:

```text
kms:v1:tenant:tenant_123:customer:ssn
kms:v1:tenant:tenant_123:invoice:inv_456:field:payment_token
kms:v1:service:billing:purpose:webhook-secret
kms:v1:kek-alias:prod/customer-pii
```

Good AAD usually includes:

- version: `kms:v1`;
- tenant or account id;
- resource type;
- stable resource id when ciphertext belongs to one row/object;
- field or purpose name;
- environment when the same keys exist in multiple environments.

Avoid:

- secrets, passwords, tokens, PII, or raw user input;
- timestamps, random values, request ids, or values that change between encrypt and decrypt;
- huge JSON blobs;
- ambiguous strings such as `"data"` or `"default"`;
- context that depends on display names or mutable labels.

If AAD changes, old ciphertext cannot decrypt until you use the original AAD or re-encrypt with the new AAD.

## KMS APIs that support AAD

### Alias encryption

`KMS.encrypt_alias/3` and `KMS.decrypt_alias/3` accept `:aad`.

```elixir
aad = "kms:v1:tenant:tenant_123:profile:user_456:field:ssn"

{:ok, encrypted} = KMS.encrypt_alias("123-45-6789", "customer-pii", aad: aad)
{:ok, cleartext} = KMS.decrypt_alias(encrypted, "customer-pii", aad: aad)
```

Decrypting with a different AAD fails:

```elixir
{:error, :decryption_failed} =
  KMS.decrypt_alias(encrypted, "customer-pii", aad: "kms:v1:tenant:other")
```

Batch decrypt can carry per-item AAD by passing maps:

```elixir
{:ok, cleartexts} =
  KMS.decrypt_alias(
    [
      %{encrypted: encrypted_a, aad: "kms:v1:tenant:a:field:ssn"},
      %{encrypted: encrypted_b, aad: "kms:v1:tenant:b:field:ssn"}
    ],
    "customer-pii"
  )
```

### Remote HTTP API

Remote clients forward `:aad` through the JSON `opts` object.

```json
{
  "cleartext": "secret",
  "alias": "customer-pii",
  "opts": {
    "aad": "kms:v1:tenant:tenant_123:field:ssn"
  }
}
```

Supported endpoints include:

- `POST /api/encrypt`
- `POST /api/decrypt`
- `POST /api/rmk_pdk/encrypt`
- `POST /api/rmk_pdk/decrypt`

### RMK password-derived-key helpers

`KMS.encrypt_rmk_pdk/3` and `KMS.decrypt_rmk_pdk/3` accept `:aad` for the inner AES-GCM payload.

```elixir
aad = "kms:v1:tenant:tenant_123:integration:stripe"

{:ok, encrypted} = KMS.encrypt_rmk_pdk("api-secret", high_entropy_secret, aad: aad)
{:ok, cleartext} = KMS.decrypt_rmk_pdk(encrypted, high_entropy_secret, aad: aad)
```

The outer RMK provider also protects the PDK payload. The supplied AAD binds the secret-derived inner encryption layer.

### RMK providers

`KMS.RMK.encrypt/2` and `KMS.RMK.decrypt/2` accept `:aad`.

```elixir
aad = "kms:v1:internal:bootstrap-secret"

{:ok, encrypted} = KMS.RMK.encrypt("secret material", aad: aad)
{:ok, cleartext} = KMS.RMK.decrypt(encrypted, aad: aad)
```

Provider behavior:

- `KMS.RMK.Local`: AES-GCM AAD; default is `"kms-rmk-local"`.
- `KMS.RMK.AWS`: `:aad` is added to the AWS KMS encryption context as `"kms:rmk:aad"`.
- `KMS.RMK.GCP`: `:aad` is sent as Cloud KMS `additionalAuthenticatedData`.
- `KMS.RMK.StackIT`: current provider client does not expose a separate AAD field.

For AWS and GCP, AAD may be visible in provider-side metadata/audit surfaces. Do not put secrets in AAD.

### Low-level crypto helpers

`KMS.Crypto.encrypt/3` and `KMS.Crypto.decrypt/3` accept AAD directly.

```elixir
key = KMS.Crypto.generate_key()
aad = "kms:v1:local:test-vector"

encrypted = KMS.Crypto.encrypt("secret", key, aad)
"secret" = KMS.Crypto.decrypt(encrypted, key, aad)
```

Use this only when you are intentionally managing the data key yourself. Normal application code should prefer `KMS.encrypt_alias/3`.

### Asymmetric fallback envelopes

`KMS.AsymmetricCrypto.encrypt_for_public_key/3` and `KMS.AsymmetricCrypto.decrypt_with_private_key/4` accept `:aad`.

```elixir
private_key = KMS.AsymmetricCrypto.generate_private_key()
public_key = KMS.AsymmetricCrypto.derive_public_key(private_key)
aad = "kms:v1:fallback:tenant_123"

{ephemeral_public_key, encrypted} =
  KMS.AsymmetricCrypto.encrypt_for_public_key("break-glass secret", public_key, aad: aad)

"break-glass secret" =
  KMS.AsymmetricCrypto.decrypt_with_private_key(
    encrypted,
    ephemeral_public_key,
    private_key,
    aad: aad
  )
```

Fallback envelope creation code accepts `:aad`; public unwrap and rotate APIs must use the same context that was used when the fallback envelope was created:

```elixir
{:ok, master_key} =
  KMS.unwrap_kek(kek_id, public_key_ref, private_key, aad: "kms:v1:fallback:kek:#{kek_id}")
```

If fallback envelopes were created with default AAD, unwrap them with default AAD too. If custom AAD was used, keep it stable for create, unwrap, and rotate operations.

## Practical guidance

### Bind to authorization scope

When ciphertext is protected by a KEK alias and authorization binding, include the same stable scope in AAD:

```elixir
aad = "kms:v1:scope:kek.alias:customer-pii:tenant:tenant_123:field:ssn"
```

This makes successful decrypt require both:

- authorization to use the KEK alias;
- exact cryptographic context match.

### Version your format

Start AAD with a version prefix:

```text
kms:v1:tenant:tenant_123:resource:user_456:field:email
```

If you later change the format, use `kms:v2` for new data. Keep the old AAD derivation code until old ciphertext is re-encrypted.

### Canonicalize structured context

If you build AAD from structured values, generate one canonical string. Do not rely on map order.

Good:

```elixir
def customer_field_aad(tenant_id, customer_id, field) do
  "kms:v1:tenant:#{tenant_id}:customer:#{customer_id}:field:#{field}"
end
```

Risky:

```elixir
Jason.encode!(%{field: field, customer_id: customer_id, tenant_id: tenant_id})
```

### Keep AAD available

AAD is not stored by KMS alias encryption or RMK APIs. Your application must be able to recreate it exactly at decrypt time.

Good sources:

- database primary key;
- tenant id;
- field name;
- stable enum/purpose string;
- API version.

Bad sources:

- current time;
- request id;
- mutable display name;
- non-canonical JSON;
- data that may be corrected later.

### Log safely

AAD is not secret, but it can still be sensitive if it includes customer ids, emails, or business identifiers. Prefer opaque ids over names or email addresses. Do not include PII.

## Failure mode

AAD mismatch should be treated as tampering, wrong context, or programmer error.

Typical result:

```elixir
{:error, :decryption_failed}
```

Do not fall back to decrypting without AAD after an AAD failure. That defeats the protection.