You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 08:02:57