Specification
Capsule is an open standard for client-side article encryption using envelope encryption. It enables secure content delivery without requiring server-side authentication or permission systems.
Architecture Overview
Capsule uses envelope encryption, combining the efficiency of symmetric encryption (AES-256-GCM) with the key management benefits of asymmetric encryption (RSA-OAEP).
Encryption Flow
1. Server-Side Pre-Encryption
Content is encrypted at build time or when published using a Data Encryption Key (DEK) associated with a subscription tier or individual article.
// Generate or retrieve DEK for subscription tier
const dek = getSubscriptionKey("premium"); // 256-bit AES key
// Generate unique IV for this article
const iv = randomBytes(12); // 96 bits for GCM
// Encrypt content with AES-256-GCM
const cipher = createCipheriv("aes-256-gcm", dek, iv);
const encrypted = Buffer.concat([
cipher.update(content),
cipher.final(),
cipher.getAuthTag() // 128-bit authentication tag
]);
// Result: { encryptedContent, iv, tier }2. HTML Embedding
Encrypted content is embedded directly in the server-rendered HTML, enabling offline access and browser caching.
<template
id="encrypted-article-123"
data-encrypted-content="base64-encoded-ciphertext"
data-iv="base64-encoded-iv"
data-tier="premium"
/>3. Client Key Generation
On first visit, the browser generates an RSA-OAEP key pair using the Web Crypto API. The private key is stored in IndexedDB with extractable: false, ensuring it cannot be exported or accessed outside the crypto engine.
const keyPair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
hash: "SHA-256",
},
true, // extractable for public key export
["wrapKey", "unwrapKey"]
);
// Store in IndexedDB
await indexedDB.put("keypair", keyPair);4. Key Exchange Protocol
When unlocking content, the client sends its public key to the server. The server wraps the DEK with the client's public key and returns it.
// Client → Server
POST /api/unlock
{
"tier": "premium",
"publicKey": "base64-encoded-spki-public-key"
}
// Server → Client
{
"encryptedDek": "base64-rsa-oaep-wrapped-dek",
"tier": "premium"
}5. Client-Side Decryption
The client unwraps the DEK using its private key, then decrypts the content using AES-GCM. The unwrapped DEK is cached in memory for the session.
// Unwrap DEK with private key
const dek = await crypto.subtle.unwrapKey(
"raw",
encryptedDek,
keyPair.privateKey,
{ name: "RSA-OAEP" },
{ name: "AES-GCM", length: 256 },
false, // non-extractable
["decrypt"]
);
// Cache DEK for this tier
dekCache.set(tier, dek);
// Decrypt content
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
dek,
encryptedContent
);Multiple DEK Models
Subscription-Based (Recommended)
One DEK per subscription tier. All articles in the tier use the same key. One key exchange unlocks all content in that tier.
- ✅ Minimal server requests
- ✅ Offline access after first unlock
- ✅ Fast subsequent article access
- ⚠️ Revoking access requires re-encrypting all articles
Per-Article
Unique DEK for each article. Requires server request per article.
- ✅ Fine-grained access control
- ✅ Easy revocation (just delete the DEK)
- ⚠️ More server requests
- ⚠️ Less efficient for many articles
Hybrid
Combination of both. Some articles use tier-level DEKs, others use unique DEKs.
Security Considerations
Private Key Protection
Private keys must be stored with extractable: false in the Web Crypto API. This prevents JavaScript from accessing the raw key material.
DEK Storage
Server-side DEKs should be stored in a secure key management system (KMS) in production. Never hardcode DEKs in source code.
Transport Security
The key exchange endpoint must use HTTPS. While the wrapped DEK is encrypted, HTTPS prevents MITM attacks on the public key exchange.
IV Uniqueness
Each encrypted article must use a unique initialization vector (IV). Never reuse IVs with the same DEK, as this breaks AES-GCM security.
Implementation Checklist
- ✅ AES-256-GCM for content encryption
- ✅ RSA-OAEP with SHA-256 for key wrapping
- ✅ Unique 96-bit IV per encrypted content
- ✅ 128-bit authentication tag (GCM)
- ✅ Private keys stored with extractable: false
- ✅ HTTPS for key exchange endpoint
- ✅ Proper error handling and validation