Cross-Tool Workflow Guide
Learn how to use zkenc-cli and zkenc-js together for maximum flexibility and power.
Why Use Both Tools?
Combining zkenc-cli and zkenc-js enables powerful workflows:
- Encrypt on server, decrypt in browser
- CLI for batch processing, JS for UI
- Different environments, same ciphertexts
- Leverage strengths of each tool
Compatibility
zkenc-cli and zkenc-js are fully compatible and use the same combined ciphertext format:
✅ Files encrypted with CLI can be decrypted with JS
✅ Files encrypted with JS can be decrypted with CLI
✅ Same circuit files work with both tools
✅ Same input format for both tools
✅ No file format conversion needed
Both tools use the same combined format:
[1 byte flag][4 bytes witness CT length][witness ciphertext]
[4 bytes public input length (if flag=1)][public input JSON (if flag=1)]
[encrypted message]
Workflow 1: CLI Encrypt → JS Decrypt
Use case: Encrypt sensitive files on a server, decrypt in a web application.
Step 1: Prepare Circuit (CLI)
# Compile circuit
circom circuit.circom --r1cs --wasm -o build
# You'll need:
# - build/circuit.r1cs (for both CLI and JS)
# - build/circuit_js/circuit.wasm (for both CLI and JS)
Step 2: Create Public Inputs (CLI)
Create public_inputs.json:
{
"publicValue": "42"
}
Step 3: Encrypt with CLI
# One-step encryption (recommended)
zkenc encrypt \
--circuit build/circuit.r1cs \
--input public_inputs.json \
--message secret.txt \
--output encrypted.bin
The output encrypted.bin is a combined ciphertext that includes:
- Witness encryption ciphertext
- Public inputs (embedded by default)
- AES-encrypted message
File sizes:
encrypted.bin(combined) ≈ witness CT (1576 bytes) + public inputs + message + overhead
Step 4: Decrypt with JS
import { zkenc } from "zkenc-js";
import fs from "fs/promises";
// Load combined ciphertext
const ciphertext = await fs.readFile("encrypted.bin");
// Load circuit files
const circuitFiles = {
r1csBuffer: await fs.readFile("build/circuit.r1cs"),
wasmBuffer: await fs.readFile("build/circuit_js/circuit.wasm"),
};
// Prepare full inputs (public + private)
const fullInputs = {
publicValue: 42,
privateValue: 123, // The witness
};
// Decrypt in one step
const decrypted = await zkenc.decrypt(circuitFiles, ciphertext, fullInputs);
console.log(new TextDecoder().decode(decrypted));
// Output: (contents of secret.txt)
That's it! No file conversion needed.
Workflow 2: JS Encrypt → CLI Decrypt
Use case: Encrypt in browser, process/decrypt on server.
Step 1: Encrypt with JS
import { zkenc } from "zkenc-js";
const circuitFiles = {
r1csBuffer: await fetch("/circuits/circuit.r1cs")
.then((r) => r.arrayBuffer())
.then((b) => new Uint8Array(b)),
wasmBuffer: await fetch("/circuits/circuit.wasm")
.then((r) => r.arrayBuffer())
.then((b) => new Uint8Array(b)),
};
const publicInputs = { publicValue: "42" };
const message = new TextEncoder().encode("Secret from browser");
// One-step encryption
const { ciphertext } = await zkenc.encrypt(circuitFiles, publicInputs, message);
// Download ciphertext
const blob = new Blob([ciphertext]);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "encrypted.bin";
a.click();
The ciphertext is already in the combined format that CLI can read directly.
Step 2: Generate Witness (CLI)
Create full inputs full_inputs.json:
{
"publicValue": "42",
"privateValue": "123"
}
Generate witness using snarkjs:
snarkjs wtns calculate \
build/circuit_js/circuit.wasm \
full_inputs.json \
witness.wtns
Step 3: Decrypt with CLI
# One-step decryption
zkenc decrypt \
--circuit build/circuit.r1cs \
--witness witness.wtns \
--ciphertext encrypted.bin \
--output decrypted.txt
cat decrypted.txt
# Output: Secret from browser
That's it! The CLI can read the JS-encrypted file directly.
Workflow 3: Hybrid Processing
Use case: Use CLI for batch operations, JS for interactive UI.
Example: Photo Encryption Service
Server (CLI):
#!/bin/bash
# encrypt-photos.sh
for photo in uploads/*.jpg; do
echo "Processing $photo..."
# Generate unique public input
PUBLIC_VALUE=$(date +%s)
echo "{\"timestamp\": \"$PUBLIC_VALUE\"}" > inputs.json
# Encrypt in one step
zkenc encrypt \
--circuit circuit.r1cs \
--input inputs.json \
--message "$photo" \
--output "${photo}.enc"
# Store metadata
echo "$photo,$PUBLIC_VALUE" >> metadata.csv
rm inputs.json
done
Client (JS):
// Decrypt selected photo
async function decryptPhoto(photoId: string, privateValue: number) {
// Fetch encrypted photo (combined format)
const response = await fetch(`/api/photos/${photoId}/encrypted`);
const ciphertext = new Uint8Array(await response.arrayBuffer());
// Get public value from metadata
const metadata = await fetch(`/api/photos/${photoId}/metadata`).then((r) =>
r.json()
);
// Decrypt in one step
const fullInputs = {
timestamp: metadata.timestamp,
userSecret: privateValue,
};
const decrypted = await zkenc.decrypt(circuitFiles, ciphertext, fullInputs);
// Display photo
const blob = new Blob([decrypted], { type: "image/jpeg" });
const url = URL.createObjectURL(blob);
imageElement.src = url;
}
Note: Public inputs can be extracted from ciphertext using getPublicInput() if embedded:
import { getPublicInput } from "zkenc-js";
// Extract embedded public inputs
const publicInputs = getPublicInput(ciphertext);
console.log(publicInputs.timestamp); // No need to fetch metadata!
Workflow 4: Multi-Platform Distribution
Use case: Encrypt once, decrypt on any platform.
Setup
# Compile circuit
circom puzzle.circom --r1cs --wasm -o dist
# Create distribution package
mkdir -p package/circuits
cp dist/puzzle.r1cs package/circuits/
cp dist/puzzle_js/puzzle.wasm package/circuits/
cp README.md package/
Encrypt Once
# Create puzzle
cat > puzzle.json <<EOF
{
"puzzle": ["5", "3", "0", "0", "7", "0", "0", "0", "0"]
}
EOF
# Encrypt message (creates combined format)
zkenc encrypt \
--circuit package/circuits/puzzle.r1cs \
--input puzzle.json \
--message treasure.txt \
--output package/treasure.enc
Distribute
package/
├── circuits/
│ ├── puzzle.r1cs # Circuit file
│ └── puzzle.wasm # Witness generator
├── treasure.enc # Combined ciphertext (works with both tools!)
└── README.md # Instructions
Users Can Decrypt With Either Tool
CLI User:
# Generate solution witness
cat > solution.json <<EOF
{
"puzzle": ["5", "3", "0", ...],
"solution": ["5", "3", "4", "6", "7", "8", "9", "1", "2", ...]
}
EOF
snarkjs wtns calculate puzzle.wasm solution.json solution.wtns
# Decrypt directly
zkenc decrypt \
--circuit puzzle.r1cs \
--witness solution.wtns \
--ciphertext treasure.enc \
--output treasure.txt
JS User:
// Load the same encrypted file
const ciphertext = await fetch('treasure.enc')
.then(r => r.arrayBuffer())
.then(b => new Uint8Array(b));
const solution = {
puzzle: ["5", "3", "0", ...],
solution: ["5", "3", "4", "6", "7", "8", "9", "1", "2", ...],
};
// Decrypt directly
const treasure = await zkenc.decrypt(circuitFiles, ciphertext, solution);
No conversion needed! Both tools read the same file format.
Advanced: Using Low-Level API
For advanced use cases, you can still use the low-level encap/decap commands separately:
CLI Low-Level Commands
# Step 1: Generate witness ciphertext and key
zkenc encap \
--circuit circuit.r1cs \
--input public.json \
--ciphertext witness.ct \
--key key.bin
# Step 2: Encrypt with any AES tool or custom implementation
# (key.bin is a 32-byte key suitable for AES-256)
# Step 3: Decrypt - recover key
zkenc decap \
--circuit circuit.r1cs \
--witness solution.wtns \
--ciphertext witness.ct \
--key recovered_key.bin
# Step 4: Decrypt with the same method used in Step 2
When to Use Low-Level API
- Custom encryption schemes
- Integration with existing encryption pipelines
- Educational purposes
- Debugging encryption/decryption separately
Note: For most use cases, the high-level encrypt/decrypt commands are recommended.
Best Practices
- Use High-Level API: Use
encrypt/decryptcommands for simplicity and compatibility - Keep Circuit Files Consistent: Use the same compiled circuit files across tools
- Document Public Inputs: Clearly document which inputs are public vs private
- Embed Public Inputs: Use default behavior (embedded) for self-contained ciphertexts
- Version Your Circuits: Track circuit versions to ensure compatibility
- Test Both Directions: Always test CLI→JS and JS→CLI workflows
Troubleshooting
"Invalid ciphertext" when decrypting:
- Ensure the file is a valid zkenc ciphertext (created by
encryptcommand) - Verify file wasn't corrupted during transfer
- Check you're using the correct circuit file
"Witness doesn't satisfy constraints":
- Verify public inputs match between encryption and decryption
- Check private inputs satisfy circuit constraints
- Ensure using same circuit version
- Use
snarkjs wtns checkto validate witness
File format issues:
- Files are already compatible - no conversion needed!
- Use binary mode for all file operations
- Avoid text editors that might corrupt binary files
- Use
xxdorhexdumpto inspect files if needed
Public inputs mismatch:
- CLI and JS both embed public inputs by default
- Use
getPublicInput()in JS to extract from ciphertext - CLI displays public inputs when decrypting (if embedded)
Next Steps
- Node.js Guide → - Build CLI tools
- React Guide → - Build web UIs
- API References → - Detailed documentation
- Playground → - Try it in browser