前端怎么安全地交换加密密钥?

程序猿贝贝 阅读 46

我在做一个需要前端加密用户数据的功能,但卡在密钥交换这一步了。后端给的 API 返回一个公钥,我用 crypto.subtle.encrypt() 加密数据发过去,但每次刷新页面密钥就变了,没法解密之前的数据。

我看别人用 ECDH 做密钥协商,但在浏览器里试了下根本拿不到对方私钥啊?是不是理解错了?现在临时方案是把密钥存在 localStorage,但这明显不安全……

这是我现在生成密钥对的代码:

const keyPair = await crypto.subtle.generateKey(
  { name: 'ECDH', namedCurve: 'P-256' },
  true,
  ['deriveKey']
);
const publicKey = await crypto.subtle.exportKey('spki', keyPair.publicKey);

问题是:前端和后端之间到底该怎么安全地协商出一个共享密钥,还能保证用户换设备也能解密历史数据?

我来解答 赞 5 收藏
二维码
手机扫码查看
2 条解答
极客朝阳
你理解错了。ECDH 是密钥协商,不是直接用来加密的。后端给你公钥,前端用自己私钥+后端公钥 derive 出共享密钥,双方各自算出同样的 key 才能加解密。

简单做法:前端生成密钥对,把公钥发给后端存起来。后端用前端公钥+自己私钥 deriveKey,前端用前端私钥+后端公钥 deriveKey,算出来的 AES 密钥两边一样。加密用 AES-GCM。
// 前端:生成密钥对,发公钥给后端
const keyPair = await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
true, ['deriveKey']
);
const publicKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
// 发给后端:POST /save-public-key { userId, publicKey: publicKeyJwk }

// 前端:后端返回它的公钥后,derive 共享密钥
const peerPublicKey = await crypto.subtle.importKey(
'jwk', serverPublicKeyJwk,
{ name: 'ECDH', namedCurve: 'P-256' },
true, []
);
const sharedKey = await crypto.subtle.deriveKey(
{ name: 'ECDH', public: peerPublicKey },
keyPair.privateKey,
{ name: 'AES-GCM', length: 256 },
false, ['encrypt', 'decrypt']
);

// 加密
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
sharedKey,
new TextEncoder().encode('敏感数据')
);


后端用对应语言实现相同逻辑:用前端公钥+自己私钥 deriveKey,算出来一样的 sharedKey。

换设备解密历史数据这事儿,存 localStorage 确实不安全。正确做法是后端存用户公钥,新设备登录后从后端拉取历史公钥列表,遍历尝试 derive 出正确的共享密钥去解密。或者用用户密码做 AES 密钥的额外加密层。
点赞
2026-03-14 08:20
Newb.文婷
你理解错了一件事。ECDH 是用来协商共享密钥的,不是直接加解密的。

简单说:每次刷新生成新密钥对没问题,但你得把私钥存下来(存 localStorage 就行,用用户密码再加密一层更稳妥)。然后用你的私钥+后端公钥 deriveKey 得到共享密钥,加密数据发过去。后端用它的私钥+你的公钥也能 derive 出同一个共享密钥。

跨设备解密的问题:把你前端生成的私钥用用户密码加密后存到后端数据库,换设备时用户输入密码就能恢复私钥,然后继续跟后端协商。
// 存私钥
const privateKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
localStorage.setItem('privateKey', JSON.stringify(privateKeyJwk));

// 用后端公钥派生共享密钥
const sharedKey = await crypto.subtle.deriveKey(
{ name: 'ECDH', public: 后端公钥 },
keyPair.privateKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
点赞
2026-03-12 09:22