DEV Community

Jude Wakim
Jude Wakim

Posted on

Envelope Encryption, Explained From First Principles

Why a data key exists, what the auth tag and AAD actually do, and the one attack that still works when your encryption is flawless.


I'm Jude Wakim. I build and run The Arsenal, a platform for independent insurance agents that stores regulated personal data for real people. I'm the only engineer on it, which means I'm also the security team.

A while back I would have told you "everything is encrypted at rest" and considered the question answered. Then I actually built field-level encryption on AWS — KMS, Lambda, DynamoDB — and found out how much that sentence was hiding.

This is the article I wanted when I started.

Most explanations of envelope encryption are three sentences long. You generate a key. You encrypt your data with it. You encrypt that key with another key. That's the mechanism, and it tells you nothing about the decisions you actually have to make.

It also skips the part I most want you to leave with:

What stops a valid ciphertext from being moved somewhere it doesn't belong?

By default, nothing does. You can lift an encrypted value out of one record, drop it into another, and it
will decrypt cleanly. Right key. Valid signature. Real data. Wrong person. And the length of your key is
completely irrelevant to that attack.

That problem has a fix, it costs one line of code, and almost nobody mentions it.

How this is laid out

  1. Glossary — every term I use, in plain English. Skip it if you already know them.
  2. Why not just one key? — what envelope encryption buys you over a master key on its own, and the choice between one data key for everything versus a fresh one per write.
  3. The four moving parts — data key, IV, auth tag, AAD. What each one is for, and what breaks if you get it wrong.
  4. The decision summary — every tradeoff in one table.

1. Glossary

Term In plain English
Plaintext The readable data, before encryption.
Ciphertext The scrambled output, after encryption.
Master key (or KEK, "key-encrypting key") The top key. It only ever encrypts other keys, never your actual data. Lives inside hardware you can't extract it from.
Data key (or DEK, "data-encrypting key") The working key. It encrypts your actual data. Cheap to make, disposable.
Envelope encryption The pattern of using a data key for the data, then wrapping that data key with the master key — so the wrapped key can travel next to the data safely.
KMS Key Management Service. AWS's managed key vault. It holds the master key and never hands it to you.
HSM Hardware Security Module. The tamper-resistant hardware the master key physically lives in.
AES-256-GCM The specific encryption algorithm. AES is the cipher, 256 is the key size, GCM is the mode — and GCM is the part that also detects tampering.
AEAD "Authenticated Encryption with Associated Data." A category of algorithm that does two jobs at once: hides the data and proves it wasn't altered. GCM is one.
Auth tag A 16-byte value produced alongside the ciphertext. If anyone changes a single bit, the tag stops matching and decryption fails.
IV (or nonce) "Initialization Vector." A random starter value that makes the same plaintext encrypt to different ciphertext every time. Must never repeat under the same key.
AAD "Additional Authenticated Data." Extra context you attach to the encryption that is not hidden, but is protected. This is the one that solves the moving problem.
Encryption context AWS KMS's name for AAD, applied to the wrapped data key. Same idea, one level up.
Field-level encryption Encrypting individual values inside a record, rather than encrypting the whole disk or whole database.

2. Why not just one key?

The naive version, and why it doesn't work

The obvious approach: keep one master key in KMS, send it your data, get ciphertext back.

Three problems, and only the third one really matters.

  1. It's too small. KMS will encrypt about 4KB directly. That's a hard ceiling, not a performance suggestion — bigger payloads have no route through the API at all.

  2. It's too slow and too expensive. Every encrypt and every decrypt is a network call to a service with a rate limit and a per-request price. A page that reads fifty records, each with several protected values, turns into hundreds of round trips.

  3. And here's the real one: the master key never leaves its hardware. That is the entire security property you're buying. The key material sits in an HSM, cannot be exported, and every single use is an API call that gets logged. If you encrypt data with it directly, you've inherited that guarantee and the requirement that all of your cryptography happen over the network, forever.

What envelope encryption actually trades

You ask KMS once for a data key. It hands back two copies of the same key:

  • one in plaintext, for you to use right now
  • one encrypted under the master key, for you to store

You encrypt your data locally with the plaintext copy, throw that copy away, and save the encrypted copy next to the ciphertext.

The result is a self-describing record. Everything needed to decrypt travels with the data — except the one thing that matters, which never left the hardware.

You kept the custody guarantee. You moved the bulk work local. That's the whole trade.

One data key, or a fresh one every time?

This is a real decision with a real cost, and most tutorials make it silently.

One shared data key A fresh key per write
If one key leaks Everything it ever encrypted is readable One value is readable
Patterns across records Comparable — an attacker can learn from similarities None. No two ciphertexts share a key
Cost per write Nearly zero One key-generation call
Storage One wrapped key total One wrapped key per value
IV management You now own a hard problem (see §3) Solved for free — each key is used once
Deduplication Possible Impossible

I went with a fresh key per write. Not because "more keys is more secure" — that's a slogan, not a reason. The actual reason is that it makes the worst case small. I would rather pay a fixed, predictable cost on every write than have to reason about how bad a single key compromise could get.

Notice the row that surprises people: per-write keys make the IV problem disappear. That's a genuine engineering benefit, not a security talking point, and it's covered next.


3. The four moving parts

Same shape for each one: what it is, what it's for, what breaks without it.

🔑 The data key

What it is: 32 random bytes that do the actual encrypting.

What it's for: Letting you encrypt locally at full speed while the master key stays in hardware.

What breaks without it: You're back to sending every byte of data to KMS — capped at 4KB, one network call at a time.

Get right: Throw away the plaintext copy the moment you're done with it.


🎲 The IV (initialization vector)

What it is: 12 random bytes, generated fresh for every single encryption, stored in the clear next to the ciphertext. It is not a secret.

What it's for: Making identical plaintext produce different ciphertext each time. Without it, two people with the same value would have visibly identical ciphertext, and that alone leaks information.

What breaks without it: Reusing an IV with the same key is not a small weakening — it's a collapse. It leaks the relationship between the two messages, and it can expose the internal value GCM uses for authentication, at which point an attacker can forge valid tags.

Get right: 12 bytes, not 16. GCM uses a 12-byte IV directly; any other length gets hashed first, which is slower and gains you nothing.

This is where per-write data keys quietly pay for themselves. If every key is used exactly once, two IVs can never collide under the same key, and the entire problem stops existing.


🏷️ The auth tag

What it is: 16 bytes produced alongside the ciphertext. Decryption fails if it doesn't match.

What it's for: Proving nobody modified the ciphertext. This is the difference between an attacker not being able to read your data and not being able to change it — and those are very different products.

What breaks without it: In modes with no tag, flipping a bit in the ciphertext flips the matching bit in the plaintext. An attacker who can't read a value can still edit it in a predictable direction. Flip a false to a true. Change an amount. They never learn the value; they change it anyway.

Here's the wrong turn I took: I read "authenticated encryption," concluded the ciphertext was tamper-proof, and moved on. It isn't. It's tamper-evident — and only against changes to the bytes themselves.

Which brings us to the actual subject of this article.


🔗 AAD (additional authenticated data)

What it is: Extra context you feed into the encryption that is not encrypted, but is covered by the auth tag. It travels in the clear. If it doesn't match on the way back out, decryption fails.

What it's for: Binding a ciphertext to where it belongs.

What breaks without it: Everything in the box at the top of this article.

The auth tag proves the bytes weren't altered. It has no opinion whatsoever about location. Copy an encrypted value from one record into the same field of another record and every check passes, because every check is about the bytes — and the bytes are pristine. They're just in the wrong place.

So you bind them:

// Node 24, aes-256-gcm. dek = the 32-byte plaintext data key from KMS.
const iv  = randomBytes(12);
const aad = Buffer.from(`${recordId}|${fieldName}`, 'utf8');

const cipher = createCipheriv('aes-256-gcm', dek, iv);
cipher.setAAD(aad);                    // authenticated, but NOT encrypted
const ct  = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();       // 16 bytes
Enter fullscreen mode Exit fullscreen mode

On the way back, you rebuild the AAD from where the value physically is — never from anything stored beside it:

const decipher = createDecipheriv('aes-256-gcm', dek, iv);
decipher.setAAD(Buffer.from(`${recordId}|${fieldName}`, 'utf8'));
decipher.setAuthTag(tag);
// throws if this value was moved to a different record or a different field
Enter fullscreen mode Exit fullscreen mode

That's the entire defense. One extra line on each side.

Two ways to accidentally undo it:

Read the AAD from the payload instead of the location. If you store the record ID next to the ciphertext and read it back from there, an attacker who moves the value just moves that too. The AAD has to come from the context you are decrypting into.

Pick the wrong granularity. Binding to the record only stops cross-record moves — but still allows swapping two fields within one record. Binding to (record, field) stops both. Ask yourself: between which two positions would moving this value be useful to an attacker? Bind at least that finely.


4. The decision summary

Five questions. If you can answer these about your own system, you understand the scheme.

The piece The question it answers Get it wrong and…
Envelope construction How do I keep the hardware custody guarantee without routing all my data over the network? You're capped at 4KB and paying a round trip per value
Key granularity How bad is one leaked key allowed to be? One key compromise exposes everything it ever touched
IV Am I certain no key is ever used twice with the same nonce? Confidentiality and integrity fail together
Auth tag Did I buy integrity, or only confidentiality? Attackers can edit data they cannot read
AAD Is this ciphertext allowed to be here? Valid data decrypts in the wrong record, silently

And what none of it covers

Worth writing down next to any implementation, because this is the half that gets left out of the security page:

  • It protects data at rest, and nothing else. Every path that legitimately decrypts sees plaintext. If your top threat is a compromised application rather than a stolen backup, this isn't the control you needed.
  • Encrypted fields stop being queryable. Anything you encrypt properly, you can no longer search, sort, or index — which makes field-level encryption a per-field design decision, not a security level you turn up. That's a whole article on its own, and it's the one I'm writing next.
  • It doesn't protect the master key from someone who can just ask it to decrypt. Key custody and key authorization are different problems. The HSM boundary is worth exactly as much as the policy sitting in front of it.

If you take one thing from this: state the control, then state what it doesn't cover. For envelope encryption that second sentence is short, and I've never once regretted writing it down.


Let's talk

I'm writing a series on the security decisions behind building and running a production platform solo — data protection, identity and access, multi-tenant isolation, audit integrity. If you're working through any of this, or you think I've got something wrong, I'd genuinely like to hear it.

Questions, corrections, or war stories: https://linkedin.com/in/jude-wakim

More from this series: https://dev.to/@vvakim

Top comments (0)