Status: This is an advanced tutorial, work in progress.
Protect persistent data with encryption and detect rollback attacks.
TEE apps often need persistent storage (databases, files). But storage lives outside the TEE:
┌─────────────────┐ ┌─────────────────┐
│ TEE │────▶│ External DB │
│ (trusted) │ │ (untrusted) │
└─────────────────┘ └─────────────────┘
An attacker (or malicious operator) could:
- Read data — if stored unencrypted
- Modify data — if no integrity checks
- Rollback data — restore old state to replay transactions
Use KMS-derived keys to encrypt data at rest:
import { DstackClient } from '@phala/dstack-sdk'
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
const client = new DstackClient()
const { key } = await client.getKey('/encryption/db')
function encrypt(plaintext) {
const iv = randomBytes(16)
const cipher = createCipheriv('aes-256-gcm', key.slice(0, 32), iv)
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()])
const tag = cipher.getAuthTag()
return { iv, encrypted, tag }
}This ensures only this TEE app can decrypt the data.
AES-GCM provides authenticated encryption — tampering is detected:
function decrypt({ iv, encrypted, tag }) {
const decipher = createDecipheriv('aes-256-gcm', key.slice(0, 32), iv)
decipher.setAuthTag(tag)
return Buffer.concat([decipher.update(encrypted), decipher.final()])
// Throws if data was tampered
}Encryption and integrity don't prevent rollback attacks. If an attacker restores an old database snapshot, the TEE can't tell.
| Approach | Trade-off |
|---|---|
| Monotonic counter | Requires trusted counter storage (e.g., on-chain) |
| Light client checkpoint | Anchor state to blockchain block number |
| Merkle tree on-chain | Store state root on-chain, verify freshness |
| Multi-party replication | Multiple TEEs cross-check state |
// After each state change, post the root hash on-chain
const stateRoot = computeMerkleRoot(database)
await contract.updateStateRoot(stateRoot)
// On startup, verify current state matches on-chain root
const onChainRoot = await contract.getStateRoot()
if (computeMerkleRoot(database) !== onChainRoot) {
throw new Error('State rollback detected')
}Even with encryption, access patterns leak information:
- Which records are accessed
- Access frequency and timing
- Size of records
Mitigations:
- ORAM (Oblivious RAM) — expensive but hides access patterns
- Dummy accesses — add noise
- Batch operations — access patterns less granular
- 07-lightclient: Use light client for freshness anchoring
- 08-extending-appauth: Custom authorization policies