Building FileBee: A Zero-Knowledge File Sharing Platform with Client-Side Encryption
published: true
description: How I built a secure file-sharing service with end-to-end encryption, zero-knowledge architecture, and automatic 7-day deletion using Angular and IndexedDB
tags: webdev, security, javascript, angular
Building FileBee: A Zero-Knowledge File Sharing Platform with Client-Side Encryption
Ever wondered why most file-sharing services can access your files? When you upload to Dropbox, Google Drive, or WeTransfer, those companies technically have the ability to decrypt and view your data. This bothered me enough to build something different.
Today, I want to share how I built FileBee - a file-sharing platform where even I (the developer) can't access your files. Let's dive into the technical architecture that makes this possible.
๐ฏ The Core Problem
Traditional file-sharing services use server-side encryption:
Files are encrypted on the server
The service holds the encryption keys
They can decrypt files anytime (for scanning, compliance, legal requests)
This creates a fundamental trust problem. You're trusting the company's security, policies, and intentions.
๐ก The Solution: Zero-Knowledge Architecture
FileBee implements client-side encryption with a zero-knowledge approach:
Encryption happens in the browser before upload
Keys are stored locally in IndexedDB
Server only stores encrypted blobs it can't decrypt
If users delete their keys, files become permanently inaccessible
Even if my database is compromised, attackers get nothing but encrypted data.
๐๏ธ Technical Architecture
Stack Overview
Frontend: Angular 15+
Storage: IndexedDB (for keys), Cloud Storage (for encrypted files)
Encryption: Web Crypto API
UI Framework: Angular Material
Key Components
- Client-Side Encryption Flow javascript// Simplified encryption flow async function encryptFile(file: File, encryptionKey: CryptoKey): Promise { const fileBuffer = await file.arrayBuffer(); const iv = crypto.getRandomValues(new Uint8Array(12)); // GCM IV
const encryptedData = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
encryptionKey,
fileBuffer
);
// Prepend IV to encrypted data for decryption later
return concatenate(iv, new Uint8Array(encryptedData));
}
- Key Generation & Storage javascript// Generate encryption key for user async function generateEncryptionKey(): Promise { return await crypto.subtle.generateKey( { name: 'AES-GCM', length: 256 }, true, // extractable ['encrypt', 'decrypt'] ); }
// Store in IndexedDB (never sent to server)
async function storeKeyLocally(keyId: string, key: CryptoKey): Promise {
const exportedKey = await crypto.subtle.exportKey('jwk', key);
await db.keys.put({
id: keyId,
key: exportedKey,
createdAt: Date.now()
});
}
- Upload Flow Architecture User selects file โ Generate/retrieve encryption key (IndexedDB) โ Encrypt file in browser (Web Crypto API) โ Upload encrypted blob to server โ Server generates short URL โ Return URL + QR code to user โ User shares URL + encryption key separately
- Download Flow Architecture User opens URL โ Prompt for encryption key โ Fetch encrypted blob from server โ Decrypt in browser using provided key โ Trigger download of decrypted file ๐ Security Features Implemented
- AES-256-GCM Encryption Using the Web Crypto API's AES-GCM mode provides:
Strong 256-bit encryption
Built-in authentication (prevents tampering)
Unique IV per file encryption
javascriptconst algorithm = {
name: 'AES-GCM',
length: 256
};
- Local Key Management Keys never leave the browser: javascript// IndexedDB schema for key storage interface EncryptionKey { id: string; // Unique key identifier key: JsonWebKey; // Exported key data createdAt: number; // Timestamp files: string[]; // Associated file IDs }
- Automatic Deletion Implemented server-side cron job: javascript// Pseudo-code for cleanup job async function cleanupExpiredFiles() { const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
const expiredFiles = await db.files
.where('uploadedAt')
.below(sevenDaysAgo)
.toArray();
for (const file of expiredFiles) {
await storage.deleteFile(file.id);
await db.files.delete(file.id);
}
}
// Run every hour
setInterval(cleanupExpiredFiles, 60 * 60 * 1000);
- Key Deletion = Data Destruction javascriptasync function deleteEncryptionKey(keyId: string): Promise { // Get all files associated with this key const key = await db.keys.get(keyId);
// Delete files from server
for (const fileId of key.files) {
await api.deleteFile(fileId);
}
// Delete key from IndexedDB
await db.keys.delete(keyId);
// Files are now permanently inaccessible
}
๐จ User Experience Challenges
Building secure software often means compromising UX. Here's how I tackled that:
Challenge 1: Key Management
Problem: Users need to save encryption keys, but they're random strings like k3j2h4kj234h23k4j23h4k23j4h
Solution:
Display keys prominently after upload
Provide copy-to-clipboard button
Generate QR codes for mobile sharing
Warn users before key deletion
typescript// Key display component
@Component({
selector: 'app-encryption-key-display',
template:
<div class="key-container">
<mat-icon>vpn_key</mat-icon>
<code>{{ encryptionKey }}</code>
<button mat-icon-button (click)="copyKey()">
<mat-icon>content_copy</mat-icon>
</button>
</div>
<mat-card class="warning">
<mat-icon>warning</mat-icon>
<p>Save this key! Without it, files cannot be decrypted.</p>
</mat-card>
})
Challenge 2: Large File Handling
Problem: Encrypting 50MB files in-browser can freeze the UI
Solution: Use Web Workers for encryption
javascript// encryption.worker.ts
self.addEventListener('message', async (e) => {
const { file, key } = e.data;
const encrypted = await encryptFile(file, key);
self.postMessage({
encrypted,
progress: 100
});
});
// Main thread
const worker = new Worker('./encryption.worker.ts');
worker.postMessage({ file, key });
worker.addEventListener('message', (e) => {
uploadEncryptedFile(e.data.encrypted);
});
Challenge 3: Folder Uploads
Problem: Maintaining directory structure while encrypting each file
Solution: Store metadata separately
typescriptinterface FolderMetadata {
files: Array<{
path: string; // Original path: "project/src/index.ts"
encryptedId: string; // Server file ID
size: number;
mimeType: string;
}>;
commonUrl?: string; // Optional common URL for all files
}
// Encrypt and upload folder
async function uploadFolder(folder: File[]): Promise {
const metadata: FolderMetadata = { files: [] };
for (const file of folder) {
const encrypted = await encryptFile(file, key);
const { id } = await uploadToServer(encrypted);
metadata.files.push({
path: file.webkitRelativePath || file.name,
encryptedId: id,
size: file.size,
mimeType: file.type
});
}
return metadata;
}
๐ Storage Limits & Optimization
Limits Implemented:
50MB per file
250MB total storage per user
7-day automatic deletion
Storage Quota Check
typescriptasync function checkStorageQuota(user: string): Promise {
const userFiles = await db.files
.where('userId')
.equals(user)
.toArray();
const totalSize = userFiles.reduce((sum, f) => sum + f.size, 0);
const MAX_STORAGE = 250 * 1024 * 1024; // 250MB
return totalSize < MAX_STORAGE;
}
File Size Validation
typescriptfunction validateFileSize(file: File): boolean {
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
if (file.size > MAX_FILE_SIZE) {
throw new Error(File exceeds 50MB limit: ${file.name});
}
return true;
}
๐ฏ QR Code Generation
Auto-generate QR codes for easy mobile sharing:
typescriptimport * as QRCode from 'qrcode';
async function generateQRCode(url: string, key: string): Promise {
// Combine URL and key for one-scan sharing
const shareData = JSON.stringify({ url, key });
const qrDataUrl = await QRCode.toDataURL(shareData, {
width: 300,
margin: 2,
color: {
dark: '#667eea',
light: '#ffffff'
}
});
return qrDataUrl;
}
๐ Performance Optimizations
- Chunked Upload for Large Files typescriptasync function uploadLargeFile( encrypted: ArrayBuffer, chunkSize = 5 * 1024 * 1024 // 5MB chunks ): Promise { const chunks = Math.ceil(encrypted.byteLength / chunkSize); const uploadId = generateUploadId();
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, encrypted.byteLength);
const chunk = encrypted.slice(start, end);
await uploadChunk(uploadId, i, chunk);
updateProgress((i + 1) / chunks * 100);
}
return uploadId;
}
- Lazy Loading Material Components typescript// Only load heavy components when needed const MatDialogModule = await import('@angular/material/dialog'); const QRCodeModule = await import('./qr-code/qr-code.module');
-
Service Worker for Offline Support
javascript// sw.js - Cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('filebee-v1').then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/main.js'
]);
})
);
});
๐ง Testing Strategy
Unit Tests for Encryption
typescriptdescribe('File Encryption', () => {
it('should encrypt and decrypt file correctly', async () => {
const originalFile = new File(['test content'], 'test.txt');
const key = await generateEncryptionKey();const encrypted = await encryptFile(originalFile, key);
const decrypted = await decryptFile(encrypted, key);expect(decrypted).toEqual(await originalFile.arrayBuffer());
});
it('should fail decryption with wrong key', async () => {
const file = new File(['secret'], 'secret.txt');
const key1 = await generateEncryptionKey();
const key2 = await generateEncryptionKey();
const encrypted = await encryptFile(file, key1);
await expectAsync(
decryptFile(encrypted, key2)
).toBeRejected();
});
});
Integration Tests
typescriptdescribe('Upload Flow', () => {
it('should complete full upload-download cycle', async () => {
const testFile = new File(['content'], 'test.txt');
// Upload
const { url, key } = await uploadFile(testFile);
// Download
const downloaded = await downloadFile(url, key);
expect(downloaded.name).toBe('test.txt');
expect(await downloaded.text()).toBe('content');
});
});
๐ Lessons Learned
- Browser Compatibility Gotchas Web Crypto API support varies. I added fallback detection: typescriptfunction checkCryptoSupport(): boolean { if (!window.crypto || !window.crypto.subtle) { alert('Your browser doesn\'t support Web Crypto API'); return false; } return true; }
- IndexedDB Pitfalls IndexedDB can be quirky. Always wrap operations: typescriptasync function safeIndexedDBOperation( operation: () => Promise ): Promise { try { return await operation(); } catch (error) { if (error.name === 'QuotaExceededError') { alert('Storage quota exceeded. Please clear some data.'); } console.error('IndexedDB error:', error); return null; } }
- Mobile Safari Limitations
iOS Safari has strict storage limits. I added checks:
typescriptif (navigator.storage && navigator.storage.estimate) {
const { usage, quota } = await navigator.storage.estimate();
console.log(
Using ${usage} of ${quota} bytes); } ๐ Metrics & Monitoring Key metrics I track: typescriptinterface Metrics { filesUploaded: number; totalDataEncrypted: number; averageEncryptionTime: number; failedUploads: number; activeUsers: number; }
// Track encryption performance
async function trackEncryption(file: File, key: CryptoKey) {
const startTime = performance.now();
const encrypted = await encryptFile(file, key);
const duration = performance.now() - startTime;
analytics.track('file_encrypted', {
fileSize: file.size,
duration,
throughput: file.size / duration
});
}
๐ Key Takeaways
Client-side encryption is viable for web apps with Web Crypto API
Zero-knowledge architecture requires thoughtful key management UX
IndexedDB works well for local key storage
Web Workers are essential for handling large file encryption without blocking UI
Security and UX don't have to be mutually exclusive
๐ฎ Future Enhancements
Planning to add:
Password-protected links (additional encryption layer)
Expiring links (custom deletion times)
File sharing analytics (download counts, access logs)
Progressive Web App (full offline support)
E2EE chat (discuss files securely)
๐ Try It Out
Check out FileBee and let me know what you think!
Features:
โ
50MB per file, 250MB total storage
โ
End-to-end encryption with local key management
โ
7-day automatic deletion
โ
QR code generation
โ
Folder uploads with directory structure
โ
No registration required
โ
100% free
๐ฌ Discussion
Questions for the community:
What other security features would you want in a file-sharing service?
Have you built similar encryption systems? What challenges did you face?
What's your take on client-side vs server-side encryption trade-offs?
Drop a comment below! I'm happy to discuss the technical details or help if you're building something similar.
Website: https://filebee.in
Tags: #webdev #security #javascript #angular #encryption #privacy #opensource
Top comments (0)