nimir /
BioWorkContact
Get in touch

© 2026 Nimir Khan. All rights reserved.

  1. CryptoCloud
  2. Cryptography
Cryptographic Implementation

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).

1

Generate Random DEK

256-bit AES key generated client-side for each file

2

Encrypt File with DEK

File encrypted with AES-256-GCM using random DEK

3

Wrap DEK with KEK

DEK encrypted with master key (derived from password) or RSA public key

4

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?

Authenticated Encryption: GCM provides both encryption and a 128-bit authentication tag, preventing tampering
Performance: Hardware acceleration (AES-NI) makes it extremely fast
Single-pass: Encrypts and authenticates in one operation
Standard: NIST approved, widely supported in Web Crypto API

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

  1. →User enters password
  2. →SHA-256 pre-hash → PBKDF2 (100k iterations) → Master Key
  3. →Generate RSA-4096 key pair
  4. →Wrap private key with master key (AES-GCM)
  5. →Send to server: bcrypt(SHA-256(password)), encrypted private key, public key

2. Login

  1. →User enters password
  2. →SHA-256 → send to server for bcrypt comparison + JWT issuance
  3. →Server returns encrypted private key
  4. →Client re-derives master key (PBKDF2 with same password)
  5. →Unwrap RSA private key with master key
  6. →Store keys in memory for session

3. File Upload

  1. →Generate random 256-bit AES key (file DEK)
  2. →Encrypt file with AES-256-GCM (DEK + random IV)
  3. →Wrap DEK with master key (AES-GCM)
  4. →Upload encrypted file to S3
  5. →Store metadata + wrapped DEK + IV in MongoDB

4. File Sharing

  1. →Unwrap file DEK with owner's master key
  2. →Fetch recipient's RSA public key from server
  3. →Wrap file DEK with recipient's RSA public key (RSA-OAEP)
  4. →Send wrapped DEK to server, server stores in shares collection
  5. →Recipient unwraps with their RSA private key when accessing

5. File Download

  1. →Request pre-signed S3 URL from server
  2. →Fetch encrypted file from S3
  3. →Fetch wrapped DEK + IV from MongoDB (via API)
  4. →Unwrap DEK (with master key if owner, or RSA private key if shared)
  5. →Decrypt file with AES-256-GCM (DEK + IV)
  6. →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.

Continue Reading

← Security Model

Threat analysis, attack vectors, security guarantees

Engineering Story →

How I built CryptoCloud, mistakes made, lessons learned