@zkp-system/node-sdk
v2.2.0
Published
Node.js SDK for the ZKClaimUpload zero-knowledge proof middleware
Downloads
1,068
Maintainers
Readme
@zkp-system/node-sdk
Node.js / Browser SDK for the ZKClaimUpload zero-knowledge proof middleware.
Requirements
- Node.js >= 18.0.0
- Runtime dependencies:
@noble/hashes,@noble/curves(both zero-dep, audited)
Installation
npm install @zkp-system/node-sdkImport paths
| Path | Environment | Crypto engine |
|---|---|---|
| @zkp-system/node-sdk | Node.js server (API routes, CLI) | node:crypto — synchronous |
| @zkp-system/node-sdk/browser | Browser / React Native / Cloudflare Workers | crypto.subtle — async |
| @zkp-system/node-sdk/crypto | Node.js crypto utilities (named exports) | node:crypto — synchronous |
Quick Start
Server-side (Next.js API route / Node.js)
import { ZKPClient, ZKPCrypto } from '@zkp-system/node-sdk';
// 1. Generate a secp256k1 key pair (done once per user)
const { privateKey, publicKey } = ZKPCrypto.generateKeyPair();
// 2. Create a client
const client = new ZKPClient({
baseUrl: 'http://localhost:3002',
apiKey: 'your-api-key',
});
// 3. Check service health
const health = await client.health();
console.log(health.status); // 'ok'
console.log(health.chainId); // 42161
// 4. Issue a credential
const { claimFile, insertCalldata } = await client.issueCredential({
userAddress: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
fileId: '0xabcdef...', // 0x-prefixed bytes32
userPublicKey: publicKey,
});
// 5. Decrypt the credential secrets
const { n, s } = ZKPCrypto.decryptSecret(claimFile.encryptedSecret, privateKey);
// 6. Generate a ZK proof
const proofResult = await client.generateProof({
userAddress: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
fileId: claimFile.fileId,
n: n.toString(),
s: s.toString(),
leafIndex: claimFile.leafIndex,
});
// 7. Submit proofResult.calldata on-chain via ethers.js / viem
console.log(proofResult.calldata);Browser / React Native (per-user keys from wallet)
import {
deriveKeyFromSignature,
deriveFileKey,
encryptFile,
decryptFile,
decryptSecret,
} from '@zkp-system/node-sdk/browser';
// 1. Derive a deterministic key pair from the user's wallet signature
// Same wallet + same message = same key pair, every session
const sig = await wallet.signMessage('Unlock ZKP Vault v1\nThis key encrypts your files.');
const { privateKey, publicKey } = deriveKeyFromSignature(sig);
// 2. Encrypt a file before uploading (client-side, key never leaves browser)
const fileId = '0xabcdef...';
const fileKey = await deriveFileKey(privateKey, fileId);
const encrypted = await encryptFile(fileKey, new Uint8Array(fileBytes));
// 3. Decrypt a downloaded file
const sameKey = await deriveFileKey(privateKey, fileId);
const plaintext = await decryptFile(sameKey, encrypted);
// 4. Decrypt a ZK credential secret (to recover n, s for proof generation)
const { n, s } = await decryptSecret(privateKey, claimFile.encryptedSecret);Key Derivation from Wallet Signature
deriveKeyFromSignature produces a deterministic secp256k1 key pair from any wallet signature. The same wallet address signing the same message will always produce the same key pair — making it the foundation for per-user encryption where keys never leave the client.
Algorithm: privateKey = keccak256(signature_bytes) — identical to:
import { keccak256, hexToBytes } from 'viem';
const privateKey = keccak256(hexToBytes(sig));// Node.js (synchronous)
import { ZKPCrypto } from '@zkp-system/node-sdk';
const { privateKey, publicKey } = ZKPCrypto.deriveKeyFromSignature(sig);
// Browser / React Native (synchronous)
import { deriveKeyFromSignature } from '@zkp-system/node-sdk/browser';
const { privateKey, publicKey } = deriveKeyFromSignature(sig);Works with any wallet that supports personal_sign / signMessage:
| Wallet | Platform | |---|---| | MetaMask (browser extension) | Web | | WalletConnect | Web + Mobile | | Privy / Dynamic embedded wallets | Web + Mobile | | Coinbase Wallet | Web + Mobile |
File Crypto
Deterministic AES-256-GCM encryption tied to a user's private key and file ID. Each user gets a unique encryption key per file — no shared secrets.
Node.js (server-side, synchronous)
import { ZKPCrypto } from '@zkp-system/node-sdk';
const { deriveFileKey, encryptFile, decryptFile, generateFileId, computeMetaHashHex } = ZKPCrypto;
// Generate a BN254-field-valid fileId (required for on-chain registration)
const fileId = generateFileId('report.pdf', Date.now());
const metaHash = computeMetaHashHex('report.pdf', fileBytes.length, Date.now());
// Derive a 32-byte AES key unique to (user, file)
const fileKey = deriveFileKey(privateKey, fileId);
// Key = SHA-256(privateKey_bytes || fileId_bytes)
// Encrypt
const encrypted = encryptFile(fileKey, Buffer.from(fileBytes));
// Wire: IV(12) || authTag(16) || ciphertext(N)
// Decrypt
const plaintext = decryptFile(fileKey, encrypted);Browser / React Native (async)
import {
deriveFileKey,
deriveFileKeyBytes,
encryptFile,
decryptFile,
generateFileId,
computeMetaHashHex,
} from '@zkp-system/node-sdk/browser';
const fileId = await generateFileId('report.pdf', Date.now());
const metaHash = await computeMetaHashHex('report.pdf', size, Date.now());
// CryptoKey variant (for single encrypt/decrypt)
const key = await deriveFileKey(privateKey, fileId);
const encrypted = await encryptFile(key, plaintextBytes);
const decrypted = await decryptFile(key, encrypted);
// Raw bytes variant (for sharing — pass AES key to another user)
const keyBytes = await deriveFileKeyBytes(privateKey, fileId);ECIES — Encrypt / Decrypt Arbitrary Payloads
In addition to the fixed (n, s) credential encryption, the SDK supports ECIES for arbitrary byte payloads. Used for the sharing flow (re-encrypting AES file keys for grantees).
Node.js
import { ZKPCrypto } from '@zkp-system/node-sdk';
const { eciesEncryptRaw, eciesDecryptRaw } = ZKPCrypto;
// Encrypt any payload for a recipient's public key
const aesKey = ZKPCrypto.deriveFileKey(ownerPrivKey, fileId);
const wire = eciesEncryptRaw(granteePubKey, aesKey);
// Wire: ephPubKey(65) | IV(12) | ciphertext(N) | GCMtag(16)
// Decrypt
const recovered = eciesDecryptRaw(granteePrivKey, wire);Browser / React Native
import { encryptRaw, decryptRaw } from '@zkp-system/node-sdk/browser';
const wire = await encryptRaw(granteePubKey, aesKeyBytes);
const recovered = await decryptRaw(granteePrivKey, wire);Cross-platform compatible: Node
eciesEncryptRawoutput can be decrypted by browserdecryptRaw, and vice versa. Wire format is identical.
File Sharing
Re-encrypt a file's AES key for a grantee, so they can access the file using their own key pair.
Browser (owner re-encrypts for grantee)
import { reencryptForGrantee } from '@zkp-system/node-sdk/browser';
// Owner re-encrypts their AES file key for the grantee's public key
// Result is stored on-chain via ZKPClient.prepareShare()
const encryptedKeyForGrantee = await reencryptForGrantee(
ownerPrivateKey,
fileId,
granteePubKey,
);
// encryptedKeyForGrantee = ECIES(granteePubKey, SHA-256(ownerKey || fileId))
// = 125 bytes (65 + 12 + 32 + 16)Decrypt a share credential (grantee, on download)
After the owner calls prepareShare(), the grantee receives an encrypted credential bundle in the on-chain ShareGranted event.
// Node.js (server-side)
import { ZKPCrypto } from '@zkp-system/node-sdk';
const credential = ZKPCrypto.decryptShareCredential(
granteePrivKey,
encryptedCredentialHex, // from ShareGranted event
);
// credential: { n: bigint, s: bigint, leafIndex: number, encryptedKeyForGrantee: string }
// Use n, s for proof generation
const proof = await client.generateProof({ n: n.toString(), s: s.toString(), ...rest });
// Decrypt the AES file key to decrypt the file blob
const aesKey = ZKPCrypto.eciesDecryptRaw(granteePrivKey, credential.encryptedKeyForGrantee);// Browser / React Native
import { decryptShareCredential, decryptRaw } from '@zkp-system/node-sdk/browser';
const credential = await decryptShareCredential(granteePrivKey, encryptedCredentialHex);
const aesKey = await decryptRaw(granteePrivKey, credential.encryptedKeyForGrantee);Admin Client
import { AdminClient } from '@zkp-system/node-sdk';
const admin = new AdminClient({ baseUrl: 'http://localhost:3002' });
await admin.login('admin', 'password');
// Create an API key (plaintext returned once only)
const key = await admin.createKey({ name: 'my-service', description: 'Service key' });
console.log(key.key); // store securely — never returned again
// List, update, revoke keys
const keys = await admin.listKeys();
await admin.updateKey(keys[0]!.id, { active: false });
await admin.revokeKey(keys[0]!.id);
await admin.logout();Full Crypto API Reference
Node.js — ZKPCrypto (from @zkp-system/node-sdk)
All functions are synchronous.
Key management
| Function | Description |
|---|---|
| generateKeyPair() | Generate a fresh secp256k1 key pair |
| privateKeyToPublicKey(privKey) | Derive uncompressed public key from private key |
| isValidPublicKey(pubKey) | Validate a 0x04-prefixed 65-byte public key |
| deriveKeyFromSignature(sig) | Derive key pair from wallet signature via keccak256 |
ECIES
| Function | Description |
|---|---|
| encryptSecret(pubKey, n, s) | ECIES-encrypt (n, s) credential secrets |
| decryptSecret(encHex, privKey) | ECIES-decrypt → { n: bigint, s: bigint } |
| eciesEncryptRaw(pubKey, buf) | ECIES-encrypt arbitrary bytes |
| eciesDecryptRaw(privKey, encHex) | ECIES-decrypt arbitrary bytes |
File crypto
| Function | Description |
|---|---|
| deriveFileKey(privKey, fileId) | SHA-256(privKey ‖ fileId) → 32-byte Buffer |
| encryptFile(key, plaintext) | AES-256-GCM encrypt, wire: IV(12)‖tag(16)‖ciphertext |
| decryptFile(key, encrypted) | AES-256-GCM decrypt, throws on tamper |
| generateFileId(filename, ts?) | SHA-256(name+ts) mod BN254 — on-chain compatible |
| computeMetaHashHex(name, size, ts) | SHA-256(name+size+ts) mod BN254 — matches publicSignals[6] |
Sharing
| Function | Description |
|---|---|
| decryptShareCredential(privKey, encHex) | Decrypt on-chain ShareGranted credential bundle |
BN254 field utilities
| Function | Description |
|---|---|
| isValidFieldElement(n) | Returns true if n ∈ [1, BN254ScalarField) |
| assertFieldElement(n, name) | Throws ValidationError if out of range |
| parseFieldElement(s, name) | Parse decimal string → validated bigint |
| BN254_SCALAR_FIELD | 21888242871839275222246405745257275088548364400416034343698204186575808495617n |
Browser — @zkp-system/node-sdk/browser
All crypto functions are async (Web Crypto API).
No node:crypto — works in browsers, React Native, Cloudflare Workers.
Key management (sync)
| Function | Description |
|---|---|
| generateKeyPair() | Generate secp256k1 key pair via @noble/curves |
| privateKeyToPublicKey(privKey) | Derive public key |
| isValidPublicKey(pubKey) | Validate public key |
| deriveKeyFromSignature(sig) | Derive key pair from wallet signature (keccak256) |
ECIES (async)
| Function | Description |
|---|---|
| encryptSecret(pubKey, n, s) | ECIES-encrypt (n, s) |
| decryptSecret(privKey, encHex) | ECIES-decrypt → Promise<{ n, s }> |
| encryptRaw(pubKey, bytes) | ECIES-encrypt arbitrary Uint8Array |
| decryptRaw(privKey, encHex) | ECIES-decrypt arbitrary bytes |
File crypto (async)
| Function | Description |
|---|---|
| deriveFileKey(privKey, fileId) | Returns Promise<CryptoKey> for AES-GCM |
| deriveFileKeyBytes(privKey, fileId) | Returns Promise<Uint8Array> (raw 32 bytes) |
| encryptFile(key, plaintext) | AES-256-GCM encrypt → Promise<Uint8Array> |
| decryptFile(key, encrypted) | AES-256-GCM decrypt → Promise<Uint8Array> |
| generateFileId(filename, ts?) | SHA-256(name+ts) mod BN254 |
| computeMetaHashHex(name, size, ts) | SHA-256(name+size+ts) mod BN254 |
Sharing (async)
| Function | Description |
|---|---|
| decryptShareCredential(privKey, encHex) | Decrypt ShareGranted credential bundle |
| reencryptForGrantee(ownerKey, fileId, granteePub) | Re-encrypt AES key for a grantee |
Error Handling
All errors extend ZKPError with a .code string:
| Class | .code | When |
|---|---|---|
| AuthError | AUTH_ERROR | 401 Unauthorized |
| RateLimitError | RATE_LIMIT_ERROR | 429 — has .retryAfterMs |
| ValidationError | VALIDATION_ERROR | Invalid input (client-side) |
| NetworkError | NETWORK_ERROR | Timeout, DNS, connection refused |
| NotFoundError | NOT_FOUND_ERROR | 404 Not Found |
| ServerError | SERVER_ERROR | 5xx — has .statusCode |
| CryptoError | CRYPTO_ERROR | ECIES / ECDH / AES-GCM failure |
| TreeFullError | TREE_FULL | Merkle tree at capacity |
import { RateLimitError, ValidationError, CryptoError } from '@zkp-system/node-sdk';
try {
await client.generateProof(req);
} catch (e) {
if (e instanceof RateLimitError) {
await sleep(e.retryAfterMs);
} else if (e instanceof ValidationError) {
console.error('Bad input:', e.message);
} else if (e instanceof CryptoError) {
console.error('Crypto failure:', e.message);
}
}Client Options
const client = new ZKPClient({
baseUrl: 'http://localhost:3002', // required
apiKey: 'your-key', // required for issueCredential / generateProof
timeoutMs: 30_000, // default: 30s (proof generation can take ~600ms)
maxRetries: 3, // default: 3 (retries on 5xx / network errors)
retryDelayMs: 200, // default: 200ms (exponential backoff with jitter)
});
// Hot-swap API key without creating a new client instance
client.setApiKey('new-key');ECIES Wire Format
The encryptedSecret in a ClaimFile is a 157-byte ECIES blob:
[ ephPubKey: 65 bytes ] [ IV: 12 bytes ] [ ciphertext: 64 bytes ] [ GCM tag: 16 bytes ]
= 157 bytes total = 0x + 314 hex charseciesEncryptRaw / encryptRaw produce the same format for arbitrary payloads:
[ ephPubKey: 65 bytes ] [ IV: 12 bytes ] [ ciphertext: N bytes ] [ GCM tag: 16 bytes ]KDF: aesKey = SHA-256(ECDH_x_coordinate) — byte-identical to the Go middleware and the testapp's browserCrypto.ts.
File Crypto Wire Format
Encrypted file blobs use the following layout:
[ IV: 12 bytes ] [ authTag: 16 bytes ] [ ciphertext: N bytes ] ← Node.js
[ IV: 12 bytes ] [ ciphertext+tag: N+16 bytes ] ← Browser (Web Crypto appends tag)Both layouts are produced and consumed by the SDK — decryptFile handles both.
Security Notes
- Private keys are never logged by the SDK. Pass them in memory only.
deriveKeyFromSignatureproduces keys deterministically — keep your wallet's seed phrase as the ultimate backup.- JWT tokens in
AdminClientare in-memory only — never written to disk orlocalStorage. - API key plaintexts are returned once (201 response) and never stored by the SDK.
- BN254 bounds on
nandsare validated client-side before any network call. - GCM authentication: tampered ciphertext or wrong key throws
CryptoErrorimmediately. - Runtime deps (
@noble/hashes,@noble/curves): zero-dependency, audited by Cure53, used internally by viem and ethers.
Building from Source
npm install
npm run build # Clean dist/ then build ESM + CJS + .d.ts for Node + Browser
npm run build:check # TypeScript strict check without building
npm test # Run all 267 tests
npm run test:coverage # Coverage (targets: statements ≥90%, functions 100%)
npm run lint # ESLint (0 errors required)