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-piiGood 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/encryptPOST /api/decryptPOST /api/rmk_pdk/encryptPOST /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::aadis added to the AWS KMS encryption context as"kms:rmk:aad".KMS.RMK.GCP::aadis sent as Cloud KMSadditionalAuthenticatedData.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:emailIf 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}"
endRisky:
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.