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

raz-encrypt

v0.2.0

Published

Local-first, hybrid classical+post-quantum signing and verification for two consenting parties. No server, no database required.

Readme

RAZ Encrypt

A local-first npm library for two consenting parties to sign documents, verify each other's signatures, and privately encrypt data for each other — with no server, no database, and no third-party account. Each party keeps their own identity and audit log in a small encrypted file on their own machine.

Built around a hybrid classical + post-quantum cryptography design (see "Security posture" below) rather than any single algorithm.

How it works (the short version)

  1. Alice and Bob each call createIdentity() once. This generates their keys and stores them, encrypted, in a local folder.
  2. They exchange their public cards — just public keys, safe to send over email/Slack/whatever — and call trustParty() to record each other as trusted.
  3. From then on: Alice can sign() a document; Bob can verify() it against the certificate. Either of them can encryptFor() the other and decryptFrom() what they receive.

Nothing here requires the two parties' computers to ever talk to each other directly — everything is exchanged "by hand" (file, email, chat), and each side's storage is entirely local and private to them.

Install

npm install raz-encrypt

Quickstart

import {
  createIdentity,
  exportPublicIdentity,
  trustParty,
  sign,
  verify,
} from "raz-encrypt";

// --- Alice, once ---
const alice = await createIdentity({
  storageDir: "./alice-store",
  passphrase: "alice's secret passphrase",
  alias: "alice",
});
const aliceCard = exportPublicIdentity(alice); // send this to Bob

// --- Bob, once ---
const bob = await createIdentity({
  storageDir: "./bob-store",
  passphrase: "bob's secret passphrase",
  alias: "bob",
});
const bobCard = exportPublicIdentity(bob); // send this to Alice

// --- Before trusting, compare fingerprints over a side channel (phone call, in person) ---
// This is what catches a spoofed or intercepted card — email/chat alone can't prove
// a card really came from the person you think it did.
import { fingerprint } from "raz-encrypt";
console.log("Alice reads this aloud:", fingerprint(aliceCard));
console.log("Bob confirms it matches what he sees:", fingerprint(bobCard));

// --- Both sides trust each other (after swapping cards AND confirming fingerprints) ---
await trustParty(alice, bobCard);
await trustParty(bob, aliceCard);

// --- Alice signs a document ---
const document = { invoiceId: "INV-1", amount: 15000 };
const certificate = await sign(alice, document);

// --- Bob verifies it (send him `document` + `certificate`) ---
const result = await verify(bob, document, certificate);
// => { valid: true }

If the document is changed even slightly, or the certificate is corrupted, or the signer was never trusted, verify() returns { valid: false, reason: "..." } instead of throwing — so a caller always gets a clear, structured answer.

Private data between the two parties

import { encryptFor, decryptFrom } from "raz-encrypt";

const box = await encryptFor(alice, bobCard, {
  secretPayload: "only Bob should read this",
});
const opened = await decryptFrom(bob, aliceCard, box);

Two important things people ask when actually wiring this up

1. sign/verify and encryptFor/decryptFrom are two separate, independent tools — not a pipeline.

  • sign + verify prove authenticity: "this exact document really came from Alice and wasn't altered." The document itself travels in the clear.
  • encryptFor + decryptFrom provide confidentiality: "nobody but Bob can read this." It says nothing about who sent it.

You use whichever one (or both) fits what you're protecting against. You do not need to call verify() before you can call decryptFrom(), or vice versa — neither depends on the other. If you want both (a document that's both proven-authentic and private), sign it, then encrypt the { document, certificate } pair together — see the worked example below.

2. A "card" (from exportPublicIdentity) is just a JSON object. However Alice actually gets it to Bob — email attachment, Slack message, QR code, USB stick — Bob ends up with the same plain JSON on disk. Loading it back is nothing more than reading that file and JSON.parse-ing it; there's no special "import" step.

Full example: Bob receives something from Alice

Assume Bob has already run createIdentity() once (so he has his own storageDir + passphrase) and already has alice-card.json — the file Alice sent him after she ran exportPublicIdentity() on her side.

import { readFile } from "node:fs/promises";
import {
  loadIdentity,
  decryptFrom,
  verify,
  type PublicCard,
  type Certificate,
} from "raz-encrypt";

// Step 1 — Bob loads his own identity (needs his own passphrase, not Alice's).
const bob = await loadIdentity({
  storageDir: "./bob-store",
  passphrase: "bob's secret passphrase",
});

// Step 2 — Bob loads Alice's card. It's just JSON — read whatever file/message it arrived in.
const aliceCard: PublicCard = JSON.parse(
  await readFile("./alice-card.json", "utf8"),
);
// (One-time only: before the first exchange, Bob must also have called
//  trustParty(bob, aliceCard) once — otherwise decryptFrom/verify will refuse Alice's card.)

// --- If what Alice sent was ENCRYPTED (confidential) ---
const box = JSON.parse(await readFile("./received-box.json", "utf8"));
const opened = await decryptFrom(bob, aliceCard, box);

// --- If what Alice sent was SIGNED (a document + a certificate, sent in the clear) ---
const { document, certificate } = JSON.parse(
  await readFile("./signed-doc.json", "utf8"),
) as {
  document: unknown;
  certificate: Certificate;
};
const result = await verify(bob, document, certificate);
// => { valid: true } or { valid: false, reason: "..." }

// --- If Alice sent something both signed AND encrypted ---
// She encrypted { document, certificate } as one payload with encryptFor(); Bob does:
const openedSignedPayload = await decryptFrom<{
  document: unknown;
  certificate: Certificate;
}>(bob, aliceCard, box);
const combinedResult = await verify(
  bob,
  openedSignedPayload.document,
  openedSignedPayload.certificate,
);

So, to directly answer "do I need to run verify first?" — no. verify() is only relevant when Alice actually signed something; decryptFrom() is only relevant when Alice actually encrypted something. Run whichever one matches what you received, in either order, or both if she did both.

API

| Function | What it does | | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | createIdentity({ storageDir, passphrase, alias, overwrite? }) | Generates a new identity (see "Security posture") and persists it encrypted at storageDir. Requires a passphrase of at least MIN_PASSPHRASE_LENGTH (12) characters. | | loadIdentity({ storageDir, passphrase }) | Loads a previously-created identity back from disk. | | exportPublicIdentity(identity) | Returns the shareable public card { alias, classicalPublicKey, pqcSignPublicKey, pqcKemPublicKey }. | | fingerprint(card) | Short, human-comparable fingerprint of a public card (grouped hex). Compare this over a side channel before trusting a card you received over email/chat. | | trustParty(identity, card) | Records another party's public card as trusted, locally. Re-trusting a previously revoked card reinstates it. | | untrustParty(identity, alias) | Revokes every currently-trusted card under alias (e.g. after a suspected key compromise). Tombstoned, not deleted — verify() reports "revoked-signer" distinctly from "untrusted-signer" (never trusted). | | listTrustedParties(identity) / getTrustedParty(identity, alias) | Read back your local trust list (revoked cards excluded). | | getTrustedPartiesByAlias(identity, alias) / getTrustRecordsByAlias(identity, alias) | Since alias isn't unique, these return every card under an alias — the latter includes revoked ones, with their revokedAt. | | listTrustRecords(identity) | Full trust history, revoked included — for audit purposes. | | sign(identity, document, options?) | Canonicalizes and hashes document, signs it with both keys, returns a Certificate. Pass { expiresAt } (ISO timestamp) to make it expire; omit it for a permanent proof (the default). | | verify(identity, document, certificate) | Checks the certificate's signer is trusted (and not revoked), the document hasn't changed, it hasn't expired, and both signatures check out. | | encryptFor(identity, counterpartCard, data) / decryptFrom(identity, counterpartCard, box) | Encrypt/decrypt data meant only for one specific counterpart. Both throw UntrustedPartyError unless that counterpart is already in your local trust list (trustParty). | | readLog(identity) | Reads the local, append-only audit log of everything this identity has signed/verified/encrypted/decrypted. | | verifyLogIntegrity(identity) | Walks the log's hash chain and reports { intact: true } or { intact: false, brokenAtIndex } — detects an edited or deleted entry. | | canonicalize(value) / hashDocument(value) | The deterministic JSON hashing used internally — exposed in case you want to compute a document hash yourself. |

Security posture

Is this "military grade"? It uses the same algorithms — ECDSA/ECDH on curve P-384, AES-256 — that banks, governments, and CNSA-1.0-compliant systems use. The math is the real thing, not a toy. What it is not is FIPS 140-2/3 (CMVP) validated: that's a certification of one specific software build/deployment, done by an accredited lab, and it takes years — no realistic npm library ships with that certification. Not being certified doesn't mean it's unsafe: it's the difference between a home-cooked meal made with the exact same top-shelf ingredients a certified restaurant uses, versus the restaurant's official health-inspection paperwork. The ingredients and technique are what make it safe; the certificate just proves it to a third party who requires that paperwork (e.g. a specific government/defense procurement rule). If your use case legally requires FIPS-validated modules, this library's logic needs to run inside a validated HSM/crypto boundary — that's a deployment decision outside what any library's code can provide on its own.

Is this quantum-safe? Half of the design is built specifically for that. ML-KEM-1024 (key exchange) and ML-DSA-87 (signatures) are NIST's official post-quantum standards (FIPS 203 / FIPS 204, finalized 2024), chosen because classical ECC and RSA can eventually be broken by a large enough quantum computer via Shor's algorithm. We don't rely on the post-quantum algorithms alone, though — they're only a few years old, with far less real-world cryptanalysis behind them than P-384. So every signature and every shared secret in this library requires both the classical layer and the post-quantum layer to hold:

  • Signing: every document is signed twice — once with ECDSA-P384, once with ML-DSA-87. verify() only returns valid: true if both signatures check out.
  • Encryption: the AES-256-GCM key is derived (via HKDF-SHA384) from both an ECDH-P384 exchange and an ML-KEM-1024 encapsulation.

If one layer is ever broken — classical by a quantum computer, or the newer post-quantum standard by an as-yet-undiscovered flaw — the other still protects you.

Bottom line: this is genuinely strong, real-world-grade cryptography built from carefully audited, widely used components (the @noble/* libraries) — not a marketing label. It just isn't a government-certified module, and true safety also depends on this specific implementation being built and tested carefully, which is exactly what the test suite's "try to break it" cases (tampered documents, corrupted individual signatures) are for.

What sign/verify guarantees, precisely. Every field that affects a verification outcome — the document hash and the optional expiresAt — is part of the signed payload, not just documentHash alone. That matters because certificates travel over the exact same untrusted channels (email, chat) this library assumes for everything else: without that, an intermediary with no secret key at all could delete or rewrite expiresAt in transit and flip a verify() result, which would quietly defeat the whole point of setting an expiry.

Two honest limitations, so you can decide if they matter for your use case:

  • No forward secrecy. encryptFor/decryptFrom derive their key from long-term keys on both sides: a static ECDH-P384 exchange (deterministic for a given pair of identities) combined with an ML-KEM-1024 encapsulation whose secret still requires the recipient's long-term KEM secret key to decapsulate. Both of those secret keys live in the same identity.enc.json. This means that if someone ever recovers your passphrase and that file, they can decrypt every box anyone ever sent you, not just future ones — there's no per-message ephemeral key rotation (the kind Signal-style ratcheting provides). Rotate identities (new storageDir, trustParty() again) if you need to bound the blast radius of a future key compromise.
  • No replay protection. An EncryptedBox decrypts identically no matter how many times, or how much later, it's replayed to the same recipient — there's no session id, counter, or timestamp binding it to a single delivery. Fine for "send this document once"; if your use case cares about replay (e.g. treating receipt of a box as a one-time authorization), add your own nonce/timestamp inside the encrypted payload and check it on the receiving end.

What this library deliberately does not do (v1)

  • No server, no HTTP API, no database — everything is local files.
  • No live network sync between the two parties' machines — public cards, documents, and certificates are exchanged "by hand" (file/email/etc.), not automatically.
  • No multi-party or hierarchical signing (buyers/vendors/approval-levels/etc.) — this is a generic building block for exactly two parties trusting each other directly.
  • Node.js only, not a browser library. The underlying @noble/* crypto libraries are isomorphic and would run fine in a browser, but the storage layer (identity.enc.json, trust.json, log.jsonl) is built on node:fs, which browsers don't have. There's no IndexedDB (or similar) storage adapter — if you need this in a browser, that adapter would need to be built.

Local storage layout

Each identity's storageDir contains:

identity.enc.json   # your keys, encrypted at rest under your passphrase (AES-256-GCM, scrypt-derived key)
trust.json          # fingerprint-keyed public cards of parties you've trusted (or revoked) — not secret, just public keys
log.jsonl           # hash-chained, append-only audit log: what you've signed/verified/encrypted/decrypted, and when

Development

npm install
npm run typecheck
npm test
npm run build