Building a Zero-Knowledge File Sharing Platform with Client-Side Encryption

Published: (December 10, 2025 at 05:56 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

🎯 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
  • The server only stores encrypted blobs it can’t decrypt
  • If users delete their keys, files become permanently inaccessible
  • Even if the database is compromised, attackers get only encrypted data

🏗️ Technical Architecture

Stack Overview

LayerTechnology
FrontendAngular 15+
StorageIndexedDB (keys), Cloud Storage (encrypted files)
EncryptionWeb Crypto API
UI FrameworkAngular Material

Key Components

Client‑Side Encryption Flow

// Simplified encryption flow
async function encryptFile(file, encryptionKey) {
  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

// Generate encryption key for user
async function generateEncryptionKey() {
  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, key) {
  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

const algorithm = {
  name: 'AES-GCM',
  length: 256
};
  • Strong 256‑bit encryption
  • Built‑in authentication (prevents tampering)
  • Unique IV per file encryption

Local Key Management

// 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
}

Keys never leave the browser.

Automatic Deletion

// 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

async function deleteEncryptionKey(keyId) {
  // 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

Challenge 1: Key Management

Problem: Users must save random encryption keys (e.g., k3j2h4kj234h23k4j23h4k23j4h).
Solution:

  • Display keys prominently after upload
  • Provide a copy‑to‑clipboard button
  • Generate QR codes for mobile sharing
  • Warn users before key deletion
@Component({
  selector: 'app-encryption-key-display',
  template: `
    <mat-card>
      <mat-icon>vpn_key</mat-icon>
      <pre>{{ encryptionKey }}</pre>
      <button mat-icon-button (click)="copyKey()">
        <mat-icon>content_copy</mat-icon>
      </button>
      <mat-icon color="warn">warning</mat-icon>
      <p>Save this key! Without it, files cannot be decrypted.</p>
    </mat-card>
  `
})

Challenge 2: Large File Handling

Problem: Encrypting 50 MB files in‑browser can freeze the UI.
Solution: Use Web Workers for encryption.

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

interface FolderMetadata {
  files: Array<any>;
  commonUrl?: string;    // Optional common URL for all files
}

// Encrypt and upload folder
async function uploadFolder(folder: File[]): Promise<any> {
  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

  • 50 MB per file
  • 250 MB total storage per user
  • 7‑day automatic deletion

Storage Quota Check

async function checkStorageQuota(user) {
  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; // 250 MB
  return totalSize  MAX_FILE_SIZE) {
    throw new Error(`File exceeds 50 MB limit: ${file.name}`);
  }
  return true;
}

🎯 QR Code Generation

Auto‑generate QR codes for easy mobile sharing:

import * as QRCode from 'qrcode';

// Example: generate a data URL
async function generateQrCode(url: string): Promise<string> {
  return await QRCode.toDataURL(url);
}
Back to Blog

Related posts

Read more »

Cost of developing fintech apps in 2025

Fintech has moved from niche to mainstream. In 2025, users expect instant onboarding, real‑time payments, smart analytics, and bank‑grade security as a default....