Picture this: You've built a great app where users can store their sensitive documents such as financial records, medical information, legal contracts. Being a responsible developer, you encrypt everything. Problem solved, right?
Not quite.
One day, a user asks: "Can I share this document with my accountant or lawyer? Oh, and I'll need to revoke the lawyer's access next month."
Suddenly, you're stuck. How do you let multiple people decrypt the same data without:
Welcome to the world of envelope encryption—an elegant solution that sounds complicated but is actually beautifully simple.
Think about how a bank safety deposit with Dual-Key access might work. You want to share access with your spouse and your lawyer.
How it works in real life
Why this system exists
Revoking access
Adding access
That's envelope encryption in a nutshell.
Let's break it down with a real example: A doctor's office managing patient records.
Dr. Smith creates a new patient file:
"Patient John Doe, diagnosis: broken arm, prescription: ibuprofen..."
Dr. Smith's app:
1. Generates a random encryption key (let's call it the "Master Key")
2. Encrypts the patient data with this Master Key
3. Encrypts the Master Key with Dr. Smith's personal password
4. Stores both in the database
The patient data is now safely encrypted. Only Dr. Smith can decrypt it because only she knows her password.
Dr. Smith needs Nurse Johnson to update the patient's vitals. Here's the magic:
Dr. Smith's app:
1. Takes the SAME Master Key from Step 1
2. Encrypts it again—but this time with Nurse Johnson's password
3. Stores this second encrypted copy in the database
Now the database has:
- The encrypted patient data (encrypted with Master Key)
- Dr. Smith's encrypted Master Key (encrypted with Dr. Smith's password)
- Nurse Johnson's encrypted Master Key (encrypted with Nurse Johnson's password)
Notice: The patient data was NOT re-encrypted. We just made another locked box for the same key.
When Nurse Johnson logs in:
1. She enters her password
2. The app uses her password to unlock her encrypted Master Key
3. Now she has the Master Key
4. She uses the Master Key to decrypt the patient data
Result: She sees the same data as Dr. Smith!
Three months later, Nurse Johnson transfers to another department:
Dr. Smith's app:
1. Deletes Nurse Johnson's encrypted Master Key from the database
That's it. Done.
- The patient data stays encrypted
- Dr. Smith still has access (her encrypted Master Key is still there)
- Nurse Johnson can't decrypt anymore (her encrypted Master Key is gone)
- No re-encryption needed!
The master key should be stored securly using a secure enclave such as a Hardware Security Module (AWS offers this as a service), a self hosted Hashicorp Vault or AWS Secrets Manager.
For confidential workloads the use of Intel Software Guard Extentions (SGX) or AWS Nitro Enclaves can be used within AWS.
These extentions allow a programmer to creates encrypted, isolated "enclaves" in CPU memory for processing sensitive data (e.g., healthcare, financial) in untrusted cloud environments, protecting data from the cloud provider.
Let's say you have a 50MB medical imaging file and you need to share this with 10 people in the medical team while maintaining encryption.
Without envelope encryption:
With envelope encryption:
The difference becomes huge at scale.
Each person has their own password/key:
Audit log:
- 2025-01-15 09:30: Dr. Smith created record (encrypted with Master Key ABC)
- 2025-01-15 09:31: Dr. Smith shared Master Key ABC with Nurse Johnson
- 2025-01-15 14:20: Nurse Johnson accessed record (decrypted with Master Key ABC)
- 2025-03-20 10:00: Dr. Smith revoked Nurse Johnson's access to Master Key ABC
This level of detail makes compliance officers very happy.
Let's look at how 1Password or LastPass Teams might work:
You: Create vault, encrypt with Master Key #1
Your data: Encrypted with Master Key #1
Master Key #1: Encrypted with YOUR master password
Team lead: Creates shared vault, generates Master Key #2
Company credentials: Encrypted with Master Key #2
Master Key #2 is encrypted THREE times:
- Once with team lead's password → stored
- Once with developer's password → stored
- Once with designer's password → stored
When developer leaves:
- Delete their encrypted copy of Master Key #2
- They can no longer decrypt company credentials
- No need to change all the passwords!
Here's what this looks like in practice (simplified for clarity):
// 1. Create encrypted data
func CreateDocument(content []byte, ownerPassword string) {
// Generate a random Master Key
masterKey := generateRandomKey()
// Encrypt the document with the Master Key
encryptedDocument := encrypt(content, masterKey)
// Encrypt the Master Key with owner's password
ownerEncryptedKey := encrypt(masterKey, ownerPassword)
// Store both
database.Save(encryptedDocument, ownerEncryptedKey)
}
// 2. Share with another user
func ShareWithUser(documentID, userPassword string) {
// Get the Master Key (decrypt with owner's password)
masterKey := getMasterKeyAsOwner(documentID)
// Encrypt the SAME Master Key with user's password
userEncryptedKey := encrypt(masterKey, userPassword)
// Store user's encrypted key
database.SaveSharedKey(documentID, userID, userEncryptedKey)
}
// 3. User accesses the document
func ReadDocument(documentID, userPassword string) []byte {
// Get user's encrypted Master Key
userEncryptedKey := database.GetUserKey(documentID, userID)
// Decrypt to get the Master Key
masterKey := decrypt(userEncryptedKey, userPassword)
// Use Master Key to decrypt document
encryptedDocument := database.GetDocument(documentID)
content := decrypt(encryptedDocument, masterKey)
return content
}
// 4. Revoke access
func RevokeAccess(documentID, userID string) {
// Just delete their encrypted key
database.DeleteUserKey(documentID, userID)
// They can no longer decrypt the Master Key
// Therefore can't decrypt the document
}
❌ WRONG:
ownerKey = masterKey
userKey = deriveNewKey(masterKey) // Creates a DIFFERENT key!
// User can't decrypt because they have a different key
decrypt(data, userKey) // FAILS!
✅ RIGHT:
ownerKey = masterKey
userKey = masterKey // The SAME key, just encrypted differently
// User CAN decrypt because they have the same key
decrypt(data, userKey) // WORKS!
This was actually MY mistake when I first designed this. The tests caught it!
❌ WRONG:
database.users {
id: 1,
master_password: "password123" // Never store passwords!
}
✅ RIGHT:
database.users {
id: 1,
encrypted_master_key: [encrypted bytes] // Store encrypted keys
}
// User types password → decrypt their key → use key to decrypt data
Like changing locks after someone loses their keys:
// Rotate the Master Key periodically
func RotateMasterKey(documentID string) {
// 1. Get old Master Key and decrypt all data
oldMasterKey := getCurrentMasterKey(documentID)
data := decryptAll(oldMasterKey)
// 2. Generate new Master Key
newMasterKey := generateRandomKey()
// 3. Re-encrypt data with new key
encryptedData := encrypt(data, newMasterKey)
// 4. Re-encrypt new Master Key for all users who had access
for user in getAllUsersWithAccess(documentID) {
userEncryptedKey := encrypt(newMasterKey, user.password)
database.UpdateUserKey(documentID, user.id, userEncryptedKey)
}
// 5. Update document
database.UpdateDocument(documentID, encryptedData)
}
Here's what you store:
-- Each user has their own encrypted Master Key
CREATE TABLE encryption_keys (
owner_id UUID PRIMARY KEY,
encrypted_master_key BYTEA, -- Master Key encrypted with owner's password
created_at TIMESTAMP
);
-- Shared access: Each collaborator gets their own encrypted copy
CREATE TABLE shared_access (
id UUID PRIMARY KEY,
owner_id UUID, -- Who owns the data
user_id UUID, -- Who has been granted access
encrypted_master_key BYTEA, -- SAME Master Key, encrypted with user's password
granted_at TIMESTAMP,
revoked_at TIMESTAMP -- NULL if still active
);
-- The actual encrypted data
CREATE TABLE encrypted_documents (
id UUID PRIMARY KEY,
owner_id UUID,
encrypted_content BYTEA, -- Encrypted with Master Key
created_at TIMESTAMP
);
Don't store the "password" that encrypts the Master Keys in your code:
❌ BAD:
const MASTER_PASSWORD = "super-secret-key" // Hard-coded!
✅ GOOD:
// Fetch from AWS KMS, Azure Key Vault, HashiCorp Vault
kek := fetchFromKMS("production-key-id")
func LogAccess(action, userID, documentID string) {
auditLog.Write({
timestamp: time.Now(),
action: action, // "created", "shared", "accessed", "revoked"
user_id: userID,
document_id: documentID,
ip_address: request.IP,
})
}
Prevent brute force attacks:
func DecryptDocument(userID, documentID string) {
// Check rate limit
if rateLimiter.IsOverLimit(userID, "decrypt") {
return errors.New("too many attempts, try again later")
}
// Proceed with decryption...
}
✅ Use: AES-256-GCM
❌ Avoid: AES-ECB, DES, RC4 (broken!)
Always consider and reason about caching stratergies.
...they are powerful and can reduce load when used in the right place
// Store in memory for a short time
cache := NewTTLCache(15 * time.Minute)
func GetMasterKey(userID, documentID string) []byte {
// Check cache first
cacheKey := fmt.Sprintf("%s:%s", userID, documentID)
if cached := cache.Get(cacheKey); cached != nil {
return cached
}
// Not in cache, decrypt it
encryptedKey := database.GetUserKey(documentID, userID)
masterKey := decrypt(encryptedKey, getUserPassword())
// Cache for next time
cache.Set(cacheKey, masterKey)
return masterKey
}
Warning: Only cache in memory, never on disk. Clear on logout.
This pattern helps you comply with:
Let's say you're building a telemedicine platform:
// Patient creates a record
patientMasterKey := generateKey()
encryptedRecord := encrypt(patientData, patientMasterKey)
patientEncryptedKey := encrypt(patientMasterKey, patientPassword)
database.Save(encryptedRecord, patientEncryptedKey)
// Patient books appointment with Dr. Smith
drSmithEncryptedKey := encrypt(patientMasterKey, drSmithPassword)
database.ShareWithDoctor(patientID, drSmithID, drSmithEncryptedKey)
// Dr. Smith can now see the record
drSmithKey := decrypt(drSmithEncryptedKey, drSmithPassword)
record := decrypt(encryptedRecord, drSmithKey)
// Appointment ends
database.RevokeAccess(patientID, drSmithID)
// Dr. Smith can no longer access the record
// Patient's data stays encrypted
// No performance hit on revocation
Here's how to verify it works:
func TestSharing(t *testing.T) {
// 1. Owner creates data
ownerKey := generateKey()
data := []byte("secret document")
encrypted := encrypt(data, ownerKey)
// 2. Share with user
userEncryptedKey := encrypt(ownerKey, userPassword)
// 3. User decrypts
userKey := decrypt(userEncryptedKey, userPassword)
decrypted := decrypt(encrypted, userKey)
// 4. Verify
if string(decrypted) != string(data) {
t.Error("Sharing failed!")
}
// 5. Verify keys match
if !bytes.Equal(ownerKey, userKey) {
t.Error("Keys don't match!")
}
}
Envelope encryption solves a problem every collaborative app faces: How do you securely share encrypted data?
The answer is elegant:
It's fast, secure, auditable and scales beautifully.
Next time you're building a feature where users need to collaborate on sensitive data, remember: you're not just encrypting data, you're encrypting keys. And that makes all the difference.
You might be surprised to learn that envelope encryption isn't some exotic technique—it's everywhere. Let's look at how major tech companies and systems use this exact pattern to protect your data.
Apple's implementation is one of the most elegant examples of envelope encryption at multiple layers.
Every modern iPhone has a dedicated chip called the Secure Enclave—a separate processor that handles encryption keys and never lets them leave.
How it works:
Your iPhone storage:
├─ User files (photos, messages, documents)
│ └─ Encrypted with: File Keys (one per file)
├─ File Keys
│ └─ Encrypted with: Class Keys (based on protection class)
└─ Class Keys
└─ Encrypted with: UID Key + Passcode (in Secure Enclave)
The layers:
UID (Unique ID Key): Burned into the Secure Enclave during manufacturing. Cannot be extracted. Ever.
Your Passcode: Combined with UID to create a key that encrypts Class Keys
Class Keys: Four different types (we'll focus on "Complete Protection"):
NSFileProtectionComplete: File accessible only when unlockedFile Keys: Individual random key for each file
Your actual file: Photo, message, document
Example: Taking a photo
Step 1: iPhone generates random File Key
File Key = [random 256-bit key]
Step 2: Encrypt the photo with File Key
Encrypted Photo = AES-256(Photo, File Key)
Step 3: Encrypt File Key with Class Key
Encrypted File Key = AES(File Key, Class Key)
Step 4: Store both
Storage: Encrypted Photo + Encrypted File Key
When you lock your iPhone:
Secure Enclave: *erases Class Keys from memory*
Now:
- Your photos are still encrypted on disk
- The File Keys are still encrypted on disk
- But the Class Keys needed to decrypt File Keys are gone
- Without your passcode, nobody can reconstruct the Class Keys
- Therefore: Nobody can decrypt the File Keys
- Therefore: Nobody can decrypt your photos
This is why FBI couldn't crack the San Bernardino iPhone!
Your passwords in iCloud Keychain use envelope encryption too:
Password for Netflix → Encrypted with Item Key
Item Key → Encrypted with Keychain Key
Keychain Key → Encrypted with iCloud Security Code
iCloud Security Code → Derived from device passcode + iCloud account
When you add a new device:
- New device gets its own encrypted copy of Keychain Key
- Same keychain, multiple encrypted copies
- Remove device = delete its encrypted key copy
Android uses a similar but slightly different approach:
Direct Boot Mode (before unlock):
├─ DE Keys (Device Encrypted)
│ └─ Available even when locked
│ └─ Used for: Alarms, phone calls
└─ CE Keys (Credential Encrypted)
└─ Available only after unlock
└─ Used for: Apps, photos, messages
Both encrypted with hardware-backed keys in Trustzone
AWS KMS is built around envelope encryption.
Your file: company-secrets.pdf (50 MB)
Step 1: Generate Data Key from KMS
Request: "KMS, give me a data key for customer-master-key-123"
Response: {
Plaintext: [random 256-bit key],
CiphertextBlob: [that key, encrypted by master key]
}
Step 2: Use plaintext key to encrypt file
Encrypted File = AES-256(company-secrets.pdf, Plaintext Key)
Step 3: Store encrypted file + encrypted key
S3 stores:
- company-secrets.pdf.encrypted (50 MB)
- Encrypted Data Key (256 bytes) ← stored as metadata
Step 4: Delete plaintext key from memory
The plaintext key is immediately wiped
To decrypt later:
1. Get encrypted key from S3 metadata
2. Send to KMS: "Decrypt this key using customer-master-key-123"
3. KMS returns plaintext key
4. Use plaintext key to decrypt file
5. Wipe plaintext key from memory again
Why this is brilliant:
Without envelope encryption:
- 50 MB file needs to be sent to KMS for encryption
- Network transfer: slow, expensive
- KMS has to process 50 MB: $$$
With envelope encryption:
- Only 256 bytes sent to KMS
- File encrypted locally: fast, cheap
- KMS just manages tiny keys: $
Corporate scenario:
Employee's laptop:
FVEK encrypted with:
1. Employee's TPM
2. Company's recovery password (stored in Active Directory)
Employee leaves company:
→ Company can still decrypt drive using recovery password
→ Company rotates FVEK
→ Re-encrypts drive with new FVEK
→ New FVEK only encrypted with company recovery password
→ Employee's TPM key useless
Employee's data on their personal devices: Unaffected
Signal uses envelope encryption for metadata protection:
Your message to Bob: "Meet at 3pm"
Step 1: Encrypt message content
Message Key = generate_random()
Encrypted Message = encrypt("Meet at 3pm", Message Key)
Step 2: Encrypt Message Key for recipient
Bob's Identity Key = [Bob's public key]
Encrypted Message Key = encrypt(Message Key, Bob's Identity Key)
Step 3: Seal the sender (envelope encryption strikes again!)
Sender Key = generate_random()
Sealed Envelope = encrypt({
from: "You",
encrypted_message: Encrypted Message,
encrypted_key: Encrypted Message Key
}, Sender Key)
Step 4: Encrypt Sender Key for recipient
Encrypted Sender Key = encrypt(Sender Key, Bob's Identity Key)
Signal server sees:
- Sealed blob (no idea who sent it)
- Destination: Bob
- Cannot read contents
- Cannot see who sent it
Bob receives:
1. Decrypts Sender Key
2. Uses Sender Key to unseal envelope (learns it's from you)
3. Decrypts Message Key
4. Decrypts message content
Vault uses envelope encryption for its transit secrets engine:
Application needs to encrypt credit card:
1. App: "Vault, encrypt this credit card with key-name-prod"
2. Vault: Generates data key from master key
3. Vault: Encrypts credit card with data key
4. Vault: Returns encrypted credit card + encrypted data key
5. App: Stores both in database
Later, to decrypt:
1. App: "Vault, decrypt this using key-name-prod"
2. App: Sends encrypted data key + encrypted credit card
3. Vault: Decrypts data key using master key
4. Vault: Decrypts credit card using data key
5. Vault: Returns plaintext credit card
6. App: Uses it, then immediately discards
Key rotation:
- New version of master key created
- Old encrypted data keys can still be decrypted
- New encryptions use new master key version
- Background job re-encrypts data keys with new master key
Chrome and Edge encrypt cookies using OS-level envelope encryption:
Windows:
Cookie value: session_token=abc123
Step 1: Generate random cookie key
Step 2: Encrypt cookie with cookie key
Step 3: Encrypt cookie key with DPAPI
(Data Protection API - uses Windows login password)
Step 4: Store encrypted cookie + encrypted key
Result:
- Cookies encrypted on disk
- Tied to your Windows user account
- Another Windows user can't decrypt your cookies
- Cookie stealing malware can't easily extract them
macOS:
Cookie value: session_token=abc123
Step 1: Generate random cookie key
Step 2: Encrypt cookie with cookie key
Step 3: Store cookie key in Keychain
(Encrypted with keychain password)
Step 4: Store encrypted cookie in browser DB
Chrome process:
1. Asks macOS Keychain for cookie key
2. User prompted for password if needed
3. Decrypts cookie
4. Uses cookie for web request
WhatsApp's multi-device support is envelope encryption magic:
Your messages: Encrypted with Message Keys
Message Keys encrypted separately:
├─ For your phone (Encrypted with Phone Key)
├─ For your laptop (Encrypted with Laptop Key)
└─ For your tablet (Encrypted with Tablet Key)
Add new device (laptop):
1. Scan QR code from phone
2. Establish secure channel
3. Phone sends: All Message Keys encrypted with Laptop Key
4. Laptop can now decrypt all messages
Remove device:
→ Delete device's encrypted Message Keys
→ Future messages: Only encrypt for remaining devices
Notice the common pattern across all these systems:
1. Data encrypted with fast, random symmetric key (DEK)
2. DEK encrypted multiple times for different users/devices/contexts
3. Access control = managing encrypted DEK copies
4. Revocation = delete encrypted DEK copy
5. No need to re-encrypt the actual data
This is the power of envelope encryption—it's not just a technique, it's the fundamental pattern for secure, scalable, multi-party access to encrypted data.
Apple, Google, Microsoft, AWS, Signal, and every major security system uses envelope encryption because:
It's not just a good idea—it's the way to do secure data sharing at scale.
Got questions or found this helpful? Get in touch if you have encryptions needs, Mechanical Rocks' encryption experts would love to help, but we need the keys first ;)