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

cleave-crypto

v0.1.0

Published

Post-quantum hybrid file encryption with key commitment, ratcheted chunk authentication and verifiable erasure. Runs in the browser.

Readme

cleave-crypto

Post-quantum hybrid file encryption for the browser. TypeScript, ESM, no runtime dependencies beyond @noble and WebCrypto.

npm install cleave-crypto
import { encrypt, decrypt, generateIdentity } from 'cleave-crypto';

const alice = generateIdentity();

const { container } = await encrypt(bytes, {
  recipients: [alice.publicKey],
  filename: 'notes.txt',
});

const { data, metadata } = await decrypt(container, { identities: [alice] });

What it is

CLEAVE is a container format (.clv) built from standardized primitives:

| Role | Primitive | | --- | --- | | Key encapsulation | X-Wing (ML-KEM-768 + X25519) | | Payload encryption | AES-256-GCM via WebCrypto | | Key derivation | HKDF-SHA-512 | | Authentication | HMAC-SHA-512, truncated to 32 bytes | | Passphrase hardening | Argon2id | | Signatures (optional) | ML-DSA-65 over a SHA-512 pre-hash |

No new primitive is introduced. The contribution is at the format layer, and it is four mechanisms:

Key commitment. A tag over the header is verified before any payload byte is decrypted. A container decrypts under exactly one key, so the format cannot be used as a partitioning oracle and cannot be crafted to decrypt differently for different recipients.

Cascade ratchet. Every chunk gets a fresh key, after which the chain key is advanced and the old one erased. Chunk index and finality are bound into both the key schedule and the associated data, so chunks cannot be reordered, duplicated or dropped undetected. This is the STREAM discipline with forward erasure added.

Access strata. An optional preview payload, keyed independently of the main payload. A credential that opens the preview yields nothing about the payload key, so graded disclosure is a cryptographic boundary rather than a policy flag.

Sentinel shard. The master secret can be split into a half stored in the container and a detached 61-byte half. Destroying every copy of the shard makes the container permanently unreadable without touching the container itself.

Recipes

Passphrase, with a cost profile chosen deliberately.

import { encrypt, Argon2Profiles } from 'cleave-crypto';

const { container } = await encrypt(bytes, {
  passphrase: userInput,
  argon2: Argon2Profiles.sensitive, // 256 MiB, t=4
});

Several recipients and a passphrase at once. Any one credential opens it.

await encrypt(bytes, {
  recipients: [alicePk, bobPk],
  passphrase: fallbackPhrase,
});

Sentinel shard. Hand the shard to whoever should hold the power to revoke.

const { container, shard } = await encrypt(bytes, { passphrase, sharded: true });
// Later, to erase: destroy every copy of `shard`. The container is now inert.
const { data } = await decrypt(container, { passphrase, shard });

Preview stratum. Wide audience sees the summary, narrow audience sees the file.

await encrypt(fullReport, {
  recipients: [boardPk],
  preview: { content: 'Q3 summary: revenue up 4%.', recipients: [staffPk] },
});

const summary = await decryptPreview(container, { identities: [staff] });

Signing, and requiring a signature on the way in.

const signer = generateSigningIdentity();
await encrypt(bytes, { recipients: [alicePk], signWith: signer });

await decrypt(container, {
  identities: [alice],
  requireSignatureFrom: signer.publicKey, // unsigned containers now rejected
});

Inspecting before prompting. Reads the public header with no credential, so a UI can ask for the right things.

const info = inspect(container);
// { sharded: true, signed: false, slots: { passphrase: 1, recipient: 2 }, ... }

Note what inspect cannot tell you: filename, size and content type live inside the encrypted payload.

Long files on the main thread. Encryption is CPU-bound. Either run it in a Web Worker, or set cooperative to yield between chunks.

await encrypt(bigFile, { passphrase, cooperative: true, onProgress: (p) => {
  setPercent((p.bytesProcessed / p.totalBytes) * 100);
}});

Cost

Argon2id in pure JavaScript is slow, and the profile is a real UX decision:

| Profile | Memory | Passes | Rough cost | | --- | --- | --- | --- | | interactive | 19 MiB | 2 | a few hundred ms | | moderate (default) | 64 MiB | 3 | several seconds | | sensitive | 256 MiB | 4 | tens of seconds |

Parameters are recorded per slot, so a container written at one cost can be re-wrapped at a higher one later without a format change. Recipient slots do not touch Argon2 at all and are attempted first when opening.

Container overhead, measured:

| Configuration | Bytes over plaintext | | --- | --- | | Passphrase only | 237 | | One recipient | 1343 | | Adding a signature | +5269 | | Detached shard file | 61 |

Plus 20 bytes per chunk, with a 1 MiB default chunk size.

Errors

Branch on the class, not the message.

  • CleaveNoMatchingSlotError — no supplied credential fits any slot
  • CleaveShardError — shard missing, foreign, or corrupt
  • CleaveAuthError — commitment or chunk authentication failed: wrong key or tampering
  • CleaveSignatureError — signature absent when required, or does not verify
  • CleaveTemporalError — outside the advisory validity window
  • CleaveFormatError — not a well-formed container
  • CleaveUsageError — a caller mistake

What this library cannot promise

These are limitations of the setting, not defects to be patched later. Read them before deploying.

Zeroization is best-effort. Uint8Array.fill(0) overwrites one copy. A JavaScript engine may have made others during garbage collection or JIT optimization, and nothing in the language exposes them. Do not assume key material has left process memory.

Delivery is the trust root. If the library reaches the user over the web, whoever controls that channel controls the code, and no property below the delivery layer survives a hostile server. This is the standard objection to browser cryptography and it is correct. Subresource integrity, code signing and extension packaging narrow it; they do not close it. For threat models that include the origin operator, ship a native or packaged client.

Timing is not fully controlled. timingSafeEqual reduces the signal but a JIT can defeat it speculatively. Lattice operations come from @noble, which targets constant time, but the runtime provides no guarantee.

Erasure is computational, not information-theoretic. The shard split is a one-time pad, but the container also carries a header tag derived from the master secret and a hint derived from the shard. An adversary with unbounded time could enumerate and confirm. The work is roughly 2^192 with the hint present and 2^256 without. Against any bounded adversary this is irrelevant; against an information-theoretic claim it is fatal, so the claim is not made.

Temporal fields are advisory. notBefore and expiresAt are authenticated, so they cannot be altered, but nothing prevents an adversary from moving the clock. They make honest software behave predictably. They are not access control.

Metadata leaks shape. Container length reveals plaintext length to within a chunk. Slot count reveals how many credentials exist, and recipient key identifiers reveal which public keys can open it. If the recipient set is sensitive, this format is the wrong tool.

Not independently audited. The primitives are standardized and their implementations audited. The protocol layer here is not. Treat the version number literally.

No streaming yet, so memory is the real ceiling. Encryption and decryption are whole-buffer. Measured peak retention is about 1.7 times the plaintext in ArrayBuffer memory and roughly 3 times in resident set, because the input, the per-chunk ciphertexts and the assembled container coexist. On a memory-limited mobile browser that puts the practical ceiling in the low hundreds of megabytes before the tab is killed. A chunked-streaming API over WritableStream is the intended next addition; the format already supports it, since the chunk count has a defined unknown sentinel and truncation resistance rests on the authenticated final-chunk flag. Until then, do not feed this multi-gigabyte files in a browser tab.

Startup cost and code splitting

ML-DSA-65 and Argon2id load on first use rather than at import. A container addressed only to recipient public keys touches neither, so a bundler that performs code splitting, which webpack, rollup, vite and esbuild all do for dynamic imports, keeps them out of the initial download. Measured on this build, the recipient-only entry is about 20 KiB gzipped, against 32 KiB for the whole library; the ML-DSA and Argon2 chunks, roughly 3.5 and 4.7 KiB gzipped, arrive only when a signature or passphrase is first used.

If an application knows it is about to prompt for a passphrase or verify a signature, it can warm both during an idle moment:

import { preloadOptionalPrimitives } from 'cleave-crypto';
preloadOptionalPrimitives(); // fire and forget

Because these primitives load lazily, generateSigningIdentity, decodeSigningIdentity and any passphrase-based operation are asynchronous.

Compatibility

Requires WebCrypto, which in browsers means a secure context: HTTPS or localhost. Node 20 or later. ESM only.

License

MIT