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

基于Angular结合硬件安全密钥(Titan、YubiKey等)实现E2EE的疑问

WebAuthn硬件密钥结合E2EE的正确实现路径

嘿,我来帮你理清楚这里的问题——首先你踩了一个WebAuthn的常见误区,咱们一步步拆解:

核心误解:WebAuthn公私钥不能直接用来做常规加解密

你当前的思路里有个关键错误:WebAuthn生成的私钥是硬件/浏览器安全存储的,永远不会暴露给前端JS代码,这是WebAuthn的核心安全设计。所以你没法直接获取私钥来解密数据,这路子走不通。

而且WebAuthn的密钥对本质是为**身份验证(签名/验证)**设计的,不是用来直接加密业务数据的。如果要结合E2EE,正确的姿势是用WebAuthn来保护你的AES对称密钥(也就是你原来用CryptoJS的那个key),而不是替换AES。

正确的实现流程(适配你的需求)

因为你原本就用AES做E2EE,我们可以把WebAuthn作为AES密钥的安全托管方案:

  1. 生成随机AES密钥:这是用来加密实际业务数据的密钥,用Web Crypto API生成比手动指定更安全:

    const aesKey = await window.crypto.subtle.generateKey(
      { name: "AES-GCM", length: 256 },
      true, // 允许导出,后续用于密钥封装
      ["encrypt", "decrypt"]
    );
    
  2. 用WebAuthn公钥封装AES密钥:
    注册硬件密钥时,要指定密钥支持「密钥封装」算法(比如RSA-OAEP或ECDH-ES),示例注册配置:

    const publicKeyOptions = {
      challenge: new Uint8Array([/* 服务器生成的随机挑战值 */]),
      rp: { name: "你的应用名称" },
      user: {
        id: new Uint8Array([/* 用户唯一标识,比如哈希后的用户ID */]),
        name: "user@example.com",
        displayName: "用户名"
      },
      pubKeyCredParams: [
        { type: "public-key", alg: -7 }, // ES256(椭圆曲线签名,用于身份验证)
        { type: "public-key", alg: -37 } // RSA-OAEP-256(用于密钥封装)
      ],
      authenticatorSelection: {
        authenticatorAttachment: "cross-platform", // 指定硬件密钥
        userVerification: "required"
      }
    };
    
    // 创建硬件密钥凭证
    const credential = await navigator.credentials.create({ publicKey: publicKeyOptions });
    // 导入公钥,用于封装AES密钥
    const publicKey = await window.crypto.subtle.importKey(
      "raw",
      credential.response.getPublicKey(),
      { name: "RSA-OAEP", hash: "SHA-256" },
      false,
      ["wrapKey"]
    );
    // 封装AES密钥(把AES密钥用公钥加密,只有对应私钥能解)
    const wrappedAesKey = await window.crypto.subtle.wrapKey(
      "raw",
      aesKey,
      publicKey,
      { name: "RSA-OAEP" }
    );
    
  3. 存储数据到数据库:
    你需要存储的内容包括:

    • credential.id:不需要加密,这是公开的凭证标识,用来后续找到对应的密钥
    • 用AES密钥加密后的业务数据(你原来的CryptoJS.AES.encrypt(data, key).toString()逻辑可以保留,建议换成Web Crypto API的AES-GCM更安全)
    • 封装后的AES密钥wrappedAesKey(转成base64格式存储)
  4. 解密流程:
    当需要解密时,先通过credential.id找到对应的wrappedAesKey,然后:

    const getOptions = {
      challenge: new Uint8Array([/* 服务器生成的新挑战值 */]),
      allowCredentials: [{
        type: "public-key",
        id: Uint8Array.from(atob(credentialId), c => c.charCodeAt(0))
      }],
      userVerification: "required"
    };
    
    // 获取硬件密钥断言(触发用户验证,比如触摸密钥)
    const assertion = await navigator.credentials.get({ publicKey: getOptions });
    // 导入私钥句柄(注意:私钥不会暴露,只是拿到可操作的安全引用)
    const privateKey = await window.crypto.subtle.importKey(
      "pkcs8",
      // 实际要从注册时存储的公钥凭证元数据中提取私钥相关材料,这里简化示例
      assertion.response.getAuthenticatorData(),
      { name: "RSA-OAEP", hash: "SHA-256" },
      false,
      ["unwrapKey"]
    );
    // 解封装得到原始AES密钥
    const aesKey = await window.crypto.subtle.unwrapKey(
      "raw",
      Uint8Array.from(atob(wrappedAesKey), c => c.charCodeAt(0)),
      privateKey,
      { name: "RSA-OAEP" },
      { name: "AES-GCM", length: 256 },
      true,
      ["decrypt"]
    );
    // 用AES密钥解密业务数据(如果用CryptoJS,需要把密钥转成它能识别的格式)
    const decryptedData = CryptoJS.AES.decrypt(encryptedData, aesKeyToString(aesKey)).toString(CryptoJS.enc.Utf8);
    

关于你的credential.id存储问题

其实credential.id不需要加密存储,它就是用来唯一标识用户硬件密钥凭证的公开值。如果出于特殊原因一定要加密它,应该用你的AES密钥加密,而不是WebAuthn公钥——因为公钥加密的内容只能通过私钥解密,但私钥拿不出来,只能通过上面的解封装流程间接处理,完全没必要绕这个弯。

额外建议

  • 尽量用原生的Web Crypto API代替CryptoJS,它更安全且是浏览器原生支持的标准实现
  • 所有WebAuthn相关的challenge(挑战值)必须由服务器生成,不能前端自行生成,防止重放攻击

内容的提问来源于stack exchange,提问作者Engineer

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 09:55:16