爆改 Zcash 查看权(终结篇):用 100 行 JS 搞定前端“密文兵工厂”,完美联动 EVM 隐私合约!

0 阅读8分钟

引言

兄弟们,还记得我们在上一篇文章里,硬核实现的 EVM 链上 Zcash 隐私存证智能合约吗?

在 Solidity 侧,我们已经成功通过了单元测试,证明了链上存取 bytes 密文、权限控制和合规事件触发完全行得通。 “铁皮保险箱”我们已经在链上造好了!

但此时,整个架构还差最后、也是最关键的两个致命痛点:

  1. 密文从哪来? 总不能在前端裸奔传输明文到节点吧?如果数据在离开浏览器那一刻没有被死死锁住,智能合约写得再安全也是白搭!
  2. 审计方怎么看? 链上存的是一串乱码 bytes。审计方需要合规审查时,绝不可能在链上写合约去解密(因为 Gas 费能让你破产,且链上解密等于把明文公开给全网)。他们必须在链下、零 Gas 代价地还原明文。

今天这篇,就是整个 Zcash 隐私复刻项目的 “最后一块拼图”——前端纯离线密码学与合规查看权验证!

不用动用复杂的 ZK 电路,不需要任何链上交互,我们用纯前端、不到 100 行底层 JavaScript 代码,直接在客户端构筑“密文兵工厂”,产出完美无缝对齐上一篇 Solidity 合约 bytes 接收规范的 Hex 密文!


一、核心黑科技:链下重度计算,链上零感知存证

为什么说这套前端验证方案硬核?因为它完美践行了 “Zero-Trust(零信任)” 架构:核心资产明文和审计私钥,在整个生命周期里从未离开过客户端本地内存一步!

我们直接调用了原生硬件加速的 WebCrypto API 和现代密码学顶流库 @noble/curves v2.4.0,在本地跑通 ECIES(椭圆曲线集成加密方案) 混合加密模型:

  1. 非对称密钥协商 (ECDH) :前端利用以太坊原生的 secp256k1 曲线,生成高阶“临时密钥”,跟审计方的公钥隔空计算出相同的共享秘密(Shared Secret)。
  2. 密钥派生 (HKDF) :通过 HKDF-SHA256 算法把秘密打散,暴力派生出标准的 AES-GCM 高强度对称密钥。
  3. AEAD 认证加密:用自带防篡改校验(Tag)的 AES-GCM 算法瞬间加密敏感明文。
  4. 数据结构完美对齐:把 [临时公钥(65B)] + [IV(12B)] + [密文+Tag] 按严格边界拼接。这串 Hex 字符串字节边界无缝对齐 Solidity 的 bytes 类型,确保上链不报 Invalid Length!

二、纯离线验证流水线:干货源码直接带走!

新建两个文件,直接在本地 Node.js 或前端运行。这套脚本不仅验证了加密,更模拟了审计端从上一篇合约里拿到 bytes 密文后,如何纯本地离线解码。

模块一:密码学重武器库 eciesCrypto.js

// ✅ 完美适配 @noble/curves v2.4.0
import { secp256k1 } from '@noble/curves/secp256k1.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { hkdf } from '@noble/hashes/hkdf.js';
import { hexToBytes, bytesToHex } from 'viem';

// 选用环境自带的全局全局隐式 WebCrypto 引擎,保障底层跨线程稳健性
const cryptoProvider = typeof window !== 'undefined' ? window.crypto : globalThis.crypto;

/**
 * 核心逻辑:从共享密钥中通过 HKDF 派生出 AES-GCM 对称密钥
 */
async function deriveKey(sharedSecret) {
  const derivedBytes = hkdf(sha256, sharedSecret, undefined, undefined, 32);
  return await cryptoProvider.subtle.importKey(
    'raw',
    derivedBytes,
    { name: 'AES-GCM' },
    false,
    ['encrypt', 'decrypt']
  );
}

/**
 * 前端离线加密
 */
export async function encryptMessage(msgStr, recipientPublicKeyHex) {
  // 1. ✅ 完美适配 v2.x 的 临时临时私钥生成逻辑
  const ephemeralPrivKey = secp256k1.utils.randomSecretKey();
  const ephemeralPubKey = secp256k1.getPublicKey(ephemeralPrivKey, false); // 65 字节非压缩公钥

  const cleanPubKeyHex = recipientPublicKeyHex.startsWith('0x') ? recipientPublicKeyHex.slice(2) : recipientPublicKeyHex;
  const recipientPubKeyBytes = hexToBytes(`0x${cleanPubKeyHex}`);

  // 2. 计算共享密钥
  const sharedSecret = secp256k1.getSharedSecret(ephemeralPrivKey, recipientPubKeyBytes);

  // 3. 派生 AES 密钥
  const aesKey = await deriveKey(sharedSecret);

  // 4. 使用 AES-GCM 离线加密
  const iv = cryptoProvider.getRandomValues(new Uint8Array(12)); // 12 字节初始向量
  const encoder = new TextEncoder();
  const plaintextBytes = encoder.encode(msgStr);
  
  const ciphertextBuffer = await cryptoProvider.subtle.encrypt(
    { name: 'AES-GCM', iv },
    aesKey,
    plaintextBytes
  );
  const ciphertextBytes = new Uint8Array(ciphertextBuffer);

  // 5. 打包输出:临时公钥 (65字节) + IV (12字节) + 密文 (含 Tag)
  const result = new Uint8Array(ephemeralPubKey.length + iv.length + ciphertextBytes.length);
  result.set(ephemeralPubKey, 0);
  result.set(iv, ephemeralPubKey.length);
  result.set(ciphertextBytes, ephemeralPubKey.length + iv.length);

  return bytesToHex(result);
}

/**
 * 本地私钥离线解密
 */
export async function decryptMessage(encryptedHex, privateKeyHex) {
  const cleanDataHex = encryptedHex.startsWith('0x') ? encryptedHex.slice(2) : encryptedHex;
  const encryptedBytes = hexToBytes(`0x${cleanDataHex}`);
  
  const cleanPrivHex = privateKeyHex.startsWith('0x') ? privateKeyHex.slice(2) : privateKeyHex;
  const privateKeyBytes = hexToBytes(`0x${cleanPrivHex}`);

  // 1. 拆包数据
  const ephemeralPubKeyBytes = encryptedBytes.slice(0, 65);
  const iv = encryptedBytes.slice(65, 65 + 12);
  const ciphertextBytes = encryptedBytes.slice(65 + 12);

  // 2. 计算共享密钥
  const sharedSecret = secp256k1.getSharedSecret(privateKeyBytes, ephemeralPubKeyBytes);

  // 3. 派生相同的 AES 密钥
  const aesKey = await deriveKey(sharedSecret);

  // 4. 解密还原
  const plaintextBuffer = await cryptoProvider.subtle.decrypt(
    { name: 'AES-GCM', iv },
    aesKey,
    ciphertextBytes
  );

  const decoder = new TextDecoder();
  return decoder.decode(plaintextBuffer);
}

模块二:自动化跑通验证流水线 main.js

import { encryptMessage, decryptMessage } from './eciesCrypto.js';
import { secp256k1 } from '@noble/curves/secp256k1.js';
import { bytesToHex } from 'viem';

async function main() {
  console.log("🔒 正在启动 ECIES 本地离线隐私加解密验证...");

  // 1. 模拟生成接收方密钥对(✅ 完美适配 v2.x 的 randomSecretKey 规范)
  const auditorPrivateKeyBytes = secp256k1.utils.randomSecretKey();
  const auditorPrivateKey = bytesToHex(auditorPrivateKeyBytes);
  // 获取非压缩公钥 (0x04...)
  const auditorPublicKey = bytesToHex(secp256k1.getPublicKey(auditorPrivateKeyBytes, false));

  console.log(`\n[🔑 密钥对初始化成功]`);
  console.log(`- 接收方本地私钥: ${auditorPrivateKey}`);
  console.log(`- 接收方公开公钥: ${auditorPublicKey}`);

  // 2. 模拟高敏感商业隐私数据(明文)
  const businessData = JSON.stringify({
    sender: "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5",
    recipient: "0x388C818CA8B9251b393131C08a736A67ccB19297",
    shieldedAmount: "5000000000000000000000", 
    memo: "Q3 跨境大宗商品供应链核心原材料结算"
  });

  console.log(`\n[📝 待加密的原始明文]`);
  console.log(JSON.parse(businessData));

  // 3. 前端离线加密
  console.log(`\n[🔒 正在执行前端本地离线加密...]`);
  const encryptedBytesForChain = await encryptMessage(businessData, auditorPublicKey);
  
  console.log(`\n[✨ 密文生成成功!这是可以安全传给智能合约 bytes 字段的数据]`);
  console.log(`- 链上密文 Hex: ${encryptedBytesForChain}`);

  // 4. 离线解密
  console.log(`\n[🔓 正在执行本地私钥离线解密...]`);
  const decryptedData = await decryptMessage(encryptedBytesForChain, auditorPrivateKey);

  console.log(`\n[🎉 完美还原!解密后的明文如下]`);
  console.log(JSON.parse(decryptedData));
}

main().catch((error) => {
  console.error("❌ 脚本执行过程中发生严重错误:", error);
});

执行结果快照

🔒 正在启动 ECIES 本地离线隐私加解密验证...

[🔑 密钥对初始化成功]
- 接收方本地私钥: 0x75d9ffc0eb44ca1da85723b98ef3bff469f10f08f90c6bf00bf80f5347fe21be
- 接收方公开公钥: 0x0465c6d8767ae08f3fb259b48ddd9645909ca873bfdad05203cabbbc0f877dea5c9fd9cf2de59de05402c0e659b337cd2437429bd81edda3d5bb05f0020548b33f

[📝 待加密的原始明文]
{
  sender: '0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5',
  recipient: '0x388C818CA8B9251b393131C08a736A67ccB19297',
  shieldedAmount: '5000000000000000000000',
  memo: 'Q3 跨境大宗商品供应链核心原材料结算'
}

[🔒 正在执行前端本地离线加密...]

[✨ 密文生成成功!这是可以安全传给智能合约 bytes 字段的数据]
- 链上密文 Hex: 0x0419ad6072ac940503b5631365428e27c81792883b5edf05e1676cc9c7d82afe4342b8beb1659c79746169a33846fe5e23592b21df42bfe57a27185cf3a90ecd6a49c722ddc3a3dd36e8c3787e59b816ac5cfe866b8937ef86d97409d82585626144e7a9f7df6708c1ce266d5dd157aef648c4b8097eecadd8c25305124ddcd829725b18e8d6267a71c65c1c22ef3ffc34419c01d3bd497a6028a604956359b1c210666f615408f5d15fb1e1e554abf785f3a183f7f0497d21e5663cacec87b3386eacd73544ab561c43a52f0d18088801b4ef9a71ac49947eee12948be1f7736ddd59cf4c650064cedb040c3f5e3484adcd38d2942e9301f8d38fca0f494732b0daaf8f412c7bb50c94759821e4ede02f97064b91b6b01f9db13a75b34ebc5f89b1711568d7473ff900884b571cac9c6f93cd92

[🔓 正在执行本地私钥离线解密...]

[🎉 完美还原!解密后的明文如下]
{
  sender: '0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5',
  recipient: '0x388C818CA8B9251b393131C08a736A67ccB19297',
  shieldedAmount: '5000000000000000000000',
  memo: 'Q3 跨境大宗商品供应链核心原材料结算'
}

三、链上合约 + 链下前端,如何打出一套完美的组合拳?

通过这篇前端离线验证,我们和上一篇的智能合约完成了教科书级的跨平台联动:

  1. 白嫖全网节点的算力:加密在用户浏览器做,解密在审计员电脑上做。智能合约不需要运行任何非对称密码学逻辑,它只当一个 “没有感情的数据公告板” 。全网节点的 CPU 都不需要为此买单,这才是真正的合规隐私(Regulated Privacy)工程美学。
  2. 绝对的前向安全性(Forward Secrecy) :每次调用加密,前端都会在内存中揉碎并生成一个全新的临时私钥。就算未来有一笔交易的临时密钥泄露,也绝对无法逆推历史任何一笔交易!
  3. 零篡改可能(AEAD) :坚决摒弃过时的 AES-CBC 模式。我们用的 AES-GCM 自带 16 字节的 Authentication Tag。在传输中或者链上存储时,只要有人动了 Hex 密文里的哪怕一个比特,解密端会瞬间抛出异常,彻底干碎重放攻击和注入攻击!

结语

到这里, 《EVM 复刻 Zcash 隐私合约系列》全面闭环! 链上合约我们写好了“保险箱”,链下前端我们造好了“密文兵工厂”和“轻节点审计工具”。

这套方案告诉我们:在 EVM 生态里玩转可编程隐私,根本不需要复杂繁琐的链上开销,控制权早已在我们的前端代码和密码学边界中被优雅锁死。