Additional authenticated data (AAD)

Copy Markdown View Source

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:

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.

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:

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

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

{: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.

{
  "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.

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.

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.

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.

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:

{: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:

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:

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:

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

Risky:

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:

{:error, :decryption_failed}

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