skip to content

The Envelope Model

Data encrypted under a per-object key, and that key encrypted under one that never leaves the manager. Asked because it is why rotating the outer key is cheap and the data itself need not be.

on this pageshow

questions

4

A store of 20 million encrypted objects rotates the key that wraps every per-object data key — what changed for the objects themselves?

level: middleimportance: must knowfreq 62%

answer

  1. two keys, one touches the bytes
  2. outer key wraps, does not encrypt
  3. rewrap the key blob, not the object
  4. a gigabyte against tens of terabytes
  5. unchanged data key still decrypts old copies

basics

~20 s

Nothing in the objects changed. Rotation rewrites each small wrapped data key under the new wrapping key and leaves every object's ciphertext and every data key exactly as they were, so any copy of a data key or of a ciphertext taken earlier still decrypts.

solid answer

~50 s

Envelope encryption puts two keys in the path: a per-object **data key** encrypts the object's bytes, and a **key-encryption key** the manager never releases wraps that data key. Rotating the wrapping key is a **rewrap** — each stored wrapped data key is unwrapped under the old key and wrapped again under the new one. The data keys are unchanged and the ciphertext is byte-identical afterwards, which is exactly why it finishes in minutes: tens of bytes of work per object instead of megabytes. The honest consequence is that it bought very little retroactively. It bounds what a future compromise of the wrapping key reaches; it does nothing about an object or a data key somebody already copied. Designs differ on whether old wraps are rewritten eagerly in a sweep or lazily the next time an object is touched.

code

pseudocode · 16 lines
pseudocode
# rotating the key-encryption key: rewrap only
for each record in metadata:
    # the manager unwraps and re-wraps internally;
    # the plaintext data key never leaves it
    record.wrappedDataKey = manager.rewrap(record.wrappedDataKey)
    metadata.write(record)
    # record.ciphertextLocation is never read and never rewritten

# changing the data keys instead: re-encrypt everything
for each record in metadata:
    dataKey    = manager.unwrap(record.wrappedDataKey)
    plaintext  = decrypt(read(record.ciphertextLocation), dataKey)   # megabytes in
    newDataKey = freshKey()
    write(record.ciphertextLocation, encrypt(plaintext, newDataKey)) # megabytes out
    record.wrappedDataKey = manager.wrap(newDataKey)
    metadata.write(record)

go deeper

for a junior

Remember the shape: the object is encrypted under its own data key, and that data key is stored encrypted under a second key the manager keeps. Two keys, and only the inner one touches the object's bytes.

for a middle

Explain the mechanics: a rotation of the outer key rewraps tens of bytes per object and leaves the ciphertext untouched, which is why it finishes in minutes rather than days. Be able to state the direction without hesitating.

for a senior

Show the operational half: how you know the sweep finished, what still holds the old key alive, and why you would not reach for this rotation during an incident. Name the remedy a leaked data key actually needs.

for a principal

The tradeoff is containment against cost. Cheap rotation is worth scheduling precisely because it is cheap, but a rotation cadence is not a containment story; decide separately what forces a re-encryption and who is allowed to authorise one.

## Two keys, and only one of them touches the bytes **Envelope encryption** nests one key inside another. Each object is encrypted under its own **data key** — a fresh symmetric key generated for that object and used for nothing else. That data key is then **wrapped**: encrypted under a **key-encryption key** held by a key manager that will perform wrap and unwrap on request but never hands out the key material itself. What the store keeps next to each object is the ciphertext plus the wrapped data key. The manager holds one key; the store holds twenty million tiny ciphertexts, each of which happens to be a key. The direction is the thing candidates reverse, and reversing it makes every later claim wrong: - the **data key** encrypts the object's bytes; - the **key-encryption key** encrypts the data key, and nothing else; - the data key is *generated*, not *derived* from the wrapping key, so changing the wrapping key cannot change it. Everything cheap about this model follows from that asymmetry. ## What rotating the wrapping key actually does 1. The manager mints a new key-encryption key and starts wrapping new data keys under it. 2. For each existing object, the stored wrapped data key is handed back to the manager, which unwraps it under the old key and wraps it again under the new one. If the manager exposes a rewrap operation, the plaintext data key never leaves it; if it does not, the caller unwraps and re-wraps, and the data key exists in the caller's memory for an instant. 3. The store writes the new wrapped blob back into the object's metadata record. The object's ciphertext is never read and never written. Nothing is decrypted except a few dozen bytes per object, inside the manager. ## The arithmetic, at 20 million objects averaging 2 MB | operation | per object | across the store | needs the plaintext object? | |---|---|---|---| | rewrap the data key | tens of bytes read, tens written | roughly a gigabyte rewritten | no | | re-encrypt under a fresh data key | ~2 MB read, ~2 MB written | about 40 TB read and 40 TB rewritten | yes | That is four orders of magnitude. The difference is not only volume. A rewrap is order-free and restartable: each record is independent, a half-finished sweep leaves a store where some records are wrapped under the old key and some under the new, and both still unwrap. A re-encryption has to run under live write traffic, needs a read path that tolerates objects in either state, and doubles the storage footprint of anything it cannot rewrite in place. ## What the rotation did not buy This is where the mechanism is usually oversold: - **Copies already taken are unaffected.** An attacker who has an object's ciphertext and its plaintext data key can still decrypt that object; the wrapping key was never in their path. - **The data keys did not change**, so nothing about the objects' own protection improved. - **The old wrapping key is still real** until it is explicitly retired. Rotating is putting a new key in place; withdrawing the old one is a separate act, and a wrapped blob copied before the sweep still unwraps under the old key for as long as that key exists. - **A leaked data key is an object-level problem with an object-level remedy** — that object has to be re-encrypted under a new data key. No amount of outer rotation substitutes. What it does buy is forward containment: from the rotation onward, a compromise of the current wrapping key reaches only what is wrapped under it, and the blast radius of the retired key shrinks to whatever was copied before the sweep finished. ## Eagerly, lazily, or not at all Designs genuinely differ here, and it is worth saying so rather than asserting one behaviour. Some managers and stores run an eager sweep that rewraps every record after a rotation. Some rewrap lazily — an object's wrapped key is upgraded the next time the object is read or written, which means a cold object can sit under a retired key for years and blocks retiring that key until a sweep is forced. Some do neither and simply keep every wrapping key alive indefinitely, which quietly makes "rotation" a label with no containment behind it. The question to ask of any such design is not what the setting is called but which of those three it is, and how the operator learns when the last record under the old key has moved. The practical read for an engineer: rotating the outer key is a cheap, safe, routine operation that should happen on a schedule, and precisely because it is cheap it is not the answer to an incident. Cheapness and containment are the same trade seen from two sides.

  • The rotation finished but the old key-encryption key cannot be retired yet. What is still holding it?
    Records still wrapped under it. Under a lazy design only objects that were touched have moved, so cold objects keep the old key alive; under an eager sweep, whatever the sweep skipped or failed on. Retiring the key before those move makes their data keys unrecoverable, so the operator needs a count of records still wrapped under the old key, not a claim that the sweep ran.
  • One object's data key is known to have been exposed. What is the remedy, and why does rotating the wrapping key not supply it?
    That object must be re-encrypted under a freshly generated data key, and the old ciphertext treated as readable by whoever holds the exposed key. Rotating the wrapping key changes only how the data key is stored, not the data key itself, so the exposed key keeps decrypting the ciphertext it encrypted — including any copy already taken.
  • Does rewrapping require the service to see the plaintext data key?
    Not if the manager offers a rewrap operation: it unwraps and re-wraps inside itself and returns only the new wrapped blob. Where the manager offers only unwrap and wrap, the caller holds the plaintext data key between the two calls — brief, but a real window, and it means the rewrap job needs the right to unwrap every key in the store.

A document in a safe, and the safe's key sealed in a second, smaller box. Changing the lock on the box means re-sealing the key, not re-typing the document.

saying these in an interview costs you the question

  • Says rotating the wrapping key re-encrypts the objects underneath it
  • Claims a rotated wrapping key makes an already-copied ciphertext unreadable
  • Thinks the data key is derived from the wrapping key, so changing one changes both
  • Treats the rotation as having closed the exposure from the old key
  • Assumes the key-encryption key must be exported to the service to rewrap
open as a page

A backup of the metadata table holding every object's wrapped data key leaks — what does the holder actually have?

level: seniorimportance: must knowfreq 50%

basics

~20 s

Wrapped data keys decrypt nothing on their own. They are usable only by getting the key manager to unwrap them, so the leak turns into an access-control question: whoever the manager will still answer can read those objects, and every unwrap it performs is on the record.

open as a page

Every object read costs an unwrap call to the key manager, so a service caches unwrapped data keys for five minutes — what does that cache cost?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The cache removes a manager call per read, and with it the manager's chance to refuse that read and its record of it. While an entry lives, withdrawing the service's right to unwrap changes nothing for those objects, and the decryption never appears in the manager's trail.

open as a page

For 20 million customer uploads across 5,000 tenants, how would you choose between one data key per object and one per tenant?

level: principalimportance: should knowfreq 30%

basics

~20 s

Granularity trades blast radius against volume. A key per object confines a leaked data key to one file and makes discarding that key an erasure; a key per tenant cuts wrapped keys and unwrap calls by a factor of 4,000 and makes one leaked key expose that tenant's entire set.

open as a page