Cryptography Deep Dive
How CryptoCloud implements client-side encryption using AES-256-GCM, RSA-4096-OAEP, and PBKDF2 with the Web Crypto API.
Cryptographic Stack
AES-256-GCM
Symmetric encryption for file contents. Fast, authenticated, provides confidentiality + integrity.
RSA-4096-OAEP
Asymmetric encryption for key wrapping. Enables secure sharing without key exchange.
PBKDF2-SHA256
Key derivation from password. 100,000 iterations slow brute force attacks.
Envelope Encryption Pattern
CryptoCloud uses envelope encryption: each file gets a unique AES key (Data Encryption Key), which is then encrypted with RSA or master key (Key Encryption Key).
Generate Random DEK
256-bit AES key generated client-side for each file
Encrypt File with DEK
File encrypted with AES-256-GCM using random DEK
Wrap DEK with KEK
DEK encrypted with master key (derived from password) or RSA public key
Upload Both Ciphertexts
Encrypted file → S3, Wrapped DEK → MongoDB
Why Envelope Encryption?
- • Performance: AES is 1000x faster than RSA for large files
- • Flexibility: Can wrap same DEK with multiple RSA keys for sharing
- • Key rotation: Re-wrap DEK without re-encrypting entire file
- • Size limits: RSA can only encrypt small payloads (190 bytes for RSA-4096-OAEP)
Master Key Derivation (PBKDF2)
The master key is derived from the user's password using PBKDF2-SHA256 with 100,000 iterations. This key is used to encrypt/decrypt the user's RSA private key and file DEKs.
Implementation
async function deriveMasterKey(password: string): Promise<CryptoKey> {
const encoder = new TextEncoder();
// 1. SHA-256 pre-hash (prevents bcrypt truncation at 72 chars)
const passwordBuffer = encoder.encode(password);
const preHash = await crypto.subtle.digest("SHA-256", passwordBuffer);
// 2. Import pre-hashed password as key material
const keyMaterial = await crypto.subtle.importKey(
"raw",
preHash,
{ name: "PBKDF2" },
false,
["deriveKey"]
);
// 3. Derive 256-bit AES key with PBKDF2
const salt = encoder.encode("static-salt-CHANGE-ME"); // ⚠️ Should be random per-user
const masterKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: salt,
iterations: 100000,
hash: "SHA-256"
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false, // Not extractable
["encrypt", "decrypt", "wrapKey", "unwrapKey"]
);
return masterKey;
}✅ Strong Points
- • 100k iterations = ~100ms computation time
- • SHA-256 pre-hash prevents bcrypt truncation
- • Master key stays in browser memory only
- • Non-extractable key (can't be exported)
⚠️ Known Issues
- • Static salt enables rainbow tables
- • Should use random salt per user
- • PBKDF2 less resistant than Argon2 to GPUs
- • No memory-hardness (unlike Argon2)
File Encryption (AES-256-GCM)
Each file is encrypted with a unique 256-bit AES key in GCM mode (Galois/Counter Mode). GCM provides both confidentiality and authenticity.
Encryption Flow
async function encryptFile(file: File): Promise<EncryptedFile> {
// 1. Generate random 256-bit AES key (DEK)
const fileKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true, // Extractable (need to wrap it later)
["encrypt", "decrypt"]
);
// 2. Generate random 96-bit IV (nonce)
const iv = crypto.getRandomValues(new Uint8Array(12));
// 3. Read file as ArrayBuffer
const fileBuffer = await file.arrayBuffer();
// 4. Encrypt file with AES-GCM
const encryptedBuffer = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv,
tagLength: 128 // 128-bit authentication tag
},
fileKey,
fileBuffer
);
// 5. Export file key for wrapping
const exportedKey = await crypto.subtle.exportKey("raw", fileKey);
return {
ciphertext: new Uint8Array(encryptedBuffer),
iv: iv,
fileKey: new Uint8Array(exportedKey), // Will be wrapped before storage
authTag: encryptedBuffer.slice(-16) // Last 16 bytes (GCM appends tag)
};
}Why AES-GCM?
Key Wrapping (RSA-4096-OAEP)
File DEKs are wrapped (encrypted) using RSA-4096 with OAEP padding (Optimal Asymmetric Encryption Padding). This enables secure sharing without key exchange.
RSA Key Generation
async function generateRSAKeyPair(masterKey: CryptoKey): Promise<KeyPair> {
// 1. Generate 4096-bit RSA key pair
const keyPair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
hash: "SHA-256"
},
true, // Extractable
["encrypt", "decrypt", "wrapKey", "unwrapKey"]
);
// 2. Export public key (stored in DB as base64)
const publicKeyJWK = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
// 3. Wrap (encrypt) private key with master key
const wrappedPrivateKey = await crypto.subtle.wrapKey(
"jwk",
keyPair.privateKey,
masterKey,
{
name: "AES-GCM",
iv: crypto.getRandomValues(new Uint8Array(12))
}
);
return {
publicKey: JSON.stringify(publicKeyJWK),
encryptedPrivateKey: new Uint8Array(wrappedPrivateKey)
};
}Wrapping File Key for Sharing
async function wrapFileKeyForRecipient(
fileKey: Uint8Array,
recipientPublicKeyJWK: string
): Promise<Uint8Array> {
// 1. Import recipient's RSA public key
const publicKey = await crypto.subtle.importKey(
"jwk",
JSON.parse(recipientPublicKeyJWK),
{ name: "RSA-OAEP", hash: "SHA-256" },
false,
["wrapKey"]
);
// 2. Import file key as CryptoKey
const fileKeyObj = await crypto.subtle.importKey(
"raw",
fileKey,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
// 3. Wrap file key with recipient's RSA public key
const wrappedKey = await crypto.subtle.wrapKey(
"raw",
fileKeyObj,
publicKey,
{ name: "RSA-OAEP" }
);
return new Uint8Array(wrappedKey);
}RSA-4096 vs RSA-2048
CryptoCloud uses 4096-bit keys instead of the more common 2048-bit for future-proofing:
- • RSA-2048: Secure until ~2030 (NIST estimate)
- • RSA-4096: Secure until ~2040+
- • Tradeoff: 8x slower encryption/decryption, but only used for small keys
- • Max payload: 190 bytes for RSA-4096-OAEP-SHA256 (plenty for 32-byte AES keys)
Complete Key Lifecycle
1. Registration
- →User enters password
- →SHA-256 pre-hash → PBKDF2 (100k iterations) → Master Key
- →Generate RSA-4096 key pair
- →Wrap private key with master key (AES-GCM)
- →Send to server: bcrypt(SHA-256(password)), encrypted private key, public key
2. Login
- →User enters password
- →SHA-256 → send to server for bcrypt comparison + JWT issuance
- →Server returns encrypted private key
- →Client re-derives master key (PBKDF2 with same password)
- →Unwrap RSA private key with master key
- →Store keys in memory for session
3. File Upload
- →Generate random 256-bit AES key (file DEK)
- →Encrypt file with AES-256-GCM (DEK + random IV)
- →Wrap DEK with master key (AES-GCM)
- →Upload encrypted file to S3
- →Store metadata + wrapped DEK + IV in MongoDB
4. File Sharing
- →Unwrap file DEK with owner's master key
- →Fetch recipient's RSA public key from server
- →Wrap file DEK with recipient's RSA public key (RSA-OAEP)
- →Send wrapped DEK to server, server stores in shares collection
- →Recipient unwraps with their RSA private key when accessing
5. File Download
- →Request pre-signed S3 URL from server
- →Fetch encrypted file from S3
- →Fetch wrapped DEK + IV from MongoDB (via API)
- →Unwrap DEK (with master key if owner, or RSA private key if shared)
- →Decrypt file with AES-256-GCM (DEK + IV)
- →Download plaintext file to user's device
Security Properties
Guarantees
- • Confidentiality: AES-256 (128-bit security)
- • Integrity: GCM authentication tags
- • Forward secrecy: Unique key per file
- • Key isolation: Keys never sent to server
- • Non-repudiation: Signatures (not implemented)
Attack Resistance
- • Brute force: 100k PBKDF2 iterations
- • Rainbow tables: Partially (static salt)
- • Chosen-ciphertext: OAEP padding
- • Timing attacks: Constant-time crypto
- • Replay attacks: Random IVs + nonces
Performance Characteristics
AES-GCM
~1-2 GB/s throughput with hardware acceleration
10 MB file encrypts in ~10ms on modern CPU
RSA-4096
~5-10ms per encryption operation
Only used for 32-byte keys, not files
PBKDF2
~100ms for 100k iterations
One-time cost at login/registration
Future Improvements
🔹 Per-User PBKDF2 Salt
Generate random salt per user, store in database, send during login. Prevents rainbow table attacks.
🔹 Argon2id Instead of PBKDF2
Memory-hard KDF resistant to GPU/ASIC attacks. Not yet in Web Crypto API (requires WebAssembly).
🔹 ChaCha20-Poly1305 Alternative
Software-optimized cipher for devices without AES-NI. Better performance on mobile.
🔹 Metadata Encryption
Encrypt filenames, sizes, folder structure. Tradeoff: lose server-side search/sorting.
🔹 Ed25519 Digital Signatures
Sign file hashes to prove ownership and detect tampering. Currently no non-repudiation.