VueBloghyhero6

关于钱包加密签名验证

2025-12-08 / 2025-12-08 / 51次浏览

写在前面,其实分析这个业务之前做了大量理论查询和论证,以及参考了大量钱包作为对比,然后算是一篇总结性的理论吧。

除了前端的架构就是浏览器框架插件那块,单拎一个签名验证出来来分析下业务,
其实熟悉了也还行,就是刚开始的时候需要一些理解。

这是我们定好的通讯协议内容
密钥协商 X25519
对称加密 AES-256GCM

很简短对吧,X25519 需要俩端自己独立计算,由插件端计算完成后,
使用 AES-256GCM 协议加密传输到后端,后端相对的拿到加密串进行解密验证

业务完成。

上面是用最简短的人话说了上面的业务,只是业务刨除什么浏览器框架那一套以及前端框架那些,这篇只暂时针对这个业务以及业务完成闭环逻辑。至于 X25519 和 AES-256GCM 这俩协议包要研究那算是另外的课题了。

好,回到整体业务,
前端插件需要实现的协议要求:
密钥协商:X25519(用于建立共享密钥)
对称加密:AES-256-GCM(用于加密实际消息)

翻译成现实应用就是:
用 X25519 生成一个 双方都能推导出的会话密钥(shared key),
再从这个 shared key 派生出一个 256-bit 对称密钥,
用 AES-256-GCM 加密所有通信。

其实细化来说,
当然是想到哪儿说哪儿
x25519 推导会话密钥, AES-256-GCM 进行加密,当然涉及到许多问题,
简单来说,在当前会话的所有消息都会采用上述加解密,当然会有一些性能问题,但是安全需要做这种端对端的加密,当然派生问题也不少其实。

比如会话密钥的使用时机

  1. 保存 AES-GCM session key(只保存在内存)
  2. 所有请求 → 用 AES-GCM 加密
  3. 所有响应 ← 用 AES-GCM 解密
    会话流程解决的是:
    身份认证
    加密通信
    数据完整性
    中间代理不可信
    防止 replay
    防止伪造请求
    防止内部系统泄露明文
    会话建立只是“安全通道建立的第一步”。
    建立好之后,你要“使用”这个通道来收发数据。

解决的主要问题还是当前会话session建立之后,我们一直使用这个会话,直到页面刷新或者时间过期当然这个都是业务层面商定的。

其实聊到主流钱包到底需不需要这种端对端通信加密的这个业务,
就举例walletconnect肯定是需要的,
因为它们的通信路径是 跨设备,例如:
网页 ↔ 手机钱包
手机 ↔ 桌面应用
DApp ↔ Relay Server ↔ Wallet
不同设备之间不共享内存
有可能经过第三方中继服务器
网络可被监听
中继服务器不可信

只要过三方中继器,那么就需要加密机制,私钥泄漏这个很严重的,而且也有第三方不可信的这么这个问题。分享的事共享密钥,第三方拿到没有对应的机制也没有办法进行加密解密,这个要配合双方私钥对应使用的。

而 metaMask 不做的理由是,
因为:
MetaMask 是本地注入,不经过网络
网页本身在浏览器沙箱内
origin 校验足够做身份认证
没有跨设备通信,也不存在中间人攻击
所有 sensitive 操作会弹 UI(用户确认)
所以 MetaMask 设计上不需要额外的 ECDH 加密层。
其实链上授权本身和这种加密通讯就是俩码事儿,根据业务选择可以做可以不做。

说白还是多签钱包的初步起始阶段,通讯内容最终使用AES-GCM-256进行一个加密流程。

┌────────────────────────────┐ ┌─────────────────────────────┐
│ 插件钱包 (Extension) │ │ 后端服务 / DApp Host │
│ - curve25519-js (X25519) │ │ - X25519 / curve25519 实现 │
│ - AES-256-GCM │ │ - AES-256-GCM │
└────────────┬───────────────┘ └─────────────┬──────────────┘
│ │
│ ① 生成本地密钥对(只在本地) │
│──────────────────────────────────────────────────▶│
│ pubWallet = generateKeyPair().public │
│ privWallet = generateKeyPair().private │
│ │
│ ② 发送自己的公钥 pubWallet │
├──────────────────────────────────────────────────▶│
│ (例如通过 postMessage / HTTP / WebSocket) │
│ │
│ │ ③ 生成对端密钥对
│ │ pubRemote, privRemote
│ │
│ ④ 返回 pubRemote │
│◀───────────────────────────────────────────────────┤
│ │
│ ⑤ 双方本地各自计算 sharedKey(同一个值) │
│ 插件: │
│ sharedKey = X25519(privWallet, pubRemote) │
│ │
│ │ 服务端:
│ │ sharedKey = X25519(privRemote, pubWallet)
│ │
│ ⑥ 各自在本地做 KDF,派生对称密钥 aesKey │
│ aesKey = SHA-256(sharedKey)[0:32] │
│ │
│ │ 同样:
│ │ aesKey = SHA-256(sharedKey)[0:32]
│ │
│ ⑦ 之后通信都走 AES-256-GCM │
│ (插件这边) │
│ - 生成随机 iv │
│ - ciphertext = AES-GCM-Encrypt(aesKey, iv, msg) │
│ - 发送 { iv, ciphertext } │
├──────────────────────────────────────────────────▶│
│ │
│ │ ⑧ 服务端用同一个 aesKey 解密:
│ │ msg = AES-GCM-Decrypt(aesKey, iv, ciphertext)
│ │
│ │ ⑨ 服务端回复时同样用 aesKey + AES-GCM 加密
│◀───────────────────────────────────────────────────┤
│ │
▼ ▼
❗ 私钥始终在插件本地 ❗ 私钥始终在服务端本地

  • privWallet 不发给任何网页 - privRemote 不发给插件
  • 助记词 / 钱包私钥也只在插件内部 - 只对等持有各自私钥

上面就是走的一个完整的流程图