? Back to Blog

How to Implement Client-Side Encryption in JavaScript: Complete Guide 2025

Brendan G · 2026-02-01

How to Implement Client-Side Encryption in JavaScript: Complete Guide 2025

Client-side encryption diagram showing data encrypted in browser before upload

Introduction

Client-side encryption is a critical security feature that ensures data is encrypted on the user's device before it ever leaves their computer. This approach provides true privacy protection, as the service provider never has access to unencrypted data.

This comprehensive guide will walk you through implementing client-side encryption in JavaScript, covering everything from basic concepts to production-ready code.

Meta Description: Learn how to implement client-side encryption in JavaScript using the Web Crypto API. Complete guide with code examples for AES-256, key derivation, and zero-knowledge file encryption.

Keywords: client-side encryption, JavaScript encryption, Web Crypto API, zero-knowledge encryption, AES-256, browser encryption, privacy-first development

Why Client-Side Encryption Matters

The Problem with Server-Side Encryption

Traditional server-side encryption has a fundamental flaw: your data is transmitted to the server in plaintext, where it's then encrypted. This means:

  • The service provider can see your data
  • Data is vulnerable during transmission
  • Government requests can be fulfilled
  • Data breaches expose unencrypted information

The Solution: Client-Side Encryption

Client-side encryption solves these issues by:

  • Encrypting data before transmission
  • Ensuring the service provider never sees plaintext
  • Providing true zero-knowledge architecture
  • Meeting GDPR and CCPA compliance requirements

Encryption types comparison: symmetric vs asymmetric encryption

Understanding Encryption Basics

Symmetric vs. Asymmetric Encryption

Symmetric Encryption: - Uses the same key for encryption and decryption - Faster and more efficient - Best for file encryption - Examples: AES-256, ChaCha20

Asymmetric Encryption: - Uses a public key for encryption and private key for decryption - Slower but more secure for key exchange - Best for sharing encrypted data - Examples: RSA, ECC

Key Derivation Functions (KDFs)

KDFs transform passwords into encryption keys:

  • PBKDF2: Password-Based Key Derivation Function 2
  • Argon2: Modern, memory-hard KDF (recommended)
  • scrypt: Memory-hard KDF

Setting Up Your Development Environment

Required Libraries

For this guide, we'll use the Web Crypto API (built into modern browsers) and the crypto-js library for additional functionality:

npm install crypto-js

Browser Compatibility

The Web Crypto API is supported in: - Chrome 37+ - Firefox 34+ - Safari 11+ - Edge 12+

Implementation: Basic Client-Side Encryption

Step 1: Generate Encryption Key

// Generate a random encryption key using Web Crypto API
async function generateEncryptionKey() {
    const key = await crypto.subtle.generateKey(
        {
            name: "AES-GCM",
            length: 256,
        },
        true, // extractable
        ["encrypt", "decrypt"]
    );
    return key;
}

// Alternative: Derive key from password
async function deriveKeyFromPassword(password, salt) {
    const encoder = new TextEncoder();
    const passwordKey = await crypto.subtle.importKey(
        "raw",
        encoder.encode(password),
        "PBKDF2",
        false,
        ["deriveBits", "deriveKey"]
    );

    const key = await crypto.subtle.deriveKey(
        {
            name: "PBKDF2",
            salt: salt,
            iterations: 100000,
            hash: "SHA-256",
        },
        passwordKey,
        {
            name: "AES-GCM",
            length: 256,
        },
        false,
        ["encrypt", "decrypt"]
    );

    return key;
}

Step 2: Encrypt Data

async function encryptData(data, key) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);

    // Generate random IV (Initialization Vector)
    const iv = crypto.getRandomValues(new Uint8Array(12));

    // Encrypt the data
    const encryptedData = await crypto.subtle.encrypt(
        {
            name: "AES-GCM",
            iv: iv,
        },
        key,
        dataBuffer
    );

    // Combine IV and encrypted data
    const combined = new Uint8Array(iv.length + encryptedData.byteLength);
    combined.set(iv, 0);
    combined.set(new Uint8Array(encryptedData), iv.length);

    // Convert to base64 for storage/transmission
    return btoa(String.fromCharCode(...combined));
}

Step 3: Decrypt Data

async function decryptData(encryptedBase64, key) {
    // Convert from base64
    const combined = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));

    // Extract IV and encrypted data
    const iv = combined.slice(0, 12);
    const encryptedData = combined.slice(12);

    // Decrypt
    const decryptedData = await crypto.subtle.decrypt(
        {
            name: "AES-GCM",
            iv: iv,
        },
        key,
        encryptedData
    );

    // Convert back to string
    const decoder = new TextDecoder();
    return decoder.decode(decryptedData);
}

Implementation: File Encryption

Encrypting Files

async function encryptFile(file, password) {
    // Read file as ArrayBuffer
    const fileBuffer = await file.arrayBuffer();

    // Generate salt for key derivation
    const salt = crypto.getRandomValues(new Uint8Array(16));

    // Derive key from password
    const key = await deriveKeyFromPassword(password, salt);

    // Generate IV
    const iv = crypto.getRandomValues(new Uint8Array(12));

    // Encrypt file
    const encryptedData = await crypto.subtle.encrypt(
        {
            name: "AES-GCM",
            iv: iv,
        },
        key,
        fileBuffer
    );

    // Combine salt, IV, and encrypted data
    const combined = new Uint8Array(
        salt.length + iv.length + encryptedData.byteLength
    );
    combined.set(salt, 0);
    combined.set(iv, salt.length);
    combined.set(new Uint8Array(encryptedData), salt.length + iv.length);

    // Create blob for download
    return new Blob([combined], { type: "application/octet-stream" });
}

Decrypting Files

async function decryptFile(encryptedBlob, password) {
    // Read encrypted blob
    const encryptedBuffer = await encryptedBlob.arrayBuffer();
    const encryptedArray = new Uint8Array(encryptedBuffer);

    // Extract salt, IV, and encrypted data
    const salt = encryptedArray.slice(0, 16);
    const iv = encryptedArray.slice(16, 28);
    const encryptedData = encryptedArray.slice(28);

    // Derive key from password
    const key = await deriveKeyFromPassword(password, salt);

    // Decrypt
    const decryptedData = await crypto.subtle.decrypt(
        {
            name: "AES-GCM",
            iv: iv,
        },
        key,
        encryptedData
    );

    // Return as blob
    return new Blob([decryptedData]);
}

Advanced: Streaming Encryption for Large Files

For large files, streaming encryption prevents memory issues:

class StreamingEncryptor {
    constructor(password) {
        this.password = password;
        this.salt = crypto.getRandomValues(new Uint8Array(16));
        this.iv = crypto.getRandomValues(new Uint8Array(12));
        this.key = null;
        this.chunkSize = 64 * 1024; // 64KB chunks
    }

    async initialize() {
        this.key = await deriveKeyFromPassword(this.password, this.salt);
    }

    async encryptStream(file) {
        await this.initialize();

        const reader = file.stream().getReader();
        const encryptedChunks = [];

        // Write salt and IV first
        encryptedChunks.push(this.salt);
        encryptedChunks.push(this.iv);

        let chunkNumber = 0;

        while (true) {
            const { done, value } = await reader.read();
            if (done) break;

            // Encrypt chunk
            const encryptedChunk = await crypto.subtle.encrypt(
                {
                    name: "AES-GCM",
                    iv: this.incrementIV(chunkNumber),
                },
                this.key,
                value
            );

            encryptedChunks.push(new Uint8Array(encryptedChunk));
            chunkNumber++;
        }

        // Combine all chunks
        const totalLength = encryptedChunks.reduce(
            (sum, chunk) => sum + chunk.length,
            0
        );
        const combined = new Uint8Array(totalLength);
        let offset = 0;
        for (const chunk of encryptedChunks) {
            combined.set(chunk, offset);
            offset += chunk.length;
        }

        return new Blob([combined]);
    }

    incrementIV(chunkNumber) {
        const newIV = new Uint8Array(this.iv);
        // Increment IV for each chunk (simplified - use proper counter mode in production)
        for (let i = 0; i < chunkNumber; i++) {
            // Increment the last bytes
            for (let j = newIV.length - 1; j >= 0; j--) {
                if (++newIV[j]) break;
            }
        }
        return newIV;
    }
}

Security Best Practices

1. Use Strong Key Derivation

// Use Argon2 for better security (requires library)
// Or increase PBKDF2 iterations
const ITERATIONS = 100000; // Minimum recommended
const ITERATIONS_SECURE = 600000; // More secure

2. Secure Random Number Generation

// Always use crypto.getRandomValues() for randomness
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
// NEVER use Math.random() for cryptographic purposes

3. Proper IV Management

// Always use unique IVs for each encryption
// Never reuse IVs with the same key
const iv = crypto.getRandomValues(new Uint8Array(12));

4. Secure Password Handling

// Clear passwords from memory when done
function clearPassword(password) {
    // Overwrite password in memory (simplified)
    password = null;
    // In production, use secure memory management
}

5. Validate Encryption Results

async function validateEncryption(original, encrypted, key) {
    const decrypted = await decryptData(encrypted, key);
    return decrypted === original;
}

Error Handling

async function encryptDataSafe(data, key) {
    try {
        return await encryptData(data, key);
    } catch (error) {
        if (error.name === "OperationError") {
            console.error("Encryption failed:", error);
            throw new Error("Failed to encrypt data");
        }
        throw error;
    }
}

async function decryptDataSafe(encrypted, key) {
    try {
        return await decryptData(encrypted, key);
    } catch (error) {
        if (error.name === "OperationError") {
            console.error("Decryption failed - incorrect key or corrupted data");
            throw new Error("Failed to decrypt data");
        }
        throw error;
    }
}

Complete Example: File Encryption Application

class SecureFileEncryptor {
    constructor() {
        this.key = null;
    }

    async generateKey() {
        this.key = await generateEncryptionKey();
        return this.key;
    }

    async encryptFile(file, password) {
        if (!password) {
            throw new Error("Password required for encryption");
        }

        const salt = crypto.getRandomValues(new Uint8Array(16));
        const key = await deriveKeyFromPassword(password, salt);
        const iv = crypto.getRandomValues(new Uint8Array(12));

        const fileBuffer = await file.arrayBuffer();
        const encryptedData = await crypto.subtle.encrypt(
            {
                name: "AES-GCM",
                iv: iv,
            },
            key,
            fileBuffer
        );

        // Package: salt (16) + iv (12) + encrypted data
        const package = new Uint8Array(
            salt.length + iv.length + encryptedData.byteLength
        );
        package.set(salt, 0);
        package.set(iv, salt.length);
        package.set(new Uint8Array(encryptedData), salt.length + iv.length);

        return new Blob([package], { type: "application/octet-stream" });
    }

    async decryptFile(encryptedBlob, password) {
        if (!password) {
            throw new Error("Password required for decryption");
        }

        const buffer = await encryptedBlob.arrayBuffer();
        const array = new Uint8Array(buffer);

        const salt = array.slice(0, 16);
        const iv = array.slice(16, 28);
        const encryptedData = array.slice(28);

        const key = await deriveKeyFromPassword(password, salt);

        try {
            const decryptedData = await crypto.subtle.decrypt(
                {
                    name: "AES-GCM",
                    iv: iv,
                },
                key,
                encryptedData
            );

            return new Blob([decryptedData]);
        } catch (error) {
            throw new Error("Decryption failed - incorrect password or corrupted file");
        }
    }
}

// Usage
const encryptor = new SecureFileEncryptor();

// Encrypt
const fileInput = document.getElementById("fileInput");
const passwordInput = document.getElementById("passwordInput");

fileInput.addEventListener("change", async (e) => {
    const file = e.target.files[0];
    const password = passwordInput.value;

    const encryptedBlob = await encryptor.encryptFile(file, password);
    
    // Download encrypted file
    const url = URL.createObjectURL(encryptedBlob);
    const a = document.createElement("a");
    a.href = url;
    a.download = file.name + ".encrypted";
    a.click();
});

Testing Your Implementation

Unit Tests

describe("Client-Side Encryption", () => {
    test("encrypts and decrypts text correctly", async () => {
        const original = "Hello, World!";
        const password = "test-password-123";
        const salt = crypto.getRandomValues(new Uint8Array(16));
        
        const key = await deriveKeyFromPassword(password, salt);
        const encrypted = await encryptData(original, key);
        const decrypted = await decryptData(encrypted, key);
        
        expect(decrypted).toBe(original);
    });

    test("fails with incorrect password", async () => {
        const file = new Blob(["test content"]);
        const correctPassword = "correct-password";
        const wrongPassword = "wrong-password";

        const encrypted = await encryptFile(file, correctPassword);
        
        await expect(decryptFile(encrypted, wrongPassword)).rejects.toThrow();
    });

    test("generates unique IVs", async () => {
        const data = "test data";
        const key = await generateEncryptionKey();
        
        const encrypted1 = await encryptData(data, key);
        const encrypted2 = await encryptData(data, key);
        
        expect(encrypted1).not.toBe(encrypted2);
    });
});

Performance Considerations

Optimization Tips

  1. Use Web Workers: Move encryption to a worker thread to avoid blocking the UI
  2. Stream Large Files: Use streaming for files over 100MB
  3. Batch Operations: Process multiple files in parallel
  4. Cache Keys: Reuse derived keys when encrypting multiple files with the same password
// Web Worker Example
// encryption-worker.js
self.onmessage = async function(e) {
    const { file, password, salt } = e.data;
    
    const key = await deriveKeyFromPassword(password, salt);
    const encrypted = await encryptFile(file, key);
    
    self.postMessage({ encrypted });
};

// Main thread
const worker = new Worker("encryption-worker.js");
worker.postMessage({ file, password, salt });
worker.onmessage = (e) => {
    const { encrypted } = e.data;
    // Handle encrypted file
};

Common Pitfalls and How to Avoid Them

1. Reusing IVs

Wrong:

const iv = new Uint8Array(12); // Same IV every time!

Correct:

const iv = crypto.getRandomValues(new Uint8Array(12)); // Unique each time

2. Weak Key Derivation

Wrong:

const iterations = 1000; // Too few iterations

Correct:

const iterations = 100000; // Minimum recommended

3. Storing Keys in Plaintext

Wrong:

localStorage.setItem("encryptionKey", key); // Never do this!

Correct:

// Derive key from password each time, never store it
const key = await deriveKeyFromPassword(userPassword, salt);

Conclusion

Implementing client-side encryption in JavaScript provides true privacy protection for your users. By encrypting data before it leaves their device, you ensure that even your service cannot access their information.

Key takeaways:

  • Always use the Web Crypto API for cryptographic operations
  • Derive keys from passwords using PBKDF2 or Argon2
  • Use unique IVs for every encryption operation
  • Implement proper error handling
  • Test thoroughly with various file types and sizes
  • Consider performance for large files

For production applications requiring client-side encryption, consider using services like FileShot.io that implement zero-knowledge encryption, ensuring your files are encrypted in your browser before upload.

FileShot.io zero-knowledge encryption interface

Get Started with Secure File Encryption

Ready to implement secure file encryption in your application? FileShot.io offers zero-knowledge encryption for all users, with files encrypted in your browser before upload. No coding required - just secure file sharing out of the box.

Try FileShot.io Free


Author: Brendan Gray
Published: December 2025
Category: Security, Cryptography, JavaScript Development
Reading Time: 15 minutes
Last Updated: December 2025


Related Topics: - Zero-knowledge encryption - Web Crypto API - File security - Data privacy - JavaScript cryptography - Client-side security

Join the affiliate program and earn 50%. No approvals, no waitlists.