使用客户端加密构建零知识文件共享平台

发布: (2025年12月10日 GMT+8 18:56)
6 min read
原文: Dev.to

Source: Dev.to

🎯 核心问题

传统的文件共享服务使用 服务器端加密

  • 文件在服务器上加密
  • 服务持有加密密钥
  • 他们可以随时解密文件(用于扫描、合规、法律请求)

这导致了一个根本的信任问题:你必须信任公司的安全性、政策和意图。

💡 解决方案:零知识架构

FileBee 实现了 客户端加密 的零知识方法:

  • 加密在浏览器中完成后再上传
  • 密钥存储在本地的 IndexedDB 中
  • 服务器仅存储它无法解密的加密块
  • 如果用户删除密钥,文件将永久不可访问
  • 即使数据库被泄露,攻击者也只能得到加密数据

🏗️ 技术架构

堆栈概览

技术
前端Angular 15+
存储IndexedDB(密钥),云存储(加密文件)
加密Web Crypto API
UI 框架Angular Material

关键组件

客户端加密流程

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

密钥生成与存储

// 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()
  });
}

上传流程架构

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

下载流程架构

User opens URL

Prompt for encryption key

Fetch encrypted blob from server

Decrypt in browser using provided key

Trigger download of decrypted file

🔐 已实现的安全特性

AES‑256‑GCM 加密

const algorithm = {
  name: 'AES-GCM',
  length: 256
};
  • 强大的 256 位加密
  • 内置认证(防止篡改)
  • 每个文件加密使用唯一 IV

本地密钥管理

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

密钥永不离开浏览器。

自动删除

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

密钥删除 = 数据销毁

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
}

🎨 用户体验挑战

挑战 1:密钥管理

问题: 用户必须保存随机的加密密钥(例如 k3j2h4kj234h23k4j23h4k23j4h)。
解决方案:

  • 上传后显著展示密钥
  • 提供“一键复制”按钮
  • 为移动端生成二维码
  • 在删除密钥前给出警告
@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>
  `
})

挑战 2:大文件处理

问题: 在浏览器中加密 50 MB 文件会导致 UI 卡顿。
解决方案: 使用 Web Workers 进行加密。

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

挑战 3:文件夹上传

问题: 在加密每个文件的同时保持目录结构。
解决方案: 将元数据单独存储。

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

📊 存储限制与优化

  • 每个文件 50 MB
  • 每位用户总计 250 MB
  • 7 天自动删除

存储配额检查

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

🎯 二维码生成

自动生成二维码,方便移动端分享:

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

相关文章

阅读更多 »