Skip to content

Additional authenticated data

Additional authenticated data (aad) is context you send with a value when you encrypt it. It is not encrypted and not stored in the ciphertext, but it is authenticated with it: the ciphertext opens only when the same aad is sent again. Use it to tie a ciphertext to the place it belongs, such as a record id and a field name, so that a ciphertext copied anywhere else fails to open.

rypt also binds your account's tenant id into every ciphertext, whatever aad you send. The tenant binding shows the exact bytes.

All five operations take an optional aad field. encrypt and wrap bind it into the ciphertext or wrapped data key they return; decrypt, unwrap and rewrap must be sent the same aad to open one.

  • It is a JSON string of standard base64 with padding, like every binary field. URL-safe or unpadded base64 is 400 invalid_base64, with the message `aad` must be standard base64 with padding. Any other JSON type, such as a number, is 400 invalid_request.
  • It can hold any bytes, not only text. What must match is the decoded bytes.
  • Absent, null and "" all mean an empty aad. They are interchangeable: a ciphertext made with one of them opens with any of them.

Direct or envelope shows every operation with an aad.

A decrypt, unwrap or rewrap with a different aad, with none where one was used, or with one where none was used, fails with 400 invalid_ciphertext. Here the ciphertext from the encrypt example, made with the aad user=42;field=email, is sent with user=42;field=phone:

curl -sS https://api.rypt.dev/v1/keys/$KEY_ID/decrypt \
  -H "Authorization: Bearer $RYPT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ciphertext": "...base64...", "aad": "dXNlcj00MjtmaWVsZD1waG9uZQ=="}'
{"error":"invalid_ciphertext","message":"ciphertext or aad invalid","request_id":"..."}

The same status, code and message answer a ciphertext that was altered or truncated. The response does not say whether the aad or the ciphertext was wrong, and rypt cannot tell you which aad a ciphertext was made with.

The one different answer is for a ciphertext made under a key version that is no longer enabled: the same 400 invalid_ciphertext, with the message ciphertext refers to a key version that is not enabled. It is about the version, not the aad, and it does not mean your aad was right: on Free (free) and Extra wrapped (wrapped) keys rypt checks the version before it tries to open the ciphertext, so this message comes back whatever aad you send. Rotation explains when a version stops being enabled.

A mismatch is recorded in the key's audit log as a failed operation with the error code invalid_ciphertext, and it is not counted as an operation.

Every ciphertext is bound to your account's tenant id as well as to your aad. You never send these bytes and cannot leave them out. What rypt adds depends on where the key's cryptography runs.

On a Software (software) or Hardware (hsm) key, encryption runs in Cloud KMS. The associated data rypt sends to Cloud KMS is your tenant id followed by your aad:

bytes 0-15    your tenant id, 16 raw bytes
bytes 16-     your aad, as decoded from base64, 0 to 65,520 bytes

The tenant id is a UUID, sent as its 16 bytes in the order of its usual hyphenated form, not as 36 characters of text. There is no separator, length prefix or label. The tenant id is always 16 bytes and always first, so the boundary is unambiguous, and an aad that begins with another account's tenant id still has yours in front of it.

rypt adds nothing else on these keys: no key id, key version, container or operation. Each Software or Hardware key is a Cloud KMS key of its own, and its ciphertext names the key version that made it.

On a Free or Extra wrapped key, encryption runs in rypt's own process with AES-256-GCM. Your aad goes into the AES-GCM associated data after 40 bytes that rypt puts first:

bytes 0-3     "RYW1" (52 59 57 31)
bytes 4-7     key version number, unsigned 32-bit big-endian (version 1: 00 00 00 01)
bytes 8-23    your tenant id, 16 raw bytes
bytes 24-39   the key's id (the {id} in /v1/keys/{id}), 16 raw bytes
bytes 40-     your aad, as decoded from base64, 0 to 65,536 bytes

The tenant id and key id are encoded as above. Bytes 0 to 7 are also the first 8 bytes of the ciphertext, which carries the version number in clear. When rypt opens a ciphertext it takes those bytes from the ciphertext itself, so a ciphertext whose version number has been changed fails to open. Every field before your aad has a fixed width, so here too your aad cannot pass for one of them.

Each version of these keys also has its own data key, so a ciphertext from another key or another version would fail to open even without the ids in the associated data.

Free, Extra wrapped Software, Hardware
Your aad Bound Bound
Your tenant id Bound by rypt Bound by rypt
The key Bound by rypt, by its id Not added by rypt. The ciphertext comes from the key's own Cloud KMS key.
The key version Bound by rypt, in the header Not added by rypt. The ciphertext names its version.
The container Not bound Not bound
The operation that made it Not bound Not bound

A ciphertext opens only for the account that made it. Another account cannot use your keys in the first place: rypt looks a key up only within the caller's own account, and checks the key against the caller again before it uses it. The tenant binding sits behind both checks.

The container is not bound, but a key stays in the container it was made in, and its ciphertext opens only under that key. Nothing in a ciphertext records whether encrypt or wrap made it; what actions do not restrict says what follows from that. The binding does not limit what rypt itself can do with your keys; the security model covers that.

Sizes are counted after base64 decoding. The aad limit depends on where the key's cryptography runs: on Software and Hardware keys the 16 bytes of tenant id count against Cloud KMS's limit of 65,536 bytes of associated data.

Tier aad alone aad with the plaintext or data key
Free, Extra wrapped 65,536 bytes No combined limit
Software 65,520 bytes No combined limit
Hardware 65,520 bytes At most 8,176 bytes together: plaintext and aad on encrypt, data key and aad on wrap

On a Hardware key, Cloud KMS limits the plaintext and the associated data together to 8,192 bytes, and the tenant id takes 16 of them. So the largest aad there is 8,175 bytes on encrypt (with a 1-byte plaintext), and 8,144 bytes on wrap with a 32-byte data key, the size wrap generates. The combined limit is checked on encrypt and wrap only.

Anything over these limits is 413 payload_too_large, with a message that states the limit: aad must be at most 65520 bytes (65536 on Free and Extra wrapped keys), plaintext and aad together must be at most 8176 bytes for an hsm key, or dek and aad together must be at most 8176 bytes for an hsm key.

The whole JSON body is also limited to 128 KiB, and base64 makes each field a third larger, so a long aad leaves less room for the plaintext, and the aad is sent again with every decrypt. Near the limit an encrypt can succeed while the decrypt of its result is too large; see a ciphertext can be too large to decrypt.

rewrap makes the new ciphertext with the same aad it opened the old one with, and has no field for a new one. No call changes the aad of an existing ciphertext; rewrap shows how to make a new ciphertext with a new aad instead.

rypt does not store your aad, return it, or put it in the ciphertext. The audit log does not record it: an entry's bytes field is the length of the operation's input (the plaintext, ciphertext, data key or wrapped value), never the aad or its length. rypt's usage log lines do not contain it, and its request log holds no request bodies. The audit log and logs describe what each record holds.

rypt does not treat aad as secret. Its copies of your aad in memory are freed, not zeroed, and on Software and Hardware keys it sends your aad to Cloud KMS, after your tenant id, on every call. Do not put secrets in it.

The five operations ignore JSON fields they do not recognize, and field names are case-sensitive. A request whose aad field is misspelled, such as "AAD" or "add", is treated as having no aad:

curl -sS https://api.rypt.dev/v1/keys/$KEY_ID/encrypt \
  -H "Authorization: Bearer $RYPT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"plaintext": "aGVsbG8gd29ybGQ=", "AAD": "dXNlcj00MjtmaWVsZD1lbWFpbA=="}'

This succeeds and returns a ciphertext made with an empty aad. Decrypting it with the intended "aad" then fails with 400 invalid_ciphertext, and decrypting it with no aad succeeds. On decrypt, unwrap or rewrap, a misspelled field sends no aad, so a ciphertext made with one is 400 invalid_ciphertext.

To catch this, add a test that decrypts one of your ciphertexts without its aad and expects 400 invalid_ciphertext. If that decrypt succeeds, your aad never reached rypt.

  • Name where the ciphertext belongs. A record id and a field name, such as user=42;field=email, mean a ciphertext moved to another record or another field fails to open.
  • Use only values you can rebuild exactly. The aad must match byte for byte, so fix its format: field order, separators, letter case, number formatting and text encoding. Do not serialize a map whose key order can change.
  • Use values that do not change. If the aad includes something that can change, such as an email address, the ciphertext stops opening when it changes, unless you decrypt and encrypt it again with the new aad.
  • Make it unambiguous. Separate fields with a character that cannot occur in their values, or give each field a fixed width, so that two different contexts cannot produce the same bytes.
  • Keep it short on Hardware keys, where it shares 8,176 bytes with the plaintext or data key.
  • Do not add your rypt tenant id. rypt binds it already. Your own application's tenant or customer ids are another matter: if they say where the ciphertext belongs, put them in, as in tenant=acme;user=42;field=email.
  • In envelope mode, use one context for both. Send it as the aad to wrap and unwrap, and use it as the associated data of your own AEAD cipher, as the Python example does.

The aad does not stop an older ciphertext from being put back in place of a newer one for the same record: both open with the same aad. If that matters to you, include something that changes on every write and that you can check independently.