我如何使用 Next.js 和 Web Crypto API 构建零知识秘密共享者
Source: Dev.to

大多数“安全”共享工具都要求你信任服务器。你粘贴密码,服务器对其加密并存储。但如果服务器记录了请求,或者数据库泄漏,你的秘密就不复存在。
我想要一个工具,让 我(开发者)即使想读也读不到数据。
于是我构建了 Nix(https://nix.jaid.dev),一个开源、零知识的秘密共享应用。下面是它的技术实现细节,使用 AES‑GCM 和 URL 哈希片段。
架构
核心约束是 Zero Knowledge —— 服务器绝不能收到解密密钥。
- Alice 在浏览器中生成随机密钥。
- Alice 在客户端加密数据。
- Alice 只发送密文(包装在 JSON 信封中)到服务器(Supabase)。
- 浏览器构造链接:
https://nix.jaid.dev/view/[ID]#[KEY]。 - Bob 点击链接。他的浏览器请求该 ID,从 URL 中提取
#[KEY](该密钥从未发送到服务器),并在本地解密数据。
技术栈
- 前端: Next.js 16 (App Router)
- 数据库: Supabase (Postgres)
- 加密: 原生 Web Crypto API (
window.crypto.subtle) - 样式: Tailwind CSS
硬核部分:Web Crypto API
Web Crypto API 功能强大但代码冗长。下面是加密流程的关键步骤。
生成密钥
我们需要一个密码学上安全的随机密钥。SubtleCrypto API 能保证安全生成。
// Generate a secure AES‑GCM key
const key = await window.crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
// Export to raw bytes (and then to Base64) for the URL
const exported = await window.crypto.subtle.exportKey("raw", key);
加密(AES‑GCM)
AES‑GCM 同时提供机密性和完整性。每次加密操作都必须生成唯一的 IV。
async function encrypt(content, key) {
const encoder = new TextEncoder();
const encodedContent = encoder.encode(content);
// 96‑bit IV for GCM
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encryptedContent = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv,
},
key,
encodedContent
);
// Return serialized JSON with IV and ciphertext
return JSON.stringify({
iv: Array.from(iv),
data: Array.from(new Uint8Array(encryptedContent)),
});
}
URL 哈希技巧
当浏览器访问 example.com/page#secret123 时,服务器只会看到 GET /page。# 之后的内容永远不会发送到服务器,这让我们可以安全地在 URL 中传递解密密钥。
// On the client (e.g., inside useEffect)
useEffect(() => {
const hash = window.location.hash; // "#5f3a..."
if (hash) {
const keyString = hash.substring(1); // Remove '#'
// Trigger decryption...
}
}, []);
数据库与过期(Supabase)
由于服务器只存储加密后的二进制数据,Supabase 配合行级安全(RLS)负责存储。客户端实现“读取即焚毁”和过期机制:
- 获取: 读取加密记录。
- 检查过期: 将
expires_at与当前时间比较;如果已过期,则视为失效并删除该密文。 - 读取即焚毁: 若元数据标记为 “Burn on Read”,在成功获取后立即向 Supabase 发起删除请求。
教训
- Hydration Errors: Next.js Server Components 没有
window。只能在useEffect或事件处理函数中调用window.crypto。 - Encoding Hell: 在
ArrayBuffer、Uint8Array与字符串之间相互转换非常繁琐。TextEncoder和TextDecoder是必不可少的工具。 - Trust: 透明度对安全工具至关重要。该仓库从一开始就是开源的,因为我自己也不会使用闭源方案。
试用
我正在寻求对加密实现和用户体验的反馈。
- Live Demo:
- Repo: (感谢星标!)