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

@de-otio/crypto-envelope

v0.3.0

Published

Opinionated authenticated-encryption envelopes for TypeScript. XChaCha20-Poly1305 + AES-256-GCM AEAD + AAD binding + HMAC-SHA256 key commitment + RFC 8785 canonical JSON + Argon2id/PBKDF2 passphrase-KDF + SecureBuffer. Safe defaults that can't be turned o

Downloads

584

Readme

@de-otio/crypto-envelope

Opinionated authenticated-encryption envelopes for TypeScript. Makes best-practice cryptography accessible to application developers while preventing common implementation mistakes (nonce reuse, skipped AAD, weak KDFs, silent decryption failure, …).

Status: pre-release (0.2.0-alpha). AES-256-GCM as a second AEAD, unified passphrase-KDF with branded MasterKey, strict-by-default browser SecureBuffer, and per-key MessageCounter with a 2³² AES-GCM hard cap landed in this line. Extracted from chaoskb — this is chaoskb's encryption layer, packaged separately for audit and reuse. The @latest tag is reserved until chaoskb ships a production release; install the alpha explicitly. The wire format is considered mutable between 0.x minors until then.

What it is

An envelope layer above cryptographic primitives (@noble/*, libsodium) and below application protocols (Signal, TLS, JOSE). Takes a plaintext payload + a master key, produces a versioned, authenticated envelope with defensible defaults. Reversibly.

The package is small and opinionated. It does one thing: encrypt and decrypt self-describing blobs. Tiered key management (SSH-wrap, passphrase-derived recovery keys, OS-keychain integration, TOFU pinning) is a separate concern that will land as @de-otio/keyring — unpublished at the time of writing.

What it isn't

  • Not a primitives library — use @noble/* for that, and this package depends on it.
  • Not a protocol library — use libsignal, mls, or age for full sessions, groups, or file encryption.
  • Not a KMS wrapper — use aws-encryption-sdk-js if you need KMS-backed master keys.
  • Not a JWT/JWE token library — use jose.
  • Not a key-management framework — use @de-otio/keyring (forthcoming) if you want tiered SSH / passphrase unlock, recovery UX, or OS keychain integration.

Install

npm install @de-otio/crypto-envelope@alpha

Supported runtimes: Node ≥22, modern browsers (MV3 extensions and pages), Deno ≥2, Bun ≥1, Cloudflare Workers, Vercel Edge. On Node, the package uses sodium-native for mlock'd secure memory (prebuilt binaries; no extra toolchain). On browsers and other WebCrypto-only runtimes, a strict-by-default SecureBufferBrowser is substituted via the "browser" field; constructing one requires an explicit { insecureMemory: true } acknowledgement because browser runtimes cannot mlock.

Quick start

import { EnvelopeClient } from '@de-otio/crypto-envelope';

using client = new EnvelopeClient({ masterKey: crypto.getRandomValues(new Uint8Array(32)) });

const wire = await client.encrypt({ type: 'note', body: 'hello' });
const back = await client.decrypt(wire);
// → { type: 'note', body: 'hello' }

encrypt / decrypt are async (the per-key MessageCounter uses a Promise-returning interface so durable backends — SQLite, DynamoDB, Redis — can plug in). wire is a Uint8Array in the compact v2 (CBOR) wire format by default; opt into v1 JSON with { format: 'v1' }, both round-trip losslessly.

Passphrase unlock

import {
  EnvelopeClient,
  deriveMasterKeyFromPassphrase,
} from '@de-otio/crypto-envelope';

const masterKey = await deriveMasterKeyFromPassphrase(
  'correct horse battery staple',
  salt, // 16+ random bytes, persisted alongside the ciphertext
  { algorithm: 'argon2id' },
);

using client = new EnvelopeClient({ masterKey });

Argon2id is the mandated default (OWASP 2023 second-tier: t=3, m=64 MiB, p=1). PBKDF2-SHA256 is available as a compatibility-only fallback for WebCrypto-constrained runtimes; the iteration floor is 1,000,000 and taking this branch emits a one-time warn.

AES-256-GCM for interop

import { EnvelopeClient } from '@de-otio/crypto-envelope';

using client = EnvelopeClient.forAesGcmInterop({ masterKey });

XChaCha20-Poly1305 is the default for every new envelope. Prefer forAesGcmInterop only when decrypting or interoperating with systems that require AES-GCM (or FIPS-constrained environments). AES-GCM carries a 2³² per-key message cap — the client refuses further encryption past this via NonceBudgetExceeded.

wire is a Uint8Array in the compact v2 (CBOR) wire format by default. Opt into v1 JSON with { format: 'v1' }; both round-trip losslessly via upgradeToV2 / downgradeToV1, and decrypt() auto-detects.

For finer control, the low-level functions are exported too:

import {
  encryptV1,
  decryptV1,
  deriveContentKey,
  deriveCommitKey,
} from '@de-otio/crypto-envelope';

const cek = deriveContentKey(masterKey);
const commitKey = deriveCommitKey(masterKey);
const envelope = encryptV1({ payload: { x: 1 }, cek, commitKey, kid: 'default' });
const recovered = decryptV1(envelope, cek, commitKey);

What this package protects against

Design justification for each feature traces back to a specific class of application-level crypto mistake:

  • Nonce reuse → 192-bit random nonces via XChaCha20-Poly1305 (default). AES-256-GCM's 96-bit nonce is available for interop with a hard 2³² per-key message cap enforced at EnvelopeClient — cross-process counter state is pluggable via MessageCounter. Nonces are never user-supplied in the public API.
  • Skipped AAD / version downgrade → AAD is mandatory and binds version + algorithm + blob ID + key identifier.
  • Algorithm substitutionalg bound into AAD; nonce-width check rejects cross-algorithm ciphertext at the primitive.
  • Multi-key / partitioning-oracle attacks → dedicated commitment key via HKDF with its own domain-separation string; commitment HMAC binds to blob ID; verified before AEAD (key-committing, not context-committing — see SECURITY.md).
  • Silent serialization drift → RFC 8785 canonical JSON for plaintext + verify-after-encrypt (every output round-trips through decrypt before release).
  • Weak KDF parameters → Argon2id at OWASP-2023 second-tier (t=3, m=64 MiB, p=1, dkLen=32) as the mandated default. PBKDF2-SHA256 available for WebCrypto-only runtimes with a 1,000,000 iteration floor and a first-use warning.
  • Key confusionMasterKey branded type prevents passphrase-derived bytes from being handed to an AEAD primitive as a CEK without an explicit unbranding cast.
  • Timing attacks → constant-time comparisons throughout (pure-JS XOR-accumulate; portable across runtimes).
  • Keys in swap / crash dumpsSecureBuffer via sodium_malloc / sodium_memzero on Node. Browsers and other mlock-less runtimes get a strict-by-default SecureBufferBrowser requiring { insecureMemory: true } at construction — no silent degradation.
  • Math.random for keysglobalThis.crypto.getRandomValues only; no user-callable RNG for security-sensitive values. Throws on missing WebCrypto rather than falling back.
  • Silent decryption failure → commitment verified before AEAD; decrypt either returns plaintext or throws.

Published test vectors cover RFC 8785 canonicalisation, RFC 5869 Appendix A.1 HKDF-SHA256, RFC 4231 §4.3 HMAC-SHA256, draft-irtf-cfrg-xchacha §A.3.1 XChaCha20-Poly1305 KAT, an Argon2id cross-implementation KAT against libsodium's crypto_pwhash, RFC 7914 §11 PBKDF2-SHA256 vectors, NIST SP 800-38D / McGrew-Viega AES-256-GCM test cases 13–16, and 66 Wycheproof adversarial AES-256-GCM vectors (keySize=256 / ivSize=96 / tagSize=128).

Error handling

All library errors are instances of EnvelopeError and carry a stable code string suitable for switch statements. Import the classes from the main entry point:

import {
  EnvelopeError,
  AuthenticationFailedError,
  UnsupportedAlgorithmError,
  UnsupportedVersionError,
  MalformedEnvelopeError,
  TruncatedCiphertextError,
  NonceBudgetExceeded,
} from '@de-otio/crypto-envelope';

Error class hierarchy

EnvelopeError                    (base — code: string, message: string)
├── AuthenticationFailedError    code: 'AUTHENTICATION_FAILED'
├── UnsupportedAlgorithmError    code: 'UNSUPPORTED_ALGORITHM'
├── UnsupportedVersionError      code: 'UNSUPPORTED_VERSION'
├── MalformedEnvelopeError       code: 'MALFORMED_ENVELOPE'
├── TruncatedCiphertextError     code: 'TRUNCATED_CIPHERTEXT'
└── NonceBudgetExceeded          code: 'NONCE_BUDGET_EXCEEDED'

Switching on error codes

import { EnvelopeError, AuthenticationFailedError } from '@de-otio/crypto-envelope';

try {
  const plaintext = await client.decrypt(wire);
} catch (e) {
  if (!(e instanceof EnvelopeError)) throw e; // rethrow non-envelope errors
  switch (e.code) {
    case 'AUTHENTICATION_FAILED':
      // Wrong key or tampered envelope — indistinguishable by design.
      // Do NOT retry with a different key; present a generic "decryption failed" error.
      break;
    case 'MALFORMED_ENVELOPE':
    case 'TRUNCATED_CIPHERTEXT':
      // Structural problem before any key material was used.
      // Log and discard; the envelope cannot be salvaged.
      break;
    case 'UNSUPPORTED_ALGORITHM':
    case 'UNSUPPORTED_VERSION':
      // Envelope was produced by a newer library version.
      // Upgrade the library or reject the envelope.
      break;
    case 'NONCE_BUDGET_EXCEEDED':
      // AES-256-GCM per-key cap reached. Rotate the master key.
      break;
  }
}

Partitioning-oracle defence

AuthenticationFailedError is the single error class for all authenticated failures: wrong CEK, wrong commit key, tampered ciphertext, tampered AAD, and tampered commitment. The message and code are intentionally identical for every case. Distinguishing them would allow a partitioning-oracle attack (Len–Grubbs–Ristenpart, USENIX 2021 §4.2): an adversary with decrypt-oracle access could binary-search a candidate key set by observing which failure mode occurred. Callers must treat all AUTHENTICATION_FAILED errors identically.

Maintenance posture

This is a small-organisation, primarily-internal project. Honest expectations:

  • This is chaoskb's encryption layer, extracted. Design decisions are made for chaoskb first; other use cases are best-effort.
  • Published publicly for transparency and reference, not as a supported product with SLAs.
  • Forking encouraged. MIT is permissive on purpose. Wire format + test vectors are designed so a fork can remain interoperable.
  • Security issues are responded to on best-effort. See SECURITY.md for the disclosure process.

Development

Requires Node 22+.

npm install
npm run build
npm test           # fast suite (~400 ms)
npm run test:slow  # Argon2id cross-implementation KAT (~15 s)
npm run lint

License

MIT.