npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zkenc-js

v0.2.1

Published

JavaScript/TypeScript bindings for zkenc witness encryption

Readme

zkenc-js

TypeScript/JavaScript bindings for zkenc - Witness Encryption using Circom circuits.

Overview

zkenc-js provides a high-level API for witness encryption, allowing you to encrypt data that can only be decrypted by someone who knows a valid witness (solution) to a computational statement defined by a Circom circuit.

Key Features:

  • 🔐 Witness-based encryption using zero-knowledge circuits
  • 🌐 Works in both Node.js and browsers
  • 🚀 Powered by WASM for high performance
  • 📦 TypeScript support with full type definitions
  • 🧪 Comprehensive test suite (29 tests passing)

Installation

npm install zkenc-js
# or
pnpm add zkenc-js
# or
yarn add zkenc-js

Quick Start

High-Level API (Recommended)

The simplest way to use zkenc-js - directly encrypt and decrypt messages:

import { encrypt, decrypt, getPublicInput } from "zkenc-js";
import { readFileSync } from "fs";

// Load your Circom circuit files
const r1csBuffer = readFileSync("circuit.r1cs");
const wasmBuffer = readFileSync("circuit.wasm");
const symContent = readFileSync("circuit.sym", "utf-8"); // Read as UTF-8 string

// Public inputs (the statement)
const publicInputs = {
  puzzle: [
    /* puzzle data */
  ],
};

// Complete inputs (public + witness)
const completeInputs = {
  puzzle: [
    /* puzzle data */
  ],
  solution: [
    /* solution data */
  ],
};

// 1. Encrypt: Automatically handles encap + AES encryption
const message = new TextEncoder().encode("Secret message");
const { ciphertext, key } = await encrypt(
  { r1csBuffer, wasmBuffer, symContent },
  publicInputs,
  message
  // Optional: { includePublicInput: false } to exclude public inputs
);
// key is available for advanced users

// 2. Extract public inputs from ciphertext (if included)
const extractedInputs = getPublicInput(ciphertext);
console.log(extractedInputs); // { puzzle: [...] }

// 3. Decrypt: Automatically handles decap + AES decryption
const decrypted = await decrypt(
  { r1csBuffer, wasmBuffer, symContent },
  ciphertext,
  completeInputs // Must include valid witness
);

const plaintext = new TextDecoder().decode(decrypted);
console.log(plaintext); // "Secret message"

Low-Level API (Advanced)

For research or custom encryption schemes, use the paper-aligned API:

import { encap, decap } from "zkenc-js";

// 1. Encap: Generate witness-encrypted key
const { ciphertext, key } = await encap(
  { r1csBuffer, symContent },
  publicInputs
);

// 2. Use key for custom encryption
// ... your custom encryption logic ...

// 3. Decap: Recover key with valid witness
const recoveredKey = await decap(
  { r1csBuffer, wasmBuffer },
  ciphertext,
  completeInputs
);

// 4. Use recovered key for decryption
// ... your custom decryption logic ...

Complete Example: Sudoku Witness Encryption

This example demonstrates encrypting a message that can only be decrypted by someone who knows a valid Sudoku solution.

import { encrypt, decrypt } from "zkenc-js";
import { readFileSync } from "fs";

// Load Sudoku circuit (defines the computational statement)
const r1csBuffer = readFileSync("sudoku.r1cs");
const wasmBuffer = readFileSync("sudoku.wasm");
const symContent = readFileSync("sudoku.sym", "utf-8");
const circuitFiles = { r1csBuffer, wasmBuffer, symContent };

// The puzzle (public input)
const puzzle = [
  5, 3, 0, 0, 7, 0, 0, 0, 0, 6, 0, 0, 1, 9, 5, 0, 0, 0,
  // ... 81 cells total
];

const solution = [
  5, 3, 4, 6, 7, 8, 9, 1, 2, 6, 7, 2, 1, 9, 5, 3, 4, 8,
  // ... complete valid solution
];

// Step 1: Encrypt - anyone can do this with just the puzzle
const secret = new TextEncoder().encode("Prize: $1000");
const { ciphertext, key } = await encrypt(
  circuitFiles,
  { puzzle }, // Only public inputs needed
  secret
);
// ciphertext now contains both witness encryption and AES encryption

// Step 2: Decrypt - only works with valid solution
try {
  const decrypted = await decrypt(
    circuitFiles,
    ciphertext,
    { puzzle, solution } // Need both public and private inputs
  );

  console.log(new TextDecoder().decode(decrypted)); // "Prize: $1000"
} catch (error) {
  console.error("Invalid witness!", error);
}

API Reference

High-Level API

encrypt(circuitFiles, publicInputs, message)

Encrypt message using witness encryption (combines encap + AES encryption).

Parameters:

  • circuitFiles: CircuitFilesForEncap - Circuit files for encryption
    • r1csBuffer: Uint8Array - R1CS circuit file
    • symContent: string - Circom symbol file (UTF-8 string)
  • publicInputs: Record<string, any> - Public inputs as JSON object
  • message: Uint8Array - Message to encrypt
  • options?: EncryptOptions - Optional encryption options
    • includePublicInput?: boolean - Include public inputs in ciphertext (default: true)

Returns: Promise<EncryptResult>

  • ciphertext: Uint8Array - Combined ciphertext (witness CT + AES CT)
  • key: Uint8Array - Encryption key (32 bytes, for advanced users)

Example:

const message = new TextEncoder().encode("Secret");
const { ciphertext, key } = await encrypt(
  { r1csBuffer, symContent },
  { puzzle: puzzleData },
  message,
  { includePublicInput: true } // Optional, true by default
);

decrypt(circuitFiles, ciphertext, inputs)

Decrypt message using witness decryption (combines decap + AES decryption).

Parameters:

  • circuitFiles: CircuitFiles - Circuit files for decryption
    • r1csBuffer: Uint8Array - R1CS circuit file
    • wasmBuffer: Uint8Array - Circom WASM file
  • ciphertext: Uint8Array - Combined ciphertext from encrypt
  • inputs: Record<string, any> - Complete inputs (public + witness)

Returns: Promise<Uint8Array> - Decrypted message

Throws: Error if witness is invalid or doesn't satisfy constraints

Example:

const decrypted = await decrypt({ r1csBuffer, wasmBuffer }, ciphertext, {
  puzzle: puzzleData,
  solution: solutionData,
});
const message = new TextDecoder().decode(decrypted);

getPublicInput(ciphertext)

Extract public inputs from ciphertext (if they were included during encryption).

Parameters:

  • ciphertext: Uint8Array - Combined ciphertext from encrypt

Returns: Record<string, any> - Public inputs as JSON object

Throws: Error if public inputs were not included in the ciphertext

Example:

// After encrypting with default options (includePublicInput: true)
const publicInputs = getPublicInput(ciphertext);
console.log(publicInputs.puzzle); // [5, 3, 0, ...]

Note: This only works if the ciphertext was created with includePublicInput: true (default).


Low-Level API

encap(circuitFiles, publicInputs)

Generate witness-encrypted key (low-level, paper-aligned API).

Parameters:

  • circuitFiles: CircuitFilesForEncap - Circuit files for encapsulation
    • r1csBuffer: Uint8Array - R1CS circuit file
    • symContent: string - Circom symbol file (UTF-8 string)
  • publicInputs: Record<string, any> - Public inputs as JSON object

Returns: Promise<EncapResult>

  • ciphertext: Uint8Array - Witness ciphertext (1576 bytes)
  • key: Uint8Array - Symmetric encryption key (32 bytes)

Example:

const { ciphertext, key } = await encap(
  { r1csBuffer, symContent },
  { puzzle: puzzleData }
);
// Use key for custom encryption...

decap(circuitFiles, ciphertext, inputs)

Recover encryption key using valid witness (low-level, paper-aligned API).

Parameters:

  • circuitFiles: CircuitFiles - Circuit files for decapsulation
    • r1csBuffer: Uint8Array - R1CS circuit file
    • wasmBuffer: Uint8Array - Circom WASM file
  • ciphertext: Uint8Array - Witness ciphertext from encap
  • inputs: Record<string, any> - Complete inputs (public + witness)

Returns: Promise<Uint8Array> - Recovered encryption key (32 bytes)

Throws: Error if witness is invalid

Example:

const key = await decap({ r1csBuffer, wasmBuffer }, ciphertext, {
  puzzle: puzzleData,
  solution: solutionData,
});
// Use key for custom decryption...

Browser Usage

zkenc-js works in browsers with bundlers like Vite, Webpack, or Rollup:

// Fetch circuit files
const r1csResponse = await fetch("/circuits/sudoku.r1cs");
const wasmResponse = await fetch("/circuits/sudoku.wasm");
const symResponse = await fetch("/circuits/sudoku.sym");

const r1csBuffer = new Uint8Array(await r1csResponse.arrayBuffer());
const wasmBuffer = new Uint8Array(await wasmResponse.arrayBuffer());
const symContent = await symResponse.text(); // Read as UTF-8 string

// Use normally
const { ciphertext, key } = await encap(
  { r1csBuffer, symContent },
  publicInputs
);

Note: Make sure your bundler is configured to handle WASM files. For Vite:

// vite.config.ts
import { defineConfig } from "vite";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";

export default defineConfig({
  plugins: [wasm(), topLevelAwait()],
});

Architecture

zkenc-js is built on top of zkenc-core and compiled to WebAssembly:

┌─────────────────────────────────────┐
│         zkenc-js                    │
│  (WASM + TypeScript Application)    │
│                                     │
│  - TypeScript API                   │
│  - AES-256-GCM encryption           │
│  - Witness calculator               │
│  - R1CS/WTNS parsers                │
└───────────────┬─────────────────────┘
                │
  ┌─────────────▼──────────────┐
  │      zkenc-core            │
  │  (Cryptographic Foundation)│
  └────────────────────────────┘

The package consists of three internal layers:

  1. TypeScript API Layer: High-level encrypt()/decrypt() functions with witness calculator integration
  2. WASM Bindings Layer: Circom R1CS parser, snarkjs witness parser, and CircomCircuit implementation
  3. Core Layer: zkenc-core providing encap()/decap() cryptographic primitives

How It Works

Witness encryption allows encrypting data to a computational statement rather than a public key:

  1. Statement (Public): A puzzle or problem defined by a Circom circuit
  2. Witness (Private): A solution that satisfies the circuit constraints
  3. Encryption: Anyone can encrypt to the statement
  4. Decryption: Only works with a valid witness

Security Properties

  • Correctness: Valid witness always recovers the correct key
  • Soundness: Invalid witness cannot recover the key (with high probability)
  • Witness Privacy: The ciphertext doesn't reveal the witness
  • CRS Security: Based on pairing-friendly elliptic curves (BN254)

Testing

# Run all tests
pnpm test

# Run specific test suites
pnpm test e2e        # End-to-end tests
pnpm test witness    # Witness calculator tests
pnpm test zkenc      # Crypto tests

# Watch mode
pnpm test --watch

Test coverage:

  • ✅ 9 witness calculator tests
  • ✅ 7 WASM integration tests
  • ✅ 8 end-to-end workflow tests
  • ✅ 5 zkenc API tests
  • Total: 29/29 passing

Development

Build the WASM module:

pnpm run build:wasm

Compile TypeScript:

pnpm run build

Troubleshooting

"R1CS parse error"

  • Ensure your .r1cs file is generated by Circom
  • Check file is not corrupted

"Witness size mismatch"

  • Verify your inputs match the circuit's expected format

"Invalid witness" during decap

  • The provided solution doesn't satisfy the circuit constraints
  • Double-check your witness values

WASM not loading in browser

  • Configure your bundler to handle WASM files
  • Check CORS headers if loading from CDN

Examples

See the tests/ directory for more examples:

  • e2e.test.ts - Complete encryption workflows
  • witness.test.ts - Witness calculator usage
  • zkenc-wasm.test.ts - Low-level WASM API

License

MIT