Getting Started with zkenc-cli
zkenc-cli is a command-line tool for witness encryption operations. It provides a simple interface for encrypting and decrypting messages using Circom circuits.
Installation
From Source
Clone the repository and build from source:
git clone https://github.com/flyinglimao/zkenc.git
cd zkenc/packages/zkenc-cli
cargo install --path .
Prerequisites
Before using zkenc-cli, you need:
-
A compiled Circom circuit with:
.r1csfile (circuit constraints).wasmfile (witness generator).symfile (signal-to-wire mapping) ← Required for encrypt
-
Input files in JSON format
Compile your circuit with the --sym flag to generate all necessary files.
Quick Start
1. Create a Simple Circuit
Create a file example.circom:
pragma circom 2.0.0;
template Example() {
signal input publicValue;
signal input privateValue;
signal output result;
result <== publicValue + privateValue;
}
component main = Example();
2. Compile the Circuit
circom example.circom --r1cs --wasm --sym --output circuit_output
This creates:
circuit_output/example.r1cscircuit_output/example_js/example.wasmcircuit_output/example.sym(symbol file for zkenc-cli)
3. Prepare Input Files
Create public_inputs.json (known when encrypting):
{
"publicValue": "42"
}
Create full_inputs.json (needed for decryption):
{
"publicValue": "42",
"privateValue": "123"
}
4. Encrypt Your Secret Message
Use encrypt to perform witness encryption in one step:
echo "Hello, zkenc!" > message.txt
zkenc encrypt \
--circuit circuit_output/example.r1cs \
--sym circuit_output/example.sym \
--input public_inputs.json \
--message message.txt \
--output encrypted.bin
This command:
- Generates a witness-encrypted key from public inputs using .sym file (encap)
- Encrypts your message with AES-256-GCM
- Combines everything into a single ciphertext file
- Embeds public inputs in the ciphertext (by default)
Output:
🔐 Step 1: Running Encap...
📂 Loading R1CS circuit...
- Constraints: 2
- Public inputs: 1
- Wires: 4
📋 Loading symbol file...
- Signal mapping loaded
📋 Loading public inputs from JSON...
- Parsed 1 field elements
✅ Witness ciphertext generated (123 bytes)
🔒 Step 2: Encrypting message...
- Message size: 14 bytes
✅ Message encrypted (42 bytes)
📦 Step 3: Creating combined ciphertext...
✅ Combined ciphertext saved (218 bytes)
✨ Encryption complete! Public inputs are embedded in the ciphertext.
5. Generate Witness File
Before decrypting, the recipient needs to generate a witness proving they have a valid solution:
snarkjs wtns calculate \
circuit_output/example_js/example.wasm \
full_inputs.json \
witness.wtns
6. Decrypt the Message
Use decrypt to recover and decrypt the message in one step:
zkenc decrypt \
--circuit circuit_output/example.r1cs \
--witness witness.wtns \
--ciphertext encrypted.bin \
--output decrypted.txt
This command:
- Parses the combined ciphertext
- Recovers the key using the witness (decap)
- Decrypts the message with AES-256-GCM
Output:
📦 Step 1: Parsing combined ciphertext...
- Flag: 1
- Witness ciphertext: 123 bytes
- Public input: {"publicValue":"42"}
- Encrypted message: 42 bytes
🔓 Step 2: Running Decap...
📂 Loading R1CS circuit...
- Constraints: 2
- Public inputs: 1
📋 Loading witness from snarkjs...
- Witness elements: 4
✅ Key recovered from witness
🔓 Step 3: Decrypting message...
✅ Decrypted message saved (14 bytes)
✨ Decryption complete!
Verify the result:
cat decrypted.txt
# Output: Hello, zkenc!
Command Reference
zkenc encap
Generate ciphertext and encryption key from circuit and public inputs.
zkenc encap \
--circuit <R1CS_FILE> \
--sym <SYM_FILE> \
--input <JSON_FILE> \
--ciphertext <OUTPUT_CT> \
--key <OUTPUT_KEY>
Arguments:
--circuit <FILE>- Path to R1CS circuit file (.r1csfrom Circom)--sym <FILE>- Path to symbol file (.symfrom Circom) ← Required--input <FILE>- Path to JSON file with public inputs--ciphertext <FILE>- Output path for ciphertext--key <FILE>- Output path for encryption key
Example:
zkenc encap \
--circuit sudoku.r1cs \
--sym sudoku.sym \
--input puzzle.json \
--ciphertext ciphertext.bin \
--key key.bin
zkenc decap
Recover the encryption key using a valid witness and ciphertext.
zkenc decap \
--circuit <R1CS_FILE> \
--witness <WTNS_FILE> \
--ciphertext <CT_FILE> \
--key <OUTPUT_KEY>
Arguments:
--circuit <FILE>- Path to R1CS circuit file--witness <FILE>- Path to witness file (.wtnsfrom snarkjs)--ciphertext <FILE>- Path to ciphertext file--key <FILE>- Output path for recovered key
Example:
zkenc decap \
--circuit sudoku.r1cs \
--witness solution.wtns \
--ciphertext ciphertext.bin \
--key recovered_key.bin
zkenc encrypt
Encrypt a message using witness encryption (high-level, one-step operation).
zkenc encrypt \
--circuit <R1CS_FILE> \
--sym <SYM_FILE> \
--input <JSON_FILE> \
--message <MESSAGE_FILE> \
--output <OUTPUT_FILE> \
[--no-public-input]
Arguments:
--circuit <FILE>- Path to R1CS circuit file (.r1csfrom Circom)--sym <FILE>- Path to symbol file (.symfrom Circom) ← Required--input <FILE>- Path to JSON file with public inputs--message <FILE>- Path to plaintext message file--output <FILE>- Output path for combined ciphertext--no-public-input- Don't embed public inputs in ciphertext (optional)
What it does:
This command combines encap and AES encryption into a single step:
- Generates witness-encrypted key from public inputs (using .sym file for correct input mapping)
- Encrypts message with AES-256-GCM
- Creates combined ciphertext with format:
[flag][witnessLen][witnessCT][publicLen][publicInput][encryptedMsg]
Example:
zkenc encrypt \
--circuit sudoku.r1cs \
--sym sudoku.sym \
--input puzzle.json \
--message secret.txt \
--output encrypted.bin
Compatibility: The output is fully compatible with zkenc-js decrypt() function.
zkenc decrypt
Decrypt a message using witness decryption (high-level, one-step operation).
zkenc decrypt \
--circuit <R1CS_FILE> \
--witness <WTNS_FILE> \
--ciphertext <CT_FILE> \
--output <OUTPUT_FILE>
Arguments:
--circuit <FILE>- Path to R1CS circuit file--witness <FILE>- Path to witness file (.wtnsfrom snarkjs)--ciphertext <FILE>- Path to combined ciphertext file--output <FILE>- Output path for decrypted message
What it does:
This command combines decap and AES decryption into a single step:
- Parses the combined ciphertext
- Recovers key using the witness
- Decrypts message with AES-256-GCM
Example:
zkenc decrypt \
--circuit sudoku.r1cs \
--witness solution.wtns \
--ciphertext encrypted.bin \
--output decrypted.txt
Compatibility: Can decrypt files created by zkenc-js encrypt() function.
Understanding the Workflow
zkenc-cli provides two levels of API:
High-Level API (Recommended)
A simple two-step process:
-
encrypt- Combines encap + AES encryption in one command- Input: circuit, public inputs, message
- Output: combined ciphertext (compatible with zkenc-js)
-
decrypt- Combines decap + AES decryption in one command- Input: circuit, witness, combined ciphertext
- Output: decrypted message
Benefits:
- Simpler workflow (2 steps vs 4)
- Single ciphertext file to manage
- Full compatibility with zkenc-js
- Public inputs can be embedded in ciphertext
Low-Level API (Advanced)
A four-step process for fine-grained control:
encap- Generate witness-encrypted ciphertext and key from public inputs- Encrypt message separately (use any AES tool)
decap- Recover the key using a valid witness- Decrypt message separately (use any AES tool)
Use cases:
- Custom encryption schemes
- Key reuse across multiple messages
- Integration with existing encryption pipelines
- Educational purposes to understand the protocol
Note: For most use cases, the high-level API is recommended as it ensures compatibility and simplifies the workflow.
Input File Formats
R1CS Circuit File (.r1cs)
Generated by Circom compiler:
circom circuit.circom --r1cs --wasm --sym
Witness File (.wtns)
Generated by snarkjs from your complete inputs:
# Calculate witness from input
snarkjs wtns calculate circuit.wasm input.json witness.wtns
# Verify witness (optional)
snarkjs wtns check circuit.r1cs witness.wtns
Input JSON File
JSON object with signal names as keys:
{
"publicValue": "42",
"privateValue": "123",
"arraySignal": ["1", "2", "3"]
}
Important Notes:
- All values must be strings (even numbers)
- Array signals are supported
- Signal names must match those defined in your circuit
- For
encrypt, only provide public inputs - For
decrypt, provide witness file generated from full inputs (public + private)
Combined Ciphertext Format
The encrypt command creates a combined ciphertext with the following structure:
[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]
Flag byte:
1= Public inputs included (default)0= Public inputs not included (use--no-public-input)
This format is compatible with zkenc-js and allows:
- Self-contained ciphertext (includes all necessary data)
- Cross-tool compatibility
- Optional public input embedding
Working with Binary Files
Encrypting Binary Files
You can encrypt any file type with the high-level API:
# Encrypt an image in one step
zkenc encrypt \
--circuit circuit.r1cs \
--input public.json \
--message photo.jpg \
--output encrypted_photo.bin
# Someone with witness decrypts the image in one step
zkenc decrypt \
--circuit circuit.r1cs \
--witness solution.wtns \
--ciphertext encrypted_photo.bin \
--output decrypted_photo.jpg
Using Low-Level API for Binary Files
For advanced use cases:
# Step 1: Generate key from circuit
zkenc encap \
--circuit circuit.r1cs \
--input public.json \
--ciphertext witness_ct.bin \
--key key.bin
# Step 2: Encrypt with external tool or custom method
# (The key.bin is a 32-byte key suitable for AES-256)
# Step 3: Recipient recovers the key
zkenc decap \
--circuit circuit.r1cs \
--witness solution.wtns \
--ciphertext witness_ct.bin \
--key recovered_key.bin
# Step 4: Decrypt with the same method used in Step 2
Advanced Usage
Encrypting Without Embedding Public Inputs
By default, encrypt embeds public inputs in the ciphertext. To exclude them:
zkenc encrypt \
--circuit circuit.r1cs \
--input public.json \
--message message.txt \
--output encrypted.bin \
--no-public-input
When to use --no-public-input:
- Public inputs are very large
- You'll distribute public inputs separately
- You want smaller ciphertext files
Note: Recipients will need the public inputs to verify the witness.
Batch Processing
Encrypt multiple messages for the same circuit and public inputs:
# Encrypt multiple files with embedded public inputs
for file in documents/*.txt; do
zkenc encrypt \
--circuit circuit.r1cs \
--input public.json \
--message "$file" \
--output "encrypted/$(basename $file).enc"
done
Each encrypted file is self-contained and can be decrypted independently.
Cross-Tool Compatibility
zkenc-cli is fully compatible with zkenc-js! You can encrypt with one tool and decrypt with the other:
CLI → JS:
# Encrypt with CLI
zkenc encrypt \
--circuit circuit.r1cs \
--input public.json \
--message message.txt \
--output encrypted.bin
# Decrypt with zkenc-js in Node.js or browser
# The encrypted.bin can be read by zkenc-js decrypt()
JS → CLI:
# After encrypting with zkenc-js encrypt()...
# Decrypt with CLI
zkenc decrypt \
--circuit circuit.r1cs \
--witness solution.wtns \
--ciphertext encrypted.bin \
--output decrypted.txt
Both tools use the same combined ciphertext format, ensuring seamless interoperability.
Learn more about cross-tool workflows →
Performance Tips
- Use high-level API:
encrypt/decryptcommands handle everything efficiently - Embed public inputs: Keeps ciphertext self-contained (default behavior)
- Pre-compile circuits: Compile circuits once, reuse many times
- Consider circuit size: Larger circuits = slower encap/decap operations
- Binary format: All files use efficient binary serialization
Common Patterns
Conditional Access Control
# Only users who solve the puzzle can decrypt
zkenc encrypt \
--circuit puzzle.r1cs \
--input question.json \
--message "Secret answer: 42" \
--output secret.bin
Time-Lock Encryption
# Requires computational work to generate witness
zkenc encrypt \
--circuit timelock.r1cs \
--input params.json \
--message future_message.txt \
--output locked.bin
Distributing Encrypted Files
# Encrypt with embedded public inputs
zkenc encrypt \
--circuit circuit.r1cs \
--input public.json \
--message secret.txt \
--output package.bin
# Share package.bin publicly
# Only those who can generate valid witness can decrypt
Next Steps
- API Reference → - Complete CLI command reference
- Cross-Tool Workflow → - Use CLI with zkenc-js
- zkenc-js Getting Started → - JavaScript alternative
Troubleshooting
"Circuit file not found"
Ensure the R1CS file path is correct:
# Check file exists
ls -lh circuit.r1cs
"Invalid inputs"
Check that your JSON file:
- Is valid JSON format
- Contains all required signal names
- Uses string values for all numbers
# Validate JSON
cat inputs.json | jq .
"Invalid ciphertext: too short"
This means the ciphertext file is corrupted or not a valid zkenc ciphertext. Ensure:
- The file was created by zkenc-cli
encryptor zkenc-jsencrypt() - The file wasn't modified or truncated
- You're using the correct file
"Decap failed"
This usually means:
- The witness doesn't satisfy the circuit constraints
- The witness file is corrupted
- Using wrong circuit files
- The witness doesn't match the public inputs used for encryption
Verify your witness first:
snarkjs wtns check circuit.r1cs witness.wtns
"Decryption failed" or "Message decryption failed"
Ensure:
- The witness satisfies the circuit constraints
- The ciphertext file is not corrupted
- Using the correct circuit file
- The witness matches the public inputs from encryption
Support
For issues or questions:
- Check the API Reference
- Review example workflows
- Open an issue on GitHub