@prxvt/hook
v0.2.0
Published
Privacy layer for the Agentic Commerce Protocol (ERC-8183) — AES-256-GCM encryption, ECDH key wrapping, ZK proof generation, and on-chain envelope construction for PrivacyHook
Maintainers
Readme
@prxvt/hook
Privacy SDK for ERC-8183 PrivacyHook. Handles AES-256-GCM encryption, ECDH key wrapping, ZK proof generation, IPFS upload, and on-chain envelope construction.
Install
npm install @prxvt/hook ethersQuick Start
Provider: Encrypt and Submit
import {
encryptAndPrepareSubmission,
buildConfigParams,
PinataAdapter,
} from "@prxvt/hook";
// 1. Configure privacy on-chain (at setBudget time)
const configParams = buildConfigParams(
"0x0000000000000000000000000000000000000000", // no ZK verifier
2 // require 2 wrapped keys (client + evaluator)
);
await core.connect(provider).setBudget(jobId, budget, configParams);
// 2. Encrypt and prepare submission
const ipfs = new PinataAdapter({ apiKey: "...", secretApiKey: "..." });
const plaintext = new TextEncoder().encode("Private job output");
const { deliverable, optParams } = await encryptAndPrepareSubmission(
plaintext,
[
{ publicKey: clientPubKey, label: "client" },
{ publicKey: evaluatorPubKey, label: "evaluator" },
],
ipfs
);
// 3. Submit on-chain
await core.connect(provider).submit(jobId, deliverable, optParams);Recipient: Fetch and Decrypt
import { fetchAndDecrypt, PinataAdapter } from "@prxvt/hook";
const ipfs = new PinataAdapter({ apiKey: "...", secretApiKey: "..." });
const plaintext = await fetchAndDecrypt(
cid, // IPFS CID from EncryptedSubmission event
wrappedKey, // your wrapped key entry from the event
recipientPrivateKey,
ipfs
);Standalone Key Wrapping
import { wrapKey, unwrapKey } from "@prxvt/hook";
// Wrap a data key for a recipient
const wrappedKey = wrapKey(dataKey, recipientPublicKey);
// Unwrap using the recipient's private key
const recoveredKey = unwrapKey(wrappedKey, recipientPrivateKey);ZK Proof Generation
import { generateGroth16Proof, buildConfigParams } from "@prxvt/hook";
// Configure with ZK verifier and 3 public inputs (for selective disclosure)
const configParams = buildConfigParams(zkVerifierAddress, 2, 3);
// Generate proof
const proof = await generateGroth16Proof(secretPreimage, jobId, {
wasmPath: "./circuits/build/privacy_preimage_js/privacy_preimage.wasm",
zkeyPath: "./circuits/build/privacy_preimage_final.zkey",
});API Reference
Encryption
| Function | Description |
|----------|-------------|
| encryptForRecipients(plaintext, recipients) | Encrypt data with AES-256-GCM, wrap key per recipient via ECDH |
| decryptWithPrivateKey(ciphertext, wrappedKey, privateKey) | Unwrap key and decrypt ciphertext |
Key Wrapping
| Function | Description |
|----------|-------------|
| wrapKey(dataKey, recipientPubKey, ephemeralPrivKey?) | Wrap a 32-byte key for a single recipient (returns 94-byte v1 key) |
| unwrapKey(wrappedKey, recipientPrivateKey) | Unwrap a v1 wrapped key to recover the data key |
Envelope Construction
| Function | Description |
|----------|-------------|
| buildConfigParams(zkVerifier, minWrappedKeys, numPublicInputs?) | ABI-encode setBudget optParams |
| buildEnvelopeParams(cid, wrappedKeys, zkProof?) | ABI-encode submit optParams |
| bytesToHex(bytes) | Convert Uint8Array to 0x-prefixed hex string |
CID Utilities
| Function | Description |
|----------|-------------|
| cidToBytes32(cid) | Convert IPFS CID string to bytes32 (keccak256) |
| verifyCidMatch(cid, deliverable) | Check CID maps to expected deliverable hash |
High-Level
| Function | Description |
|----------|-------------|
| encryptAndPrepareSubmission(plaintext, recipients, ipfs, zkProof?) | Full flow: encrypt → IPFS upload → build submit params |
| fetchAndDecrypt(cid, wrappedKey, privateKey, ipfs) | Full flow: IPFS download → decrypt |
ZK Proving
| Function | Description |
|----------|-------------|
| generateGroth16Proof(preimage, jobId, options) | Generate Groth16 proof for privacy_preimage circuit |
| poseidonHash(preimage) | Compute Poseidon hash matching the circuit |
| formatProofForChain(snarkjsProof) | ABI-encode proof for on-chain submission |
IPFS Adapters
| Adapter | Description |
|---------|-------------|
| PinataAdapter({ apiKey, secretApiKey, gateway? }) | Pinata pinning service |
| LocalNodeAdapter({ url? }) | Local IPFS node (Kubo HTTP API, default localhost:5001) |
Implement IpfsAdapter interface for custom backends:
interface IpfsAdapter {
upload(data: Uint8Array): Promise<string>; // returns CID
download(cid: string): Promise<Uint8Array>;
}Constants
| Constant | Value | Description |
|----------|-------|-------------|
| WRAPPED_KEY_VERSION | 0x01 | Current wrapped key format version |
| WRAPPED_KEY_V1_LENGTH | 94 | Byte length of a v1 wrapped key |
Wrapped Key Format (v1)
Each recipient gets a 94-byte wrapped key entry:
Offset Length Field
────── ────── ────────────────────────────
0 1 Version byte (0x01)
1 33 Ephemeral secp256k1 public key (compressed)
34 12 AES-GCM wrapping nonce
46 48 Encrypted AES key (32) + GCM auth tag (16)
────── ────── ────────────────────────────
Total 94 bytesKey Derivation
- Generate ephemeral secp256k1 key pair (one per envelope)
- For each recipient's public key:
- Compute ECDH shared secret
- Derive 256-bit wrapping key via HKDF-SHA256 (info=
"erc8183-privacy-wrap") - Encrypt AES data key with AES-256-GCM using the wrapping key
Versioning
The version byte enables future format evolution without breaking existing integrations. The on-chain PrivacyHook validates both the length (94 bytes) and version byte (0x01) of each wrapped key.
Crypto Details
| Component | Algorithm |
|-----------|-----------|
| Data encryption | AES-256-GCM (random 96-bit nonce) |
| Key wrapping | ECDH on secp256k1 → HKDF-SHA256 → AES-256-GCM |
| Key derivation | HKDF-SHA256 with info = "erc8183-privacy-wrap" |
| Ephemeral keys | One secp256k1 key pair per envelope |
| ZK proofs | Groth16 on BN128 (snarkjs) |
Ciphertext Format
dataNonce (12) || AES-GCM ciphertext || authTag (16)