1How Encryption Works
Step 1: Key Derivation (TEE)
// Client requests key from TEE
const wrappedKey = await tee.getKey('user.near/private');
// Inside TEE:
// 1. Hardware seed (TEE-protected)
// 2. CKD derive: seed + group_id → AES-256 key
// 3. Wrap key with user's public key
// 4. Return wrapped key + attestation✓ Key derived in hardware-protected enclave
✓ Nobody can access the hardware seed
Step 2: Key Unwrapping (Client)
// Client unwraps the key
const key = await crypto.subtle.unwrapKey(
'raw',
wrappedKey,
userPrivateKey, // User's private key
'RSA-OAEP',
{ name: 'AES-GCM' },
false, // Not extractable
['encrypt', 'decrypt']
);✓ Only user can unwrap their keys
⚠ Key stored in browser memory (client-side risk)
Step 3: Encrypt (Client)
async encryptValue(plaintext: string): Promise<string> {
// 1. Get AES key
const key = await this.getOrCreateKey();
// 2. Generate random IV (12 bytes)
const iv = crypto.getRandomValues(new Uint8Array(12));
// 3. Encrypt with AES-256-GCM
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
new TextEncoder().encode(plaintext)
);
// 4. Serialize: enc:AES256:keyId:base64(iv + ciphertext)
return this.serializeEncrypted(iv, encrypted);
}✓ AES-256-GCM authenticated encryption
✓ Random IV ensures uniqueness
✓ 128-bit authentication tag
Step 4: Storage
// Store ciphertext only
await this.storage.set('my-secret', encryptedValue);
// Stored value:
// enc:AES256:01560100ddd39635:hM60OnrQ6W2yUKX0QvTCo8iSz2f4g3t...✓ Only ciphertext stored
✓ No plaintext in storage
✓ No keys in storage
2Threat Model
Protected Against
- ✓Network observers — ISP, WiFi, proxies only see ciphertext
- ✓Storage providers — FastKV/blockchain only see ciphertext
- ✓TEE operator — OutLayer never sees plaintext or keys
- ✓Other users — Can't access your group's keys
- ✓Key theft — Keys wrapped with user's public key
NOT Protected Against
- ✗XSS attacks — Can steal keys from browser memory
- ✗Malicious extensions — Can intercept plaintext
- ✗Browser exploits — Memory dumps can expose keys
- ✗Compromised client — Modified encryption code
- ✗Key reuse — If key leaked, all data compromised
3Client-Side Encryption Trade-offs
Why Client-Side?
Requests are public — If you sent plaintext to the TEE, anyone observing the network would see it before encryption.
// ❌ If encryption happened in TEE:
POST /encrypt
{
"plaintext": "my secret" ← EXPOSED ON NETWORK!
}
// ✅ Client-side encryption:
POST /storage
{
"value": "enc:AES256:..." ← ONLY CIPHERTEXT
}| Aspect | Client-Side | TEE-Side |
|---|---|---|
| Network exposure | ✅ Ciphertext only | ❌ Plaintext exposed |
| Key security | ⚠️ Browser memory | ✅ TEE-protected |
| XSS protection | ❌ Can steal keys | ✅ Can't access TEE |
| Forward secrecy | ❌ No | ✅ Possible |
| Practicality | ✅ Works everywhere | ❌ Violates constraint |
4Best Practices
Do
- ✓ Use Content Security Policy (CSP)
- ✓ Implement Subresource Integrity (SRI)
- ✓ Rotate keys regularly
- ✓ Audit key access logs
- ✓ Use short-lived sessions
- ✓ Monitor for suspicious activity
- ✓ Keep browser/extensions updated
- ✓ Use HTTPS everywhere
Don't
- ✗ Store keys in localStorage
- ✗ Reuse IVs with same key
- ✗ Share groups between users
- ✗ Skip attestation verification
- ✗ Use in insecure contexts (HTTP)
- ✗ Trust unverified extensions
- ✗ Ignore security warnings
- ✗ Log plaintext or keys
5Encryption Parameters
| Parameter | Value | Reason |
|---|---|---|
| Algorithm | AES-256-GCM | Authenticated encryption |
| Key Size | 256 bits (32 bytes) | Military-grade security |
| IV Size | 96 bits (12 bytes) | Optimal for GCM |
| Tag Length | 128 bits (16 bytes) | Maximum integrity |
| Key Derivation | CKD (HKDF-SHA256) | Hardware-backed |
| Wrapping | RSA-OAEP | Secure key transport |
6Summary
TEE Protection
Keys derived in hardware enclave with attestation
Client Encryption
AES-256-GCM in browser to protect data in transit
Trade-offs
Client-side risks exist but are unavoidable given public network