使用客户端加密构建零知识文件共享平台
发布: (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);
}