情绪日记客户端本地加密存储:用 Web Crypto API 守护隐私
在做情绪日记这款产品时,用户最关心的首要问题就是:“我的私人碎碎念和情绪数据,到底安不安全?”
在这个数据泄露频发的时代,把未经加密的个人私密文字直接明文存放在云端数据库里,无异于在用户头顶悬了一把达摩克利斯之剑。即便开发者在隐私政策里承诺“绝不查看用户数据”,也无法完全消除用户的顾虑。
最让人安心的方案是端到端本地加密(Zero-Knowledge Local Encryption):用户的日记在离开浏览器前,在本地使用现代浏览器原生的 Web Crypto API 通过 AES-GCM 算法进行高强度加密。数据即使存放在本地 IndexedDB 或同步到云端,任何第三方(包括开发者自己)拿到的也只是一串无法破解的随机密文。
今天我把自用的轻量客户端加密实现拆解出来,不到 80 行代码即可安全落地。
+——————————————————————–+
| 基于 Web Crypto API 的本地零知识加密流程 |
+——————————————————————–+
| 1. 用户自定义主密码 (Passphrase) |
| | |
| v (PBKDF2 算法 + 100,000 次哈希迭代 + 随机 Salt) |
| 2. 派生出 AES-GCM 256 位加密对称密钥 |
| | |
| v (结合每次随机生成的 12 字节 IV 初始化向量) |
| 3. 明文日记 JSON ————————-> 密文 ArrayBuffer |
| | |
| v |
| 4. 存入本地 IndexedDB (密文 + Salt + IV,无明文残留) |
+——————————————————————–+
1. 为什么不用第三方 JS 加密库(如 CryptoJS)
在很多老旧教程中,大家习惯 npm install crypto-js。但在现代前端开发中,强烈建议直接使用浏览器内置的 window.crypto.subtle(Web Crypto API):
- 原生性能与安全性:由浏览器底层的 C++ / Rust 原生实现,执行加密时不易受到 JavaScript 运行时的侧信道攻击(Side-channel Attack),且加密大文本时速度快数十倍。
- 零打包体积:不需要引入任何第三方 npm 包,直接节省几十 KB 的前端构建产物。
2. 密码派生与 AES-GCM 加密完整实现
以下是封装好的客户端加密工具模块 src/lib/crypto.ts:
// 基于 PBKDF2 派生密钥与 AES-GCM 算法的加密工具
export class LocalVault {
// 从用户密码派生出强加密密钥
private static async deriveKey(password: string, salt: Uint8Array): Promise<CryptoKey> {
const encoder = new TextEncoder();
const keyMaterial = await window.crypto.subtle.importKey(
"raw",
encoder.encode(password),
{ name: "PBKDF2" },
false,
["deriveKey"]
);
return window.crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: salt,
iterations: 100000, // 10 万次迭代抵御彩虹表爆破
hash: "SHA-256",
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
// 加密数据
public static async encrypt(plainText: string, password: string): Promise<string> {
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const iv = window.crypto.getRandomValues(new Uint8Array(12)); // AES-GCM 标准 12 字节 IV
const key = await this.deriveKey(password, salt);
const encoder = new TextEncoder();
const encryptedBuffer = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
key,
encoder.encode(plainText)
);
// 将 salt, iv, 密文打包为 JSON 字符串
const payload = {
salt: Array.from(salt),
iv: Array.from(iv),
cipher: Array.from(new Uint8Array(encryptedBuffer)),
};
return JSON.stringify(payload);
}
// 解密数据
public static async decrypt(encryptedPayloadStr: string, password: string): Promise<string> {
const payload = JSON.parse(encryptedPayloadStr);
const salt = new Uint8Array(payload.salt);
const iv = new Uint8Array(payload.iv);
const cipher = new Uint8Array(payload.cipher);
const key = await this.deriveKey(password, salt);
try {
const decryptedBuffer = await window.crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv },
key,
cipher
);
const decoder = new TextDecoder();
return decoder.decode(decryptedBuffer);
} catch (err) {
throw new Error("密码错误或数据已被篡改,解密失败");
}
}
}
3. 生产环境下的关键避坑点
把数据主权彻底交还给用户,是独立开发产品赢得信任的最坚固护城河。




