| name | envelope-encryption |
| description | Envelope encryption patterns with X25519 for agent memories. Use when implementing key generation, DEK management, encrypting/decrypting records, key rotation, or sharing encrypted data between agents. Triggers on encryption, X25519, DEK, envelope encryption, key exchange, crypto. |
Envelope Encryption
Every memory is encrypted with a per-record Data Encryption Key (DEK). The DEK is encrypted with the agent's public key.
Why Envelope Encryption?
- Key rotation — Rotate agent key without re-encrypting all records
- Sharing — Re-encrypt DEK for recipient without touching content
- Performance — Symmetric DEK is fast; asymmetric only for DEK
Key Hierarchy
Agent Identity (X25519 keypair)
└── Record 1: DEK₁ → Encrypted content
└── Record 2: DEK₂ → Encrypted content
└── Record 3: DEK₃ → Encrypted content
└── Shared with Bob: DEK₃ encrypted for Bob's public key
Agent Identity
interface AgentIdentity {
did: string
signingKey: CryptoKeyPair
encryptionKey: X25519KeyPair
createdAt: number
rotatedAt?: number
}
async function createIdentity(doId: string): Promise<AgentIdentity> {
const encryptionKey = await crypto.subtle.generateKey(
{ name: 'X25519' },
true,
['deriveBits']
)
const signingKey = await crypto.subtle.generateKey(
{ name: 'Ed25519' },
true,
['sign', 'verify']
)
return {
did: `did:cf:${doId}`,
signingKey,
encryptionKey,
createdAt: Date.now()
}
}
Encrypted Record Schema
interface EncryptedRecord {
id: string
collection: string
ciphertext: Uint8Array
encryptedDek: Uint8Array
nonce: Uint8Array
public: boolean
recipients?: string[]
createdAt: string
}
Encrypt on Store
async function encryptRecord(
content: unknown,
identity: AgentIdentity
): Promise<EncryptedRecord> {
const dek = crypto.getRandomValues(new Uint8Array(32))
const nonce = crypto.getRandomValues(new Uint8Array(12))
const plaintext = new TextEncoder().encode(JSON.stringify(content))
const key = await crypto.subtle.importKey(
'raw', dek, 'AES-GCM', false, ['encrypt']
)
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: nonce },
key,
plaintext
)
const encryptedDek = await encryptForPublicKey(dek, identity.encryptionKey.publicKey)
return {
id: generateTid(),
collection: content.$type,
ciphertext: new Uint8Array(ciphertext),
encryptedDek,
nonce,
public: false,
createdAt: new Date().toISOString()
}
}
Decrypt on Retrieve
async function decryptRecord(
record: EncryptedRecord,
identity: AgentIdentity
): Promise<unknown> {
if (record.public) {
return JSON.parse(new TextDecoder().decode(record.ciphertext))
}
const dek = await decryptWithPrivateKey(
record.encryptedDek,
identity.encryptionKey.privateKey
)
const key = await crypto.subtle.importKey(
'raw', dek, 'AES-GCM', false, ['decrypt']
)
const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: record.nonce },
key,
record.ciphertext
)
return JSON.parse(new TextDecoder().decode(plaintext))
}
Share with Another Agent
Re-encrypt the DEK for the recipient's public key:
async function shareRecord(
recordId: string,
recipientDid: string,
identity: AgentIdentity,
db: D1Database
): Promise<void> {
const record = await db.prepare(
'SELECT * FROM records WHERE id = ?'
).bind(recordId).first()
const dek = await decryptWithPrivateKey(
record.encrypted_dek,
identity.encryptionKey.privateKey
)
const recipientKey = await resolvePublicKey(recipientDid)
const sharedDek = await encryptForPublicKey(dek, recipientKey)
await db.prepare(`
INSERT INTO shared_records (record_id, recipient_did, encrypted_dek)
VALUES (?, ?, ?)
`).bind(recordId, recipientDid, sharedDek).run()
}
Make Public
Convert encrypted record to plaintext:
async function makePublic(
recordId: string,
identity: AgentIdentity,
db: D1Database
): Promise<void> {
const record = await getRecord(recordId, identity, db)
const content = await decryptRecord(record, identity)
await db.prepare(`
UPDATE records
SET ciphertext = ?, encrypted_dek = NULL, public = TRUE
WHERE id = ?
`).bind(
new TextEncoder().encode(JSON.stringify(content)),
recordId
).run()
}
Key Rotation
Rotate agent key without re-encrypting all records:
async function rotateKey(
identity: AgentIdentity,
storage: DurableObjectStorage
): Promise<AgentIdentity> {
const newKey = await crypto.subtle.generateKey(
{ name: 'X25519' },
true,
['deriveBits']
)
const records = await storage.list({ prefix: 'record:' })
for (const [key, record] of records) {
const dek = await decryptWithPrivateKey(
record.encryptedDek,
identity.encryptionKey.privateKey
)
record.encryptedDek = await encryptForPublicKey(dek, newKey.publicKey)
await storage.put(key, record)
}
const newIdentity = {
...identity,
encryptionKey: newKey,
rotatedAt: Date.now()
}
await storage.put('identity', newIdentity)
return newIdentity
}
Cloudflare Web Crypto Notes
Cloudflare Workers support Web Crypto API with some specifics:
const key = await crypto.subtle.generateKey(
{ name: 'X25519' },
true,
['deriveBits']
)
const sharedSecret = await crypto.subtle.deriveBits(
{ name: 'X25519', public: recipientPublicKey },
myPrivateKey,
256
)
const encKey = await crypto.subtle.deriveKey(
{ name: 'HKDF', salt, info, hash: 'SHA-256' },
await crypto.subtle.importKey('raw', sharedSecret, 'HKDF', false, ['deriveKey']),
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
)
References