使用Web Crypto API生成RSA密钥对后,如何安全持久存储以跨会话保留?
安全持久化Web Crypto RSA密钥对方案
要实现密钥对跨页面刷新、登出后仍保留,首先得解决两个核心问题:密钥可导出(你的原始代码中extractable设为false,无法导出保存),以及安全存储导出后的密钥数据。以下是几种可行方案,按安全性和实用性排序:
1. IndexedDB存储(前端首选方案)
IndexedDB是浏览器提供的异步、大容量存储方案,同源隔离,适合存储敏感程度中等的密钥数据。步骤如下:
第一步:修改密钥生成代码,允许导出
将generateKey的第二个参数改为true,否则无法导出密钥:
const keyPair = await window.crypto.subtle.generateKey( { name: 'RSASSA-PKCS1-v1_5', modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, true, // 允许导出密钥,才能持久化 ['sign', 'verify'] );
第二步:导出密钥为JWK格式
JWK(JSON Web Key)是适合Web环境的密钥序列化格式:
// 导出私钥和公钥为JWK const privateKeyJwk = await window.crypto.subtle.exportKey('jwk', keyPair.privateKey); const publicKeyJwk = await window.crypto.subtle.exportKey('jwk', keyPair.publicKey);
第三步:封装IndexedDB存储/加载逻辑
// 存储密钥对到IndexedDB async function saveKeyPair(privateJwk, publicJwk) { const db = await new Promise((resolve, reject) => { const request = indexedDB.open('UserKeyStore', 1); request.onupgradeneeded = (e) => { const dbInstance = e.target.result; if (!dbInstance.objectStoreNames.contains('rsaKeys')) { dbInstance.createObjectStore('rsaKeys', { keyPath: 'id' }); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); const transaction = db.transaction('rsaKeys', 'readwrite'); const store = transaction.objectStore('rsaKeys'); await store.put({ id: 'primary', privateKey: privateJwk, publicKey: publicJwk }); await transaction.complete; db.close(); } // 从IndexedDB加载并导入密钥对 async function loadKeyPair() { const db = await new Promise((resolve, reject) => { const request = indexedDB.open('UserKeyStore', 1); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); const transaction = db.transaction('rsaKeys', 'readonly'); const store = transaction.objectStore('rsaKeys'); const keyData = await new Promise((resolve, reject) => { const request = store.get('primary'); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); db.close(); if (!keyData) return null; // 导入私钥 const privateKey = await window.crypto.subtle.importKey( 'jwk', keyData.privateKey, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['sign'] ); // 导入公钥 const publicKey = await window.crypto.subtle.importKey( 'jwk', keyData.publicKey, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['verify'] ); return { privateKey, publicKey }; }
第四步:初始化密钥对(优先加载,不存在则生成)
async function initUserKeyPair() { let keyPair = await loadKeyPair(); if (!keyPair) { // 生成新密钥对 keyPair = await window.crypto.subtle.generateKey( { name: 'RSASSA-PKCS1-v1_5', modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, true, ['sign', 'verify'] ); // 保存到IndexedDB const privateJwk = await window.crypto.subtle.exportKey('jwk', keyPair.privateKey); const publicJwk = await window.crypto.subtle.exportKey('jwk', keyPair.publicKey); await saveKeyPair(privateJwk, publicJwk); } return keyPair; } // 使用示例 const userKeyPair = await initUserKeyPair();
2. 后端加密存储(最高安全等级)
如果密钥涉及敏感操作(如签名重要数据),不建议将私钥存在前端。最优方案是:
- 用用户登录密码生成AES对称密钥,加密私钥后上传到后端
- 后端将加密后的私钥与用户ID关联存储
- 用户下次登录时,用密码生成对称密钥,从后端获取加密私钥并解密导入
核心代码示例(加密私钥)
// 用用户密码生成AES密钥 async function generateAesKeyFromPassword(password, salt) { const encoder = new TextEncoder(); const passwordBytes = encoder.encode(password); const saltBytes = encoder.encode(salt); // 用PBKDF2派生密钥 const keyMaterial = await window.crypto.subtle.importKey( 'raw', passwordBytes, { name: 'PBKDF2' }, false, ['deriveKey'] ); return window.crypto.subtle.deriveKey( { name: 'PBKDF2', salt: saltBytes, iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ); } // 加密私钥JWK async function encryptPrivateKey(privateJwk, aesKey) { const encoder = new TextEncoder(); const plaintext = encoder.encode(JSON.stringify(privateJwk)); const iv = window.crypto.getRandomValues(new Uint8Array(12)); // AES-GCM标准IV长度 const ciphertext = await window.crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv }, aesKey, plaintext ); // 返回IV和加密后的数据(转成Base64方便传输存储) return { iv: btoa(String.fromCharCode(...iv)), ciphertext: btoa(String.fromCharCode(...new Uint8Array(ciphertext))) }; }
3. LocalStorage(仅适合存公钥)
LocalStorage是同步字符串存储,容易被XSS攻击窃取,绝对不要存私钥。如果只是需要持久化公钥,可以用:
// 存储公钥 const publicJwk = await window.crypto.subtle.exportKey('jwk', keyPair.publicKey); localStorage.setItem('user-rsa-public-key', JSON.stringify(publicJwk)); // 加载公钥 const publicJwkStr = localStorage.getItem('user-rsa-public-key'); if (publicJwkStr) { const publicKey = await window.crypto.subtle.importKey( 'jwk', JSON.parse(publicJwkStr), { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['verify'] ); }
关键安全注意事项
- XSS防护:启用CSP(内容安全策略),避免内联脚本,过滤所有用户输入,防止恶意脚本窃取存储的密钥。
- 私钥加密:即使存在前端,也要用用户密码或生物识别加密私钥后再存储,避免设备丢失后密钥泄露。
- 密钥轮换:提供密钥轮换功能,允许用户生成新密钥对替换旧的,降低长期使用单一密钥的风险。
- HTTPS传输:如果涉及后端存储,必须用HTTPS加密传输数据,防止中间人攻击。
内容的提问来源于stack exchange,提问作者Faraz
相关产品推荐
相关产品推荐

