基于 fhEVM JS SDK 的隐私解密实战指南:从传输密钥对到链上可验证的公钥解密 📅 发布时间:2026/9/13 20:01:47 👁 浏览次数: 基于 fhEVM JS SDK 的隐私解密实战指南从传输密钥对到链上可验证的公钥解密【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevmfhEVMFully Homomorphic Encryption EVM将全同态加密引入区块链而数据从密文回到明文的过程——解密——是整个闭环的最后一环也是信任模型最敏感的一环。本文以 sdk/js-sdk/docs/decryption.md 为骨架结合仓库中sdk/js-sdk/src的实际源码实现系统讲解 fhEVM JS SDKfhevm/sdk中两类解密私有解密与公开解密的完整流程如何生成传输密钥对、签署 EIP-712 解密许可Permit、批量解密、委托解密、权限预检以及如何将公开解密结果携带 KMS 签名上链验证并给出会话持久化等生产级实践。解密的两种信任模型在 fhEVM 中解密并不是一个统一操作而是两种信任模型截然不同的操作。SDK 文档首先强调这一点因为它决定了你在哪个客户端上、携带什么凭证、以什么方式获得明文私有解密Private decryption只把值揭示给被授权的用户本人。明文会被重新加密re-encrypt到只有该用户持有的密钥下因此任何人都无法在传输过程中看到明文——包括 Relayer中继服务。它需要createFhevmClient或createFhevmDecryptClient即需要加载 TKMS约 600 KBWASM。公开解密Public decryption读取合约已显式标记为可公开解密的值例如密封拍卖的获胜出价结果对所有人公开。它在任何客户端上都可用包括最轻量的基础客户端createFhevmBaseClient不需要任何密码学 WASM。这一区别在源码层面被固化。SDK 的客户端工厂按加载的 WASM 模块区分能力clients.md 中的能力矩阵显示createFhevmClient加载 TFHE约 4.9 MB TKMS约 600 KB支持加密与私有解密createFhevmEncryptClient只加载 TFHE 仅支持加密createFhevmDecryptClient只加载 TKMS 仅支持私有解密而createFhevmBaseClient不加载任何 WASM——公开解密与签署 permit 在基础客户端上依然可用。因此一个只读公开值的页面永远不应该下载 4.9 MB 的加密模块。在客户端装饰器实现 src/core/clients/decorators/decrypt.ts 中可以看到私有解密能力decryptValue、decryptValues、decryptValuesFromPairs与generateTransportKeyPair被统一挂载到DecryptActions作为扩展模块注入客户端而公开解密与 permit 签署则位于基础动作fhevm/sdk/actions/base中因此每个客户端都有。私有解密三步完成安全读取读取一个私有加密值需要三样东西传输密钥对transport key pair——在应用内生成。KMSKey Management System用其公钥加密解密结果只有你能还原明文私钥永远不离开你的应用浏览器或 Node 进程。已签名的解密许可signed decryption permit——由值的所有者签署的 EIP-712 消息授权在有限时间窗口内对一组特定合约进行解密。要解密的加密值bytes32handle及其所属合约。核心提示permit 是可复用的。签署一次就可以在其过期前对所列合约内的任意多个值反复解密无需对每个值重新签名。第一步生成传输密钥对const transportKeyPair await client.generateTransportKeyPair(); transportKeyPair.publicKey; // BytesHex — 可安全发送会被嵌入 permit // 私钥保存在对象内部永不暴露从源码看这一步的实际执行者是 src/core/actions/decrypt/generateTransportKeyPair.ts 与底层 src/core/kms/TransportKeyPair-p.ts密钥对内部由 TKMSTfhe KMSWASM 生成SDK 先调用generateTkmsPrivateKey再序列化私钥、导出公钥十六进制串最后包装成TransportKeyPairImpl实例返回私钥被闭包与Symbol私有令牌双重保护PRIVATE_TOKEN 隐藏的[GetTkmsPrivateKeyFn]方法外部代码即使通过Object.getOwnPropertySymbols发现该 Symbol 也无法调用类的toJSON()被刻意设计为只输出公钥防止JSON.stringify(keyPair)意外泄露私钥每次生成都会记录tkmsVersion用于后续与链上 KMS 版本对齐。decryptValue动作的实现decryptValue.ts也印证了密钥对的用途它从signedPermit.encryptedDataOwnerAddress提取所有者地址将handle、合约地址、所有者地址三元组送入 KMS 解密管线最终明文由 KMS 份额在本地重建。第二步签署解密许可SDK 提供两种 permit 变体区别在于它们适用的协议版本两者都在一步内完成 EIP-712 的构建与签名参数完全相同signLegacyDecryptionPermit——V1 EIP-712 结构协议 v13 及以下。兼容所有部署除非明确需要 V2否则用它。signUnifiedDecryptionPermit——V2 统一 EIP-712 结构协议 v14 及以上。要求 SDK 为协议 API v0.14.0且链上的 KMSVerifier/ProtocolConfig 已升级到该版本——先用独立动作canUseUnifiedDecryptionPermit来自fhevm/sdk/actions/base检查。signer在这里传入——这是客户端唯一触碰钱包的地方。import { canUseUnifiedDecryptionPermit } from fhevm/sdk/actions/base; const now Math.floor(Date.now() / 1000); const params { transportKeyPair, contractAddresses: [0xYourContract…], startTimestamp: now, durationSeconds: 7 * 24 * 60 * 60, // 7 天 signerAddress: await signer.getAddress(), signer, // ethers Signer或 viem Account / WalletClient }; const signedPermit (await canUseUnifiedDecryptionPermit(client)) ? await client.signUnifiedDecryptionPermit(params) : await client.signLegacyDecryptionPermit(params);参数明细两个签名函数完全一致参数类型说明transportKeyPairTransportKeyPair来自第一步其公钥被绑定进 permit。contractAddressesreadonly string[]该 permit 授权解密的每个合约。startTimestampnumberUnix 秒级时间戳1970-01-01 起。permit 何时开始生效。durationSecondsnumber有效期长度单位是秒。signerAddressstring签署方地址——通常是值的所有者。signer原生 signerethersSigner/ viemAccount或WalletClient。delegatorAddressstring可选仅委托解密时出现——见下文。警告durationSeconds是秒而不是天。一周的 permit 要传7 * 24 * 60 * 60。提示signerAddress不一定是普通 EOA。如果是智能合约钱包如 SafeSDK 会在把签名发给 KMS 之前通过 ERC-1271isValidSignature验证结果签名而非ecrecover——无需额外配置。这正符合 signLegacyDecryptionPermit.ts 中参数类型signer: NativeSigner的设计。危险提示client.signDecryptionPermit仍然存在但已deprecated——它是signLegacyDecryptionPermit的别名。新代码应显式调用signLegacyDecryptionPermit或signUnifiedDecryptionPermit。底层实现 SignedDecryptionPermit-p.ts 也印证了这一点公共封装signDecryptionPermit被刻意固定在 V1注释明确说明是为了兼容尚未迁移的调用方。permit 可复用签一次然后在过期前对所列合约内的多个值反复解密。signedPermit.assertNotExpired()在过期时会抛出异常。第三步解密const decrypted await client.decryptValue({ transportKeyPair, encryptedValue, // 从合约读到的 bytes32 handle contractAddress: 0xYourContract…, signedPermit, }); decrypted.value; // 42 (number)、1000n (bigint)、true 或 0x… (address) decrypted.type; // uint32、bool、address…Solidity 值类型名结果是TypedValue。加密类型到 JavaScript 类型的映射加密类型typevalueeuint8/16/32对应类型numbereuint64/128/256对应类型biginteboolboolbooleaneaddressaddress校验和格式字符串明文是在本地从 KMS 份额重建的——绝不以明文形式在网络上传输。批量解密减少签名与网络往返两种批量变体可以避免逐个值签名和网络往返// 同一合约内的多个值 const results await client.decryptValues({ transportKeyPair, contractAddress: 0xYourContract…, encryptedValues: [handleA, handleB, handleC], signedPermit, }); // 分布在多个不同合约的值 const results await client.decryptValuesFromPairs({ transportKeyPair, pairs: [ { encryptedValue: handleA, contractAddress: 0xContractA… }, { encryptedValue: handleB, contractAddress: 0xContractB… }, ], signedPermit, // 必须列出上面引用的每个合约 });两者都按输入顺序返回readonly TypedValue[]。从源码看decryptValuesdecryptValues.ts内部会把同一合约的多个 handle 统一带上ownerAddress构造 pair 列表再交给底层的decryptValuesFromPairs管线——因此批量与单值走的是同一条 KMS 解密路径只是请求合并为一次而decryptValue本质上就是只有一个 pair 的decryptValuesFromPairs见 decryptValue.ts 第 47-51 行的转发。什么可以被解密EncryptedValueLike你传入的encryptedValue是一个bytes32handle。SDK 接受多种形态EncryptedValueLike十六进制字符串例如从合约 getter 读到的值32 字节的Uint8ArrayEncryptedValuehandle 对象。典型做法是直接从合约调用中读取 handle。例如读取一个加密计数器const rawCount await counter.getCount(); // uint256 getter 返回 bigint const countHex 0x rawCount.toString(16).padStart(64, 0); const decrypted await client.decryptValue({ transportKeyPair, encryptedValue: countHex, contractAddress, signedPermit, });解密前的权限预检canDecrypt*如果 ACLAccess Control List访问控制列表不允许解密调用会失败。为了提前检查——例如把reveal按钮置灰——可以使用canDecrypt*动作。它们返回布尔值加明细权限不通过时绝不抛异常。这些是独立动作而非客户端方法需要导入并把客户端作为第一个参数传入import { canDecryptValue } from fhevm/sdk/actions/decrypt; const { allowed, details } await canDecryptValue(client, { encryptedValue, contractAddress, signedPermit, // 或userAddress: 0x… }); allowed; // boolean details.contractAllowed; // 该合约是否被允许持有这个值 details.userAllowed; // 该用户是否被允许解密它复数形式canDecryptValues与canDecryptValuesFromPairs同样来自fhevm/sdk/actions/decrypt与批量解密方法一一对应。从 canDecryptValue.ts 的源码注释可以看到其完整的预检语义它检查链上 ACL 对两件事的授权contractAddress对encryptedValue的访问以及目标用户对encryptedValue的访问传入signedPermit时额外检查 permit 的结构有效性、当前时间是否在有效窗口内、permit 是否限定在请求的contractAddress同时传入signedPermit与transportKeyPair时还会校验 permit 是否绑定到对应的transportKeyPair.publicKeypermit 限定于用户、合约地址、传输公钥、有效时间窗口但不限定于单个 encryptedValue——值级别的授权始终由链上 ACL 单独决定。委托解密Delegated decryption委托允许一个账户解密另一个账户拥有的值——例如一个服务代表链上已授权的用户执行解密。用委托方的signer签署 permit并在delegatorAddress中指明所有者。signLegacyDecryptionPermit与signUnifiedDecryptionPermit的用法与上文完全一致——委托在两者上是同一个delegatorAddress参数const signedPermit await client.signLegacyDecryptionPermit({ transportKeyPair, contractAddresses: [0xYourContract…], startTimestamp: now, durationSeconds: 24 * 60 * 60, signerAddress: delegateAddress, // 委托方签署 signer: delegateSigner, delegatorAddress: ownerAddress, // 要解密谁的值 });生成的 permit 会报告isDelegated: true。用它解密的方式与上文完全相同。委托本身必须已经在链上 ACL 中被授予签署 permit 只是授权这次请求。这一点在底层 SignedDecryptionPermit-p.ts 的signDecryptionPermit文档注释中也有对应说明提供delegatorAddress时创建的是允许 signer 解密属于delegatorAddress账户的加密值的委托 permit否则创建 signer 解密自己值的标准 permit。公开解密当合约将一个值标记为可公开解密时——例如密封拍卖的获胜出价这是一次所有人都该看到的机密计算结果——用公开方法读取它。不需要传输密钥对也不需要 permit。// 单个值 const value await client.decryptPublicValue({ encryptedValue }); value.value; // 解出的明文 value.type; // uint32、bool… // 批量 const values await client.decryptPublicValues({ encryptedValues: [handleA, handleB], });两者返回的TypedValue(s) 与私有解密结果形状完全一致。从源码看公开解密的管线publicDecrypt.ts与私有解密有一个关键差异它不要求用户提供 EIP-712 签名。SDK 可以透明地为用户读取当前 KMS signers 上下文并构建extraData源码注释明确指出 the publicDecrypt doesnt require for an EIP-712 signature from the user。该管线还会执行一系列防御性校验至少一个 handle、2048 bits 位数上限assertKmsDecryptionBitLimit、所有 handle 属于当前 host chainId、以及 ACL 权限检查之后才调用 Relayer 获取 KMS 份额并在本地验证签名后构造PublicDecryptionProof。在链上验证公开解密结果要向合约证明某个 handle 解密为某个特定明文值使用decryptPublicValuesWithSignatures。它返回 KMS 签名以及验证合约所期望的确切参数const { clearValues, checkSignaturesArgs } await client.decryptPublicValuesWithSignatures({ encryptedValues: [handle], }); clearValues; // 解出的明文 TypedValue[] checkSignaturesArgs.handlesList; // 所有 handle checkSignaturesArgs.abiEncodedCleartexts; // ABI 编码的明文值 checkSignaturesArgs.decryptionProof; // KMS 门限签名quorum signatures把checkSignaturesArgs传给合约的验证函数合约就能确认 KMS 门限已对这些明文值作出证明。对应实现 decryptPublicValuesWithSignatures.ts 中checkSignaturesArgs的类型定义明确包含handlesList非空HandleBytes32Hex数组、abiEncodedCleartextsBytesHex与decryptionProofBytesHex三者被Object.freeze冻结后返回。GLOSSARYGLOSSARY.md将其中的decryptionProof定义为 KMS 公开解密证明包含 KMS 签名、关联元数据以及链上验证所需的上下文。持久化解密会话传输密钥对和已签名的 permit 都可以序列化为普通对象——可用于在页面刷新间缓存解密会话使用户不必每次访问都重新签名。// 序列化异步——两者都会先解析客户端的协议上下文 const kp await client.serializeTransportKeyPair({ transportKeyPair }); const permit await client.serializeSignedDecryptionPermit({ signedPermit }); // 持久化 kp 和 permit例如存入 storage // 稍后恢复 const transportKeyPair await client.parseTransportKeyPair(kp); const signedPermit await client.parseSignedDecryptionPermit({ serializedPermit: permit, transportKeyPair, });序列化实现中有两个值得注意的安全设计TransportKeyPair-p.ts 与 SignedDecryptionPermit-p.ts序列化后的传输密钥对包含私钥——serializeTransportKeyPair的 JSDoc 直接标注The output contains sensitive key material — handle and store securelypermit 序列化时会把 EIP-712 domain 中的 bigintchainId转为十进制字符串保证产物能安全通过JSON.stringify()/JSON.parse()如存入 localStorageparseSignedDecryptionPermit再将其还原为 bigint。同时toJSON()被刻意不放在 permit 类上防止JSON.stringify(permit)意外序列化敏感数据。危险提示序列化的传输密钥对包含私钥。请把它当作秘密对待绝不写入日志、绝不嵌入 URL、绝不发送到服务器。只存放在你会存放会话密钥的地方。相关文档Encryption——生成你读回来的加密值。Types——TransportKeyPair、SignedDecryptionPermit、TypedValue的类型定义。Actions——以独立函数形式提供的相同操作如fhevm/sdk/actions/decrypt与fhevm/sdk/actions/base的完整导出清单。Error handling——AclUserDecryptionError、AclPublicDecryptionError等错误类型。Clients——四类客户端工厂的 WASM 加载与能力矩阵。GLOSSARY——公开解密、DecryptionProof等术语的精确定义。【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考