Protecting Sensitive Data Using Envelope Encryption
Source: Dev.to
Envelope encryption is a technique that uses two layers of encryption to protect sensitive data.
- First layer: encrypts the data using a symmetric encryption algorithm with a randomly generated Data Encryption Key (DEK), which secures the actual plaintext.
- Second layer: encrypts (or wraps) the DEK using a Key Encryption Key (KEK). The KEK is typically a long‑lived key stored and protected inside a KMS or HSM, and it may be either symmetric or asymmetric depending on the system.
This two‑tiered approach ensures the security of both the DEK and the encrypted data. Envelope encryption is widely used as part of centralized key‑management systems (KMS) provided by major cloud platforms.
In this article, we will explore how this technique works through a simple example implementation.
What Are The Benefits Of Envelope Encryption?
-
Stronger security
In most production environments, the KEK is stored and protected inside a KMS backed by HSMs, ensuring that it never leaves the secure hardware boundary. Because the KEK is required to unwrap the DEK, an attacker cannot decrypt the ciphertext even if they obtain the encrypted data—the DEK itself cannot be recovered without the KEK. -
Simplified key management
Envelope encryption allows an organization to manage many DEKs under one or more KEKs, greatly reducing operational complexity. KEKs can be rotated without re‑encrypting all the data, since existing encrypted DEKs continue to work with older KEK versions. This makes large‑scale key rotation feasible and minimizes impact on the underlying data storage.
Encryption Flow
Now that we’ve covered the fundamentals of envelope encryption, let’s walk through a simple implementation to demonstrate how to encrypt sensitive data — such as a credit card number — using this technique.
// Fake card number
String plainText = "5105105105105100";
1. Generate a random Data Encryption Key (DEK)
private static final String KEY_ALGORITHM = "AES";
private static final int AES_KEY_LENGTH = 256;
// ...
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
keyGenerator.init(AES_KEY_LENGTH);
SecretKey dataEncryptionKey = keyGenerator.generateKey();
2. Encrypt the plaintext with AES‑GCM
private static final String ENC_ALGORITHM = "AES/GCM/NoPadding";
// ...
byte[] bPlainText = plainText.getBytes(StandardCharsets.UTF_8);
Cipher cipher = Cipher.getInstance(ENC_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, dataEncryptionKey);
byte[] iv = cipher.getIV(); // Initialization vector
byte[] cipherText = cipher.doFinal(bPlainText);
3. Encode the IV and ciphertext with Base64
String encodedIv = Base64.getEncoder().encodeToString(iv);
String encodedCipherText = Base64.getEncoder().encodeToString(cipherText);
Sample output
Base64 Encoded IV: g4xPa5FCgkw+KzmT
Base64 Encoded Cipher: fWegEZo4dlmm1YHvAe+njcZrcz10r5W9ntEEpQK5OgA=
Wrapping the Data Encryption Key (DEK)
Up to this point, we’ve covered the first layer of envelope encryption, which encrypts the input data. Now we’re ready for the second layer — wrapping the DEK itself.
In production, KEKs are generated and stored inside KMS services backed by HSMs. Since the KEK never leaves secure hardware, the application cannot directly access it.
For illustration, we’ll use a simplified in‑memory KMS shown below.
package com.example;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class InMemoryKMS {
private static final String KEY_ALGORITHM = "AES";
private static final int AES_KEY_LENGTH = 256;
private static final String WRAP_TRANSFORMATION = "AESWrap";
private final Map<String, List<SecretKey>> keyStore = new HashMap<>();
public String createKEK() {
SecretKey key = generateKey();
String keyId = UUID.randomUUID().toString();
keyStore.computeIfAbsent(keyId, k -> new ArrayList<>()).add(key);
return keyId;
}
public void rotateKEK(String keyId) {
List<SecretKey> versions = keyStore.get(keyId);
if (versions == null) {
throw new IllegalArgumentException("Unknown keyId: " + keyId);
}
SecretKey newKek = generateKey();
versions.add(newKek);
}
public byte[] wrapDEK(String keyId, SecretKey dataEncryptionKey) {
try {
SecretKey activeKek = getLatestKey(keyId);
Cipher cipher = Cipher.getInstance(WRAP_TRANSFORMATION);
cipher.init(Cipher.WRAP_MODE, activeKek);
return cipher.wrap(dataEncryptionKey);
} catch (NoSuchPaddingException | IllegalBlockSizeException |
NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
}
// (truncated for brevity)
}
The remainder of the InMemoryKMS implementation is omitted for brevity.
Envelope Encryption in Java – Cleaned Up Markdown
unwrapDEK Method
SecretKey unwrapDEK(String keyId, byte[] wrappedDataEncryptionKey) {
try {
List<SecretKey> versions = keyStore.get(keyId);
if (versions == null) {
throw new IllegalArgumentException("Unknown keyId: " + keyId);
}
// Try each KEK version (newest first), just like cloud KMS
for (int i = versions.size() - 1; i >= 0; i--) {
SecretKey kek = versions.get(i);
try {
Cipher cipher = Cipher.getInstance(WRAP_TRANSFORMATION);
cipher.init(Cipher.UNWRAP_MODE, kek);
return (SecretKey) cipher.unwrap(
wrappedDataEncryptionKey,
KEY_ALGORITHM,
Cipher.SECRET_KEY);
} catch (GeneralSecurityException ignored) {
// Try next version
}
}
throw new IllegalStateException("Unable to unwrap DEK with any KEK version");
} catch (Exception e) {
throw new IllegalStateException("Failed to unwrap DEK", e);
}
}
Helper Methods
private SecretKey generateKey() {
try {
KeyGenerator generator = KeyGenerator.getInstance(KEY_ALGORITHM);
generator.init(AES_KEY_LENGTH);
return generator.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private SecretKey getLatestKey(String keyId) {
List<SecretKey> versions = keyStore.get(keyId);
if (versions == null || versions.isEmpty()) {
throw new IllegalArgumentException("Unknown keyId: " + keyId);
}
return versions.get(versions.size() - 1);
}
Wrapping the Data Encryption Key (DEK)
InMemoryKMS kms = new InMemoryKMS();
String keyId = kms.createKEK();
byte[] wrappedDEK = kms.wrapDEK(keyId, dataEncryptionKey);
Encode the Wrapped DEK for Storage
String encodedWrappedDEK = Base64.getEncoder().encodeToString(wrappedDEK);
Sample Encryption Output
Base64 Encoded IV: +W1d4YVNBBJFDuaB
Base64 Encoded Cipher: Wib4Hx96rSBn7dTXrrdKEb3BTxbUdDm7PPCxybc20/I=
KEK ID: eeeb0c35-b166-4680-a800-3a28641fb03c
Base64 Encoded WrappedDEK: dzQCY+x+JsDdHpscjf1qiWgm2utGEwNFIBIX6QiWqW/mjIRP42jFgw==
Decryption Flow
During encryption we stored:
- The IV (initialization vector)
- The ciphertext
- The wrapped DEK
- The KEK identifier
Because the KEK never leaves the KMS, the application must ask the KMS to unwrap the DEK before it can decrypt the ciphertext.
Unwrap the DEK
byte[] wrappedDek = Base64.getDecoder().decode(encodedWrappedDEK);
SecretKey dataEncryptionKey = kms.unwrapDEK(keyId, wrappedDek);
Note: The DEK should never be persisted; keep it in memory only for as short a time as possible.
Decrypt the Ciphertext
byte[] iv = Base64.getDecoder().decode(encodedIv);
byte[] cipherText = Base64.getDecoder().decode(encodedCipherText);
Cipher cipher = Cipher.getInstance(ENC_ALGORITHM);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, dataEncryptionKey, gcmParameterSpec);
byte[] decrypted = cipher.doFinal(cipherText);
String decryptedText = new String(decrypted, StandardCharsets.UTF_8);
System.out.println("Decrypted Text: " + decryptedText);
Result
Decrypted Text: 5105105105105100
A Note on Key Rotation
Key rotation is a broader topic, but the basic idea is:
- When the KMS rotates a KEK, it creates a new KEK version and marks it as the active key for future wrapping operations.
- Older KEK versions remain available for unwrapping previously wrapped DEKs, so existing data can still be decrypted without re‑encryption.
Final Thoughts
Envelope encryption provides a strong, scalable way to protect sensitive data while simplifying key management. It underpins most modern cloud KMS solutions.
You can find the complete code example in the repository below:
Thanks for reading!
Credits
- AES/GCM encryption in Java
- Envelope encryption
- Hybrid cryptosystem – Envelope encryption (Wikipedia)
Cover photo by Mauro Sbicego on Unsplash.