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

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
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.
The Two Primary Approaches
For most business applications, I recommend a hybrid approach that leverages the security benefits of both methods.
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');
}
}
}
Performance Impact
Encryption adds processing overhead. For large files, consider:
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:
// 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);
}
}
When pitching file encryption to stakeholders, focus on:
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();
});
});
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.
Explore the top 3 key use cases for adding file encryption to secure your web app effectively.
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.
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.
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.
From startups to enterprises and everything in between, see for yourself our incredible impact.
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.Â