/web-app-features

How to Add File Encryption to Your Web App

Learn how to add file encryption to your web app for enhanced security and data protection in easy steps.

Book a free  consultation
4.9
Clutch rating 🌟
600+
Happy partners
17+
Countries served
190+
Team members
Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

How to Add File Encryption to Your Web App

Adding File Encryption to Your Web Application: A Developer's Guide

 

Why File Encryption Matters

 

Adding encryption to your web app isn't just a security checkbox—it's increasingly becoming table stakes for applications handling sensitive information. When implemented properly, file encryption ensures that even if your storage is compromised, the data remains unreadable without proper decryption keys.

 

Encryption Fundamentals You Should Know

 

The Two Primary Approaches

 

  • Client-side encryption: Files are encrypted in the user's browser before upload. This means the server never sees unencrypted data.
  • Server-side encryption: Files are encrypted after they reach your server but before storage. Your application maintains temporary access to unencrypted files.

 

For most business applications, I recommend a hybrid approach that leverages the security benefits of both methods.

 

Implementation Strategy: The 5-Layer Approach

 

1. Setting Up Your Encryption Infrastructure

 

First, you'll need reliable encryption libraries. For JavaScript environments:

// Client-side implementation using the Web Crypto API
async function encryptFile(file, publicKey) {
  // Generate a random symmetric key for this specific file
  const symmetricKey = await window.crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    true, 
    ["encrypt", "decrypt"]
  );
  
  // Read the file as an ArrayBuffer
  const fileBuffer = await file.arrayBuffer();
  
  // Generate a random initialization vector
  const iv = window.crypto.getRandomValues(new Uint8Array(12));
  
  // Encrypt the file with the symmetric key
  const encryptedFile = await window.crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    symmetricKey,
    fileBuffer
  );
  
  // Export the symmetric key
  const exportedKey = await window.crypto.subtle.exportKey("raw", symmetricKey);
  
  // Encrypt the symmetric key with the recipient's public key
  // (Asymmetric encryption allows secure key transfer)
  const encryptedKey = await encryptKeyWithRSA(exportedKey, publicKey);
  
  return {
    encryptedFile: new Blob([encryptedFile]),
    encryptedKey,
    iv
  };
}

 

For server-side implementations, libraries like Node.js crypto or PHP's sodium extension are excellent choices:

// Server-side encryption in Node.js
const crypto = require('crypto');
const fs = require('fs');

function encryptFile(inputPath, outputPath, key) {
  // Generate a random initialization vector
  const iv = crypto.randomBytes(16);
  
  // Create cipher using AES-256-GCM (authenticated encryption)
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
  
  // Create read/write streams
  const input = fs.createReadStream(inputPath);
  const output = fs.createWriteStream(outputPath);
  
  // Write the IV at the beginning of the encrypted file
  output.write(iv);
  
  // Pipe the input through the cipher to the output
  input.pipe(cipher).pipe(output);
  
  return new Promise((resolve, reject) => {
    output.on('finish', () => {
      // Return the authentication tag for verification during decryption
      const authTag = cipher.getAuthTag();
      // Store this authTag alongside the file or with its metadata
      resolve(authTag);
    });
    
    output.on('error', reject);
  });
}

 

2. Key Management System

 

The trickiest part of encryption isn't the algorithms—it's key management. Here's a pragmatic approach:

// Key management system architecture
class KeyManagementSystem {
  constructor(masterKeyProvider) {
    this.masterKeyProvider = masterKeyProvider; // External KMS like AWS KMS, Azure Key Vault
  }
  
  async generateDataKey() {
    // Generate a data key for encrypting a specific file
    const dataKey = crypto.randomBytes(32); // 256 bits
    
    // Encrypt the data key with the master key
    const encryptedDataKey = await this.masterKeyProvider.encrypt(dataKey);
    
    return {
      plaintextKey: dataKey, // Used for immediate encryption, never stored
      encryptedKey: encryptedDataKey // Stored alongside the encrypted file
    };
  }
  
  async decryptDataKey(encryptedDataKey) {
    // Use the master key to decrypt the data key
    return this.masterKeyProvider.decrypt(encryptedDataKey);
  }
}

 

3. Integrating with File Upload and Download Flows

 

For React applications, here's how you might implement the client-side encryption flow:

function SecureFileUploader() {
  const [file, setFile] = useState(null);
  const [isUploading, setIsUploading] = useState(false);
  
  const handleFileSelect = (e) => {
    setFile(e.target.files[0]);
  };
  
  const uploadFile = async () => {
    if (!file) return;
    
    setIsUploading(true);
    
    try {
      // Step 1: Request encryption parameters from your server
      const { publicKey, uploadUrl } = await fetch('/api/prepare-upload', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ filename: file.name, contentType: file.type })
      }).then(res => res.json());
      
      // Step 2: Encrypt the file client-side
      const { encryptedFile, encryptedKey, iv } = await encryptFile(file, publicKey);
      
      // Step 3: Create form data with encrypted file and metadata
      const formData = new FormData();
      formData.append('file', encryptedFile);
      formData.append('encryptedKey', JSON.stringify(encryptedKey));
      formData.append('iv', JSON.stringify(Array.from(iv)));
      
      // Step 4: Upload encrypted file
      await fetch(uploadUrl, {
        method: 'PUT',
        body: formData
      });
      
      alert('File securely uploaded!');
    } catch (error) {
      console.error('Upload failed:', error);
      alert('Upload failed. Please try again.');
    } finally {
      setIsUploading(false);
    }
  };
  
  return (
    <div>
      <input type="file" onChange={handleFileSelect} />
      <button 
        onClick={uploadFile}
        disabled={!file || isUploading}
      >
        {isUploading ? 'Encrypting & Uploading...' : 'Secure Upload'}
      </button>
    </div>
  );
}

 

4. Secure File Storage Architecture

 

A solid storage architecture for encrypted files separates the content from its metadata:

// Example database schema (using Prisma ORM syntax)
model EncryptedFile {
  id            String    @id @default(uuid())
  filename      String
  contentType   String
  size          Int
  storageKey    String    // Path or ID in your storage system (S3, etc.)
  encryptedKey  Bytes     // The encrypted data key
  iv            Bytes     // Initialization vector
  authTag       Bytes?    // Authentication tag (for GCM mode)
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  ownerId       String
  owner         User      @relation(fields: [ownerId], references: [id])
  
  @@index([ownerId])
}

 

5. Implementing the Decryption Flow

 

The download process reverses the encryption flow:

// Server-side handler for file download requests
async function handleFileDownload(req, res) {
  const { fileId } = req.params;
  const userId = req.user.id; // From your auth middleware
  
  // Retrieve file metadata
  const fileRecord = await db.encryptedFile.findFirst({
    where: {
      id: fileId,
      ownerId: userId // Ensure the user has access to this file
    }
  });
  
  if (!fileRecord) {
    return res.status(404).send('File not found');
  }
  
  // For client-side decryption: provide necessary parameters
  if (req.query.clientDecrypt === 'true') {
    // Get file from storage
    const encryptedFileStream = await storage.getFileStream(fileRecord.storageKey);
    
    // Set appropriate headers
    res.setHeader('Content-Type', fileRecord.contentType);
    res.setHeader('Content-Disposition', `attachment; filename="${fileRecord.filename}"`);
    res.setHeader('X-Encrypted-Key', fileRecord.encryptedKey.toString('base64'));
    res.setHeader('X-Initialization-Vector', fileRecord.iv.toString('base64'));
    
    // Stream the encrypted file to the client
    encryptedFileStream.pipe(res);
  } 
  // For server-side decryption
  else {
    try {
      // Get the encrypted data key
      const encryptedDataKey = fileRecord.encryptedKey;
      
      // Decrypt the data key using the KMS
      const dataKey = await keyManagementSystem.decryptDataKey(encryptedDataKey);
      
      // Get the encrypted file
      const encryptedFileStream = await storage.getFileStream(fileRecord.storageKey);
      
      // Create a decryption transform stream
      const decipher = crypto.createDecipheriv(
        'aes-256-gcm', 
        dataKey, 
        fileRecord.iv
      );
      
      // Set auth tag for authenticated decryption
      if (fileRecord.authTag) {
        decipher.setAuthTag(fileRecord.authTag);
      }
      
      // Set appropriate headers for download
      res.setHeader('Content-Type', fileRecord.contentType);
      res.setHeader('Content-Disposition', `attachment; filename="${fileRecord.filename}"`);
      
      // Pipe the encrypted file through the decipher to the response
      encryptedFileStream.pipe(decipher).pipe(res);
    } catch (error) {
      console.error('Decryption failed:', error);
      res.status(500).send('Failed to decrypt file');
    }
  }
}

 

Real-World Considerations

 

Performance Impact

 

Encryption adds processing overhead. For large files, consider:

  • Chunked encryption/decryption to avoid memory issues
  • Progress indicators to keep users informed during the process
  • Worker threads (Web Workers in browsers) to prevent UI blocking

 

Here's how you might implement chunked encryption in the browser:

async function encryptLargeFile(file, publicKey) {
  // Create a readable stream from the file
  const fileStream = file.stream();
  const reader = fileStream.getReader();
  
  // Generate encryption key
  const symmetricKey = await window.crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"]
  );
  
  // Generate IV
  const iv = window.crypto.getRandomValues(new Uint8Array(12));
  
  // Initialize result chunks array
  const encryptedChunks = [];
  let processedBytes = 0;
  const totalBytes = file.size;
  
  // Process the file in chunks
  while (true) {
    const { done, value } = await reader.read();
    
    if (done) break;
    
    // Encrypt this chunk
    const encryptedChunk = await window.crypto.subtle.encrypt(
      { name: "AES-GCM", iv },
      symmetricKey,
      value
    );
    
    encryptedChunks.push(new Uint8Array(encryptedChunk));
    processedBytes += value.byteLength;
    
    // Report progress
    const progress = Math.round((processedBytes / totalBytes) * 100);
    console.log(`Encryption progress: ${progress}%`);
  }
  
  // Combine encrypted chunks
  let totalLength = 0;
  encryptedChunks.forEach(chunk => { totalLength += chunk.byteLength; });
  
  const encryptedFile = new Uint8Array(totalLength);
  let offset = 0;
  
  encryptedChunks.forEach(chunk => {
    encryptedFile.set(chunk, offset);
    offset += chunk.byteLength;
  });
  
  // Export and encrypt the symmetric key
  const exportedKey = await window.crypto.subtle.exportKey("raw", symmetricKey);
  const encryptedKey = await encryptKeyWithRSA(exportedKey, publicKey);
  
  return {
    encryptedFile: new Blob([encryptedFile]),
    encryptedKey,
    iv
  };
}

 

Key Rotation and Recovery

 

No encryption system is complete without considering the lifecycle of your keys:

  • Implement regular key rotation to limit the damage of potential key exposure
  • Create a secure recovery mechanism for when users lose access to their keys

 

// Example key rotation implementation
async function rotateFileKeys(fileIds) {
  for (const fileId of fileIds) {
    // 1. Retrieve file metadata and encrypted content
    const fileRecord = await db.encryptedFile.findUnique({ where: { id: fileId } });
    const encryptedContent = await storage.getFile(fileRecord.storageKey);
    
    // 2. Decrypt the file's data key using the old master key
    const oldDataKey = await keyManagementSystem.decryptDataKey(
      fileRecord.encryptedKey, 
      { version: fileRecord.keyVersion }
    );
    
    // 3. Decrypt the file content
    const decryptedContent = decryptWithKey(
      encryptedContent, 
      oldDataKey,
      fileRecord.iv,
      fileRecord.authTag
    );
    
    // 4. Generate a new data key
    const { plaintextKey: newDataKey, encryptedKey: newEncryptedKey } = 
      await keyManagementSystem.generateDataKey();
    
    // 5. Re-encrypt the file with the new key
    const { encryptedContent: newEncryptedContent, iv: newIv, authTag: newAuthTag } = 
      encryptWithKey(decryptedContent, newDataKey);
    
    // 6. Store the re-encrypted file and update metadata
    const newStorageKey = await storage.storeFile(newEncryptedContent);
    
    await db.encryptedFile.update({
      where: { id: fileId },
      data: {
        storageKey: newStorageKey,
        encryptedKey: newEncryptedKey,
        iv: newIv,
        authTag: newAuthTag,
        keyVersion: keyManagementSystem.currentVersion
      }
    });
    
    // 7. Delete the old encrypted file
    await storage.deleteFile(fileRecord.storageKey);
  }
}

 

Common Pitfalls to Avoid

 

  • Using outdated encryption algorithms like DES, MD5, or SHA-1
  • Hardcoding encryption keys in your application code
  • Generating weak keys with insufficient entropy
  • Neglecting proper IV management (reusing IVs or using predictable values)
  • Failing to authenticate encrypted data (use authenticated encryption modes like GCM)

 

Making the Business Case

 

When pitching file encryption to stakeholders, focus on:

  • Risk mitigation: Even if your cloud storage is compromised, encrypted files remain protected
  • Customer trust: Marketing "end-to-end encryption" builds confidence in your product
  • Competitive advantage: Security features differentiate your product in crowded markets
  • Future-proofing: Building encryption early avoids costly retrofitting later

 

Testing Your Encryption Implementation

 

Here's a practical approach to testing your encryption system:

// Jest test example for encryption/decryption flow
describe('File Encryption System', () => {
  let testFile, encryptionResult;
  
  beforeAll(async () => {
    // Create a test file with known content
    testFile = new File(['This is test content for encryption'], 'test.txt', {
      type: 'text/plain'
    });
    
    // Generate a test key pair
    const keyPair = await window.crypto.subtle.generateKey(
      {
        name: "RSA-OAEP",
        modulusLength: 2048,
        publicExponent: new Uint8Array([1, 0, 1]),
        hash: "SHA-256",
      },
      true,
      ["encrypt", "decrypt"]
    );
    
    // Store for later use in tests
    this.publicKey = keyPair.publicKey;
    this.privateKey = keyPair.privateKey;
  });
  
  test('should encrypt a file', async () => {
    // Encrypt the test file
    encryptionResult = await encryptFile(testFile, this.publicKey);
    
    // Verify the result has expected properties
    expect(encryptionResult).toHaveProperty('encryptedFile');
    expect(encryptionResult).toHaveProperty('encryptedKey');
    expect(encryptionResult).toHaveProperty('iv');
    
    // Encrypted content should be different from original
    const encryptedContent = await encryptionResult.encryptedFile.text();
    expect(encryptedContent).not.toBe('This is test content for encryption');
  });
  
  test('should decrypt the file correctly', async () => {
    // Decrypt the previously encrypted file
    const decryptedFile = await decryptFile(
      encryptionResult.encryptedFile,
      encryptionResult.encryptedKey,
      encryptionResult.iv,
      this.privateKey
    );
    
    // Verify the decrypted content matches original
    const decryptedContent = await decryptedFile.text();
    expect(decryptedContent).toBe('This is test content for encryption');
  });
  
  test('should fail decryption with wrong key', async () => {
    // Generate a different key pair
    const wrongKeyPair = await window.crypto.subtle.generateKey(
      {
        name: "RSA-OAEP",
        modulusLength: 2048,
        publicExponent: new Uint8Array([1, 0, 1]),
        hash: "SHA-256",
      },
      true,
      ["encrypt", "decrypt"]
    );
    
    // Attempt to decrypt with the wrong private key
    await expect(
      decryptFile(
        encryptionResult.encryptedFile,
        encryptionResult.encryptedKey,
        encryptionResult.iv,
        wrongKeyPair.privateKey
      )
    ).rejects.toThrow();
  });
});

 

Final Thoughts

 

Adding file encryption to your web app isn't a minor feature—it's a fundamental architectural decision that affects how data flows through your entire system. The implementation I've outlined balances security with practical considerations like performance and user experience.

 

Remember that encryption is just one layer of a comprehensive security strategy. It should complement—not replace—other security measures like proper authentication, authorization, secure infrastructure, and routine security audits.

 

The most successful encryption implementations are those that users never notice. When done right, the complexity remains hidden, while users simply enjoy the peace of mind that comes with knowing their sensitive files are protected even if the worst happens.

Ship File Encryption 10x Faster with RapidDev

Connect with our team to unlock the full potential of code solutions with a no-commitment consultation!

Book a Free Consultation

Top 3 File Encryption Usecases

Explore the top 3 key use cases for adding file encryption to secure your web app effectively.

 

Protected Data in Transit

 

File encryption secures sensitive information as it moves between systems, creating a protective shield that follows your data wherever it goes. Even if intercepted during transfer, encrypted files remain unreadable without proper decryption keys.

 

  • Business value: Prevents costly data breaches during file sharing with clients, partners, or remote employees while maintaining compliance with regulations like GDPR, HIPAA, and CCPA.
  • Implementation consideration: Balance strong encryption (AES-256) with user experience by implementing seamless key management that doesn't create friction during legitimate file transfers.

 

Secure Cloud Storage

 

Client-side encryption before uploading to cloud storage ensures that even if your cloud provider is compromised, your sensitive data remains protected. Your files exist in the cloud only in encrypted form, with decryption keys controlled exclusively by your organization.

 

  • Business value: Maintains control of sensitive data while leveraging cost-effective cloud storage solutions, creating a "trust no one" security model without sacrificing cloud benefits.
  • Implementation consideration: Design for key recovery scenarios to prevent permanent data loss if encryption keys are misplaced, while ensuring the recovery process itself remains secure.

 

Data Leak Prevention

 

Automatically encrypting sensitive files based on content, classification, or location creates a persistent protection layer that follows data throughout its lifecycle. Even if exfiltrated, encrypted files remain useless to attackers without proper authorization.

 

  • Business value: Reduces insider threat risks and mitigates the impact of unauthorized access, while providing audit trails of file access that demonstrate due diligence for compliance requirements.
  • Implementation consideration: Implement granular access controls that allow different user roles appropriate levels of encryption/decryption privileges, avoiding security that's so restrictive it hampers productivity.


Recognized by the best

Trusted by 600+ businesses globally

From startups to enterprises and everything in between, see for yourself our incredible impact.

RapidDev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with.

They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

Arkady
CPO, Praction
Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost.

He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Donald Muir
Co-Founder, Arc
RapidDev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space.

They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Mat Westergreen-Thorne
Co-CEO, Grantify
RapidDev is an excellent developer for custom-code solutions.

We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Emmanuel Brown
Co-Founder, Church Real Estate Marketplace
Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 

This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Samantha Fekete
Production Manager, Media Production Company
The pSEO strategy executed by RapidDev is clearly driving meaningful results.

Working with RapidDev has delivered measurable, year-over-year growth. Comparing the same period, clicks increased by 129%, impressions grew by 196%, and average position improved by 14.6%. Most importantly, qualified contact form submissions rose 350%, excluding spam.

Appreciation as well to Matt Graham for championing the collaboration!

Michael W. Hammond
Principal Owner, OCD Tech

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â