前端侧加密:会话全程中密码应存储于何处?
针对你这个无账户、零知识加密存储的需求,我分享一套经过实践验证的方案,核心是让密钥完全掌握在用户手里,后端只做「存管」不碰明文:
核心原则:零知识加密
要实现「应用维护者无法解密」,必须遵循零知识架构:
- 用户的加密密钥完全在前端生成、使用,绝不传输到后端
- 后端仅存储加密后的密文、加密辅助信息(如盐、IV),永远接触不到明文数据或密钥
具体实现步骤
1. 密钥生成:从用户密码派生加密密钥
因为没有账户系统,密钥必须由用户输入的密码派生而来,这里一定要用慢哈希算法防止暴力破解:
- 推荐用
PBKDF2(浏览器兼容性好)或Argon2(更安全,需要前端库支持) - 生成密钥时要加随机盐(每次派生都用新盐,盐和密文一起存在后端)
- 迭代次数要足够高(PBKDF2至少10万次,Argon2用官方推荐参数)
前端示例代码(用Web Crypto API):
async function getAesKeyFromPassword(userPassword) { const encoder = new TextEncoder(); const passwordBytes = encoder.encode(userPassword); // 生成随机盐,后续要和密文一起存储 const salt = crypto.getRandomValues(new Uint8Array(16)); // 导入密码作为密钥材料 const keyMaterial = await crypto.subtle.importKey( "raw", passwordBytes, { name: "PBKDF2" }, false, ["deriveKey"] ); // 派生AES-GCM密钥(256位,支持加密解密) const aesKey = await crypto.subtle.deriveKey( { name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"] ); return { aesKey, salt: btoa(String.fromCharCode(...salt)) }; }
2. 前端加密流程
JS图形编辑器生成JSON后,在前端完成加密再传给后端:
- 用AES-GCM算法(自带完整性校验,防止密文被篡改)
- 每次加密生成随机IV(初始化向量,12字节是GCM推荐长度)
- 将密文、IV、盐一起转成Base64(方便传输和存储)
前端加密示例:
async function encryptGraphJson(graphData, aesKey) { const encoder = new TextEncoder(); const dataBytes = encoder.encode(JSON.stringify(graphData)); const iv = crypto.getRandomValues(new Uint8Array(12)); // 执行加密 const encryptedBuffer = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, aesKey, dataBytes ); // 转成Base64字符串 const ciphertext = btoa(String.fromCharCode(...new Uint8Array(encryptedBuffer))); const ivBase64 = btoa(String.fromCharCode(...iv)); return { ciphertext, iv: ivBase64 }; }
3. Rails后端的职责
后端只需要提供简单的存储接口,完全不涉及加密解密逻辑:
- 创建一个
encrypted_graphs表,字段包括:ciphertext:text(存加密后的JSON)、iv:string(加密用的IV)、salt:string(密码派生用的盐)、unique_identifier:string(可选,用户用来标识自己的图形,比如随机生成的ID) - 提供创建、查询、更新接口,接收前端传来的加密字段并存储
Rails模型示例:
# app/models/encrypted_graph.rb class EncryptedGraph < ApplicationRecord validates :ciphertext, :iv, :salt, presence: true # 可以加唯一索引防止重复,或者用uuid作为主键 end
4. 数据恢复流程
用户下次访问时,输入相同密码:
- 前端用密码+后端返回的盐重新派生AES密钥
- 从后端拉取对应图形的密文和IV
- 前端解密得到原始JSON,传给图形编辑器加载
前端解密示例:
async function decryptGraphJson(ciphertext, ivBase64, aesKey) { const decoder = new TextDecoder(); const iv = new Uint8Array(atob(ivBase64).split('').map(char => char.charCodeAt(0))); const encryptedBuffer = new Uint8Array(atob(ciphertext).split('').map(char => char.charCodeAt(0))); // 执行解密 const decryptedBuffer = await crypto.subtle.decrypt( { name: "AES-GCM", iv }, aesKey, encryptedBuffer ); return JSON.parse(decoder.decode(decryptedBuffer)); }
关键注意事项
- 绝对不要把密码或密钥传给后端:所有密钥派生、加密解密操作都在前端完成,后端只处理密文
- 避免XSS风险:密钥存在浏览器内存中,要及时清除(比如操作完成后将密钥对象设为
null),同时确保页面没有XSS漏洞,防止密钥被窃取 - 密码强度提醒:必须提示用户使用强密码(长度≥12位,包含大小写、数字、特殊字符),否则即使加密了也可能被暴力破解
- 备份提示:因为没有账户,用户忘记密码就永远无法恢复数据,要在界面上提示用户备份自己的密码,或者提供密钥分片备份功能(比如让用户下载密钥分片,自己存储)
内容的提问来源于stack exchange,提问作者A. Neumann
相关产品推荐
相关产品推荐

