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

@hiprax/crypto

v1.9.1

Published

High-security encryption/decryption library using AES-256-GCM and Argon2id

Readme

@hiprax/crypto

🔐 High-security encryption/decryption library using AES-256-GCM and Argon2id, for Node.js and the browser, with full TypeScript support.

CI codecov CodeQL npm provenance Post-Quantum Ready License: MIT npm version TypeScript Node.js


Contents

| | | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Getting started | Features · Installation · Module system · Argon2 providers · Browser build · CommonJS interop · Quick Start | | Reference | API Reference · Constructor options · Methods · Types and enums · Utility functions · Wire-format and codec exports · Full export surface | | Runtimes | Isomorphic API & browser support · Cross-runtime interop · Browser Argon2id profile · CSP for WASM · Node-only methods in the browser | | Formats | Ciphertext format (v1) · Container mode (v2) · Container format (v2) · Telling the formats apart | | Configuration | Sync vs async · Progress callbacks · Security levels · Password requirements | | Security | Security features · (key, IV) reuse boundary · Post-quantum security · Threat model · SECURITY.md | | Errors | Error handling · Error types · AES-GCM size limit · Container error codes | | Project | Testing · Benchmarks · Development · Contributing · Changelog · License |


✨ Features

  • 🔐 AES-256-GCM authenticated encryption
  • 🔑 Argon2id memory-hard key derivation
  • 🌐 Isomorphic (Node + browser) — the same encryptBytes / decryptBytes / encryptText / decryptText run in Node and the browser over one wire format, so a ciphertext produced in one runtime decrypts in the other (details)
  • 🔮 Post-quantum resistant by design — symmetric-only cryptography, nothing for Shor's algorithm to break (proofs)
  • 📁 Streaming file encryption AND decryption (bounded memory regardless of file size) with atomic temp-file output
  • 📦 Container mode — an optional authenticated v2 envelope with confidential metadata (filename/mime) and an embedded, re-verified plaintext hash (details)
  • 🛡️ Memory-safe operations with secure clearing
  • ✅ Strong password validation with detailed feedback
  • 🔄 Cross-platform compatibility
  • 📝 Full TypeScript support with strict typing
  • 🧪 Comprehensive testing — 1,261 tests: 1,224 in the Node suite (28 files, Jest) plus 37 in a real headless Chromium (Vitest Browser Mode), behind a one-way coverage ratchet (96% statements / 88% branches / 98% functions / 96% lines)
  • 🚀 Modern ES modules with tree-shaking support
  • 🔒 Security-focused with constant-time comparisons
  • 🔑 Default passphrase support for simplified usage

📦 Installation

npm install @hiprax/crypto

Module system

This package is ESM-only ("type": "module"). It is not shipped with a CommonJS build because Node.js ESM consumers (the package minimum is Node 22) can use it directly via import, and CJS consumers can still load it via dynamic import(). See the CommonJS interop section below.

The main entry (.) is an isomorphic conditional export: Node resolves the Node build (dist/index.js) and browser bundlers resolve a separate, node:-free browser build (dist/index.browser.js). The browser build ships the same in-memory async API over Web Crypto + WebAssembly Argon2id — see Browser build and Isomorphic API & Browser Support.

Argon2 providers (native, Node built-in, WASM)

The async key-derivation paths (encryptText, decryptText, encryptFile, decryptFile, deriveKey) use Argon2id — the gold standard for password hashing. Three providers are supported and tried in order:

  1. Native argon2 — fastest, but requires a working C++ toolchain (Python + node-gyp) at install time on platforms without a prebuilt binary. Declared as an optional dependency.
  2. Node's own crypto.argon2 — built into the runtime from Node 24.7.0 onward. Nothing to install: no compiler, no WebAssembly package, no dependency of any kind. Like the native addon, it runs the derivation off the event loop.
  3. WASM hash-wasm — pure WebAssembly, zero native deps, works everywhere Node.js runs, including Node 22, where there is no built-in. Declared as an optional dependency. It is also the provider the browser build uses unconditionally, because Web Crypto has no Argon2id. Unlike the other two it computes synchronously on the calling thread, so in Node it blocks the event loop for the whole derivation — see Which provider answered, and why it matters before using it to serve untrusted traffic.

[!WARNING] The async API is non-blocking on two of the three providers, not all three. Measured on Node v24.19.0 at the default 128 MiB profile, with a 5 ms timer probing event-loop liveness: the native addon fired 76 of ~78 expected ticks and the built-in 85 of ~86, while hash-wasm fired 0 of ~138 during a 694 ms derivation. At this library's own Node decrypt-budget ceiling a single hash-wasm derivation blocked for 6,309 ms. Confirmed on Node v22.23.2 too (609 ms, 0 of ~121 ticks). Because both native packages are optional and a failed native build does not fail an npm install, a deployment can land on the blocking provider silently. Check with getArgon2Provider().

All three implement the same RFC 9106 Argon2id reference and derive bit-identical keys for the same (password, salt, memoryCost, timeCost, parallelism, hashLength) tuple — verified across nine parameter sets at development time, including the memoryCost === 8 * parallelism floor and memory values that are not a multiple of 4 * parallelism (all three apply the same rounding). Six (memoryCost, timeCost, parallelism) tuples are pinned as regression tests in src/__tests__/argon2-provider-parity.test.ts, each checked against every provider the host has — all three where the native addon built and the runtime is Node >= 24.7, and never fewer than two, since one implementation cannot evidence a parity claim: the known-answer vector (4096, 2, 1), plus (8, 2, 1), (9, 2, 1), (100, 2, 7), (8, 1, 1) and (4096, 1, 1). Exactly two of the six — the known-answer vector and (8, 1, 1) — are literally among the nine; the other four are neighbours chosen to reach the same corners, including a timeCost of 1, which @types/node declares out of range and which this library nonetheless permits and produces. (parallelism carries the identical wording and matters more, since p = 1 is the default in essentially every ciphertext this library has produced; the known-answer vector and four of the five other tuples pin it.) A seventh case re-runs the known-answer tuple with a multi-byte, non-ASCII password in both NFC and NFD, which is what pins that the three providers agree on how a JavaScript string becomes bytes; an ASCII vector cannot say that, because ASCII is byte-identical under every plausible encoding. The remaining seven development probes are one-off checks recorded in no committed test. A v1 ciphertext produced under any one provider therefore round-trips under any other, and which provider answers is a performance and packaging question rather than a compatibility one. There is deliberately no way to select one, because all three derive the same key and a switch would only pick a slower one. That framing was incomplete before v1.9.0 and is worth stating precisely: the providers are interchangeable for correctness, but not for threading. Two derive off the event loop and one does not, which is a behavioural difference rather than a performance one. The answer is not a selection flag, which would let a caller pick a provider their host does not actually have; it is getArgon2Provider(), which reports the one that answered so a service can refuse to start on it.

Measured cost per derivation at the library's default 128 MiB / t=3 / p=1 profile, five runs each on the maintainer's Linux machine (Node v24.19.0) on 2026-09-21: native 343 ms, Node built-in 403 ms, hash-wasm 597 ms. Treat those as an order of magnitude rather than a budget. A single Argon2id derivation is not a stable constant, and this repository records more than one figure for the same operation — an earlier session on the same machine measured ~357 ms native against ~631 ms WASM on 2026-09-10, and bench/README.md cites 395 ms — so budget for a wider spread on weaker hardware and run npm run bench for a number from your own host. To check the bit-identical claim yourself, hash the same tuple through each provider and compare the bytes — that is what src/__tests__/argon2-provider-parity.test.ts does. (bench/kdf.mjs will not tell you: it times whichever provider the chain resolves, it does not compare them.)

On Node >= 24.7.0 the async API needs no optional dependency at all. npm i @hiprax/crypto --omit=optional installs nothing but the package itself, and encryptText / decryptText / encryptFile / decryptFile / deriveKey still work, because the runtime supplies Argon2id. On Node 22 — the package minimum, in Maintenance LTS until 2027-04-30 — there is no built-in, so one of the two optional packages is required for any async path; the synchronous PBKDF2 methods need neither on any version.

The library tries native first (highest performance), then the runtime's built-in, then WASM. If all three are unavailable, async encryption throws CryptoError(MEMORY_ERROR, 'ARGON2_NOT_AVAILABLE') with the message:

argon2 native module unavailable. Install build tools (Python + node-gyp), or run on Node >= 24.7.0 (whose built-in `crypto.argon2` needs no install at all), or install the optional `hash-wasm` package for a pure-WASM Argon2id fallback (slower than native but works everywhere). Alternatively, use *Sync methods (PBKDF2). Native error: <msg>. Node built-in error: <msg>. WASM error: <msg>.

(the trailing Native error: … Node built-in error: … WASM error: … carries the concrete failure reason from each link of the chain, so "not installed" stays distinguishable from "installed but broken" for every one of them.)

Which provider answered, and why it matters

import { getArgon2Provider } from '@hiprax/crypto/crypto-manager';

// 'native' | 'node' | 'wasm'. Forces the lazy load, so it is meaningful at boot.
if ((await getArgon2Provider()) === 'wasm') {
  throw new Error(
    'Argon2id resolved to the WASM provider, which blocks the event loop.'
  );
}

The three providers derive bit-identical keys, so this never affects whether a ciphertext round-trips. It affects availability: hash-wasm blocks the calling thread, so an endpoint that decrypts untrusted input on it can be stalled for seconds by a single small ciphertext, which is exactly what the async paths are supposed to prevent.

You will land on hash-wasm when the native addon is absent or unloadable and hash-wasm is not. Prebuilt binaries cover linux-x64/arm64/arm (glibc and musl, so Alpine is fine), darwin-arm64, freebsd-x64/arm64 and win32-x64. The gaps worth knowing: Intel macOS (darwin-x64) and win32-arm64 have no prebuild and need a working C++ toolchain; so do s390x/ppc64le. A glibc older than the prebuild's build host fails to load. Bundlers and serverless packagers that cannot ship a .node binary will also fall through. None of these fails your install, because the dependency is optional.

Three remedies, in the order worth trying:

  1. Install a C++ toolchain (Python + node-gyp) so the native addon builds.
  2. Run on Node >= 24.7.0, whose built-in crypto.argon2 needs no install at all and derives off the event loop.
  3. Move decryption into a worker_thread, which keeps the main loop free whichever provider answers. This is the same recipe the synchronous paths need, and it is the only one that works when you cannot change the install.

getArgon2Provider is exported from the @hiprax/crypto/crypto-manager subpath rather than the package root, because the root entry's surface is held to a deliberate shape (it exceeds the browser entry by exactly the Node-only file helpers) and this symbol is Node-only for a different reason.

To recover from an "all three unavailable" error, do any of the following — they are listed in the chain's own order, which is also fastest-first:

  1. Install build tools and reinstall so argon2's node-gyp step succeeds (best performance), or
  2. Run on Node >= 24.7.0, where crypto.argon2 is part of the runtime — the only option on this list that installs nothing, or
  3. Install hash-wasm explicitly: npm install hash-wasm (no build tools needed; transparent fallback once installed), or
  4. Use the synchronous methods (encryptTextSync, decryptTextSync, encryptFileSync, decryptFileSync, deriveKeySync) which use PBKDF2-HMAC-SHA256 and have no Argon2id provider dependency at all. Note that PBKDF2 is materially weaker than Argon2id against GPU/ASIC adversaries — prefer (1), (2) or (3) for new ciphertexts.

Note: a successful first load is cached for the lifetime of the process (subsequent async calls reuse the same provider). A failed first load is NOT cached — the next caller retries the whole chain from scratch, so transient failures (e.g. a temporary FS permission glitch on Windows during a build-tool install) can recover within the same process.

Browser build

@hiprax/crypto is isomorphic: the . entry is a conditional export, so a browser bundler (webpack 5, Vite/Rollup, esbuild, Next.js) automatically resolves the separate browser build (dist/index.browser.js) while Node resolves the Node build (dist/index.js). The browser build's import graph contains zero node: builtins and references no Buffer/process global, so nothing like node:crypto/node:fs/node:stream is ever dragged into your bundle. Three complementary static gates enforce this continuously (in CI and at publish), and they cover three disjoint defect classes:

  • npm run check:browser — an esbuild platform:'browser' bundle of the browser entry; fails on any node: specifier reaching the browser graph.
  • The ESLint no-restricted-globals (Buffer/process) + no-restricted-imports (node:*) override on the isomorphic source files — catches a bare Buffer/process value that esbuild cannot see, because a bundle succeeds and then ReferenceErrors at runtime.
  • npm run check:types:browser — type-checks a throwaway browser-only consumer against the built dist/, resolved through the package's browser export condition with "types": [] and "skipLibCheck": false. This is the only gate that can see a type-position Node reference: no-restricted-globals does not fire on interface X { y: Buffer } (it inspects value references only), and esbuild erases types before it ever sees them. A Node type reaching dist/index.browser.d.ts — or anything it re-exports — now fails the build instead of reaching your project as a Cannot find name 'Buffer' error.
# The browser build needs the WASM Argon2id provider (Web Crypto has no Argon2id):
npm install @hiprax/crypto hash-wasm
// In a browser (or any bundler that sets the "browser" condition):
import { CryptoManager } from '@hiprax/crypto';

const cm = new CryptoManager(); // Web Crypto + hash-wasm Argon2id, 32 MiB default
const ct = await cm.encryptText('secret', 'MySecureP@ssw0rd123!');
const back = await cm.decryptText(ct, 'MySecureP@ssw0rd123!');

Requirements and behavioral differences from Node — the 32 MiB browser Argon2id default, the secure-context requirement, the CSP 'wasm-unsafe-eval' one-liner, the browser memory-hygiene caveats, and the Node-only methods that throw UNSUPPORTED_IN_BROWSER — are all covered under Isomorphic API & Browser Support. The ./crypto-manager and ./utils subpath exports remain Node-only (they pull in node:fs/node:path); import from the package root (@hiprax/crypto) in browser code.

CommonJS interop

@hiprax/crypto is ESM-only. The package.json exports map has no require entry in any condition — the . entry resolves in JSON source order browser → node → default, and each of those branches nests its own types + default (so a browser bundler resolves dist/index.browser.js with the matching dist/index.browser.d.ts, while Node resolves dist/index.js with dist/index.d.ts); none of the branches is a require target. The supported and portable interop for CommonJS callers is a dynamic import() from an async function — it is the only pattern the test suite verifies:

// my-cjs-file.cjs
async function main() {
  const { CryptoManager } = await import('@hiprax/crypto');
  const cm = new CryptoManager();
  const ciphertext = await cm.encryptText('hello', 'MySecureP@ssw0rd123!');
  console.log(ciphertext);
}
main();

On Node 22+ (the package minimum) a plain require('@hiprax/crypto') call may actually succeed via Node's built-in require(esm) path (enabled by default for ESM files with no top-level await). Do not rely on this: the behavior is undocumented as a stable API, the require path is untested by this library, and earlier minor releases of Node 22 may differ. await import() is the supported path.

If you need pure-CJS interop, the recommended path is to migrate the calling module to ESM ("type": "module" or .mjs).

🚀 Quick Start

Basic Usage

Text: asynchronous operations (recommended)

import { CryptoManager } from '@hiprax/crypto';

const crypto = new CryptoManager();

// Encrypt text
const encrypted = await crypto.encryptText(
  'Hello World',
  'MySecureP@ssw0rd123!'
);
console.log('Encrypted:', encrypted);

// Decrypt text
const decrypted = await crypto.decryptText(encrypted, 'MySecureP@ssw0rd123!');
console.log('Decrypted:', decrypted);

Text: synchronous operations

For scenarios where you need synchronous operations (note: uses PBKDF2 instead of Argon2id for key derivation):

import { CryptoManager } from '@hiprax/crypto';

const crypto = new CryptoManager();

// Encrypt text synchronously
const encrypted = crypto.encryptTextSync('Hello World', 'MySecureP@ssw0rd123!');
console.log('Encrypted:', encrypted);

// Decrypt text synchronously
const decrypted = crypto.decryptTextSync(encrypted, 'MySecureP@ssw0rd123!');
console.log('Decrypted:', decrypted);

Using Default Passphrase

You can set a default passphrase when creating the CryptoManager instance, which allows you to encrypt and decrypt without specifying a password each time:

import { CryptoManager } from '@hiprax/crypto';

// Create instance with default passphrase
const crypto = new CryptoManager({
  defaultPassphrase: 'MySecureP@ssw0rd123!',
});

// Encrypt text without specifying password
const encrypted = await crypto.encryptText('Hello World');
console.log('Encrypted:', encrypted);

// Decrypt text without specifying password
const decrypted = await crypto.decryptText(encrypted);
console.log('Decrypted:', decrypted);

// You can still override with a custom password
const encryptedWithCustom = await crypto.encryptText(
  'Hello World',
  'CustomP@ssw0rd456!'
);

Memory-retention caveat. Configuring defaultPassphrase keeps the password resident as a regular V8 string for the full lifetime of the CryptoManager instance — and beyond, until V8's garbage collector reclaims any internal copies the engine made along the way (interning, deopt paths, etc.). The library cannot scrub V8 strings; secureClear only zero-fills Buffer-backed allocations. For long-lived processes that handle sensitive data, prefer passing the password explicitly to each encrypt/decrypt call: that bounds the password's V8-string lifetime to the call frame instead of the manager. The convenience of defaultPassphrase is appropriate for short-lived scripts, CLI tools, or scopes where the password's residency in process memory is not part of your threat model. See the Threat Model section below for the broader memory-hygiene picture.

File Encryption

Asynchronous File Operations (Recommended)

import { CryptoManager } from '@hiprax/crypto';

const crypto = new CryptoManager();

// Encrypt file
await crypto.encryptFile('input.txt', 'output.enc', 'MySecureP@ssw0rd123!');

// Decrypt file
await crypto.decryptFile('output.enc', 'decrypted.txt', 'MySecureP@ssw0rd123!');

Synchronous File Operations

For scenarios where you need synchronous file operations (note: uses PBKDF2 instead of Argon2id for key derivation):

import { CryptoManager } from '@hiprax/crypto';

const crypto = new CryptoManager();

// Encrypt file synchronously
crypto.encryptFileSync('input.txt', 'output.enc', 'MySecureP@ssw0rd123!');

// Decrypt file synchronously
crypto.decryptFileSync('output.enc', 'decrypted.txt', 'MySecureP@ssw0rd123!');

File Encryption with Default Passphrase

Files with a default passphrase: asynchronous

import { CryptoManager } from '@hiprax/crypto';

// Create instance with default passphrase
const crypto = new CryptoManager({
  defaultPassphrase: 'MySecureP@ssw0rd123!',
});

// Encrypt file without specifying password
await crypto.encryptFile('input.txt', 'output.enc');

// Decrypt file without specifying password
await crypto.decryptFile('output.enc', 'decrypted.txt');

// You can still override with a custom password
await crypto.encryptFile('input.txt', 'output.enc', 'CustomP@ssw0rd456!');

Files with a default passphrase: synchronous

import { CryptoManager } from '@hiprax/crypto';

// Create instance with default passphrase
const crypto = new CryptoManager({
  defaultPassphrase: 'MySecureP@ssw0rd123!',
});

// Encrypt file synchronously without specifying password
crypto.encryptFileSync('input.txt', 'output.enc');

// Decrypt file synchronously without specifying password
crypto.decryptFileSync('output.enc', 'decrypted.txt');

// You can still override with a custom password
crypto.encryptFileSync('input.txt', 'output.enc', 'CustomP@ssw0rd456!');

Custom Configuration

import { CryptoManager } from '@hiprax/crypto';

const crypto = new CryptoManager({
  memoryCost: 2 ** 19, // 512 MiB — the ULTRA tier floor
  timeCost: 4, // Higher time cost
  parallelism: 2, // Use 2 threads
  aad: 'my-app-v1', // Custom AAD
});

console.log('Security Level:', crypto.getSecurityLevel()); // 'ultra'

Note: in pre-1.0 development (prior to the v0.15.0 dev iteration) the ULTRA tier was memoryCost: 2 ** 18 (256 MiB). It is 2 ** 19 (512 MiB) in v1.0.0 so the bar tracks OWASP 2026 guidance — the previous ULTRA configuration now classifies as HIGH.

📚 API Reference

CryptoManager

The main class for encryption/decryption operations.

Constructor

const crypto = new CryptoManager(options?: CryptoManagerOptions);

Options:

  • memoryCost (number): Argon2 memory cost in KiB (default: 131072 — 2 ** 17, 128 MiB, the HIGH tier; see Security Levels). This is deliberately above every configuration OWASP lists. OWASP's Password Storage Cheat Sheet does two separate things. It states a minimum — "Use Argon2id with a minimum configuration of 19 MiB of memory, an iteration count of 2, and 1 degree of parallelism" — and it separately lists five configurations that "provide an equal level of defense, and the only difference is a trade off between CPU and RAM usage": m=47104 (46 MiB) t=1, m=19456 (19 MiB) t=2, m=12288 (12 MiB) t=3, m=9216 (9 MiB) t=4, m=7168 (7 MiB) t=5, all at p=1. It designates none of them a "first choice" — the ordering is a CPU/RAM trade-off, not a ranking — and the lower-memory rows are not below the minimum, because each raises t as m falls. At 128 MiB the library's default carries roughly 2.8x the memory of the highest-memory entry on that list and clears the stated minimum on both axes, so it is a conservative choice rather than a quotation from it. Resource-constrained callers (mobile, embedded, low-memory containers) can opt back into the previous 64 MiB profile by passing memoryCost: 65536 (2 ** 16).
  • timeCost (number): Argon2 time cost (default: 3)
  • parallelism (number): Argon2 parallelism (default: 1)
  • aad (string): Custom Additional Authenticated Data (default: 'secure-crypto-tool-v2')
  • defaultPassphrase (string): Default passphrase to use when no password is provided to encryption/decryption methods
  • legacyMode ('auto' | 'strict' | 'reject'): How to handle legacy (pre-v1) ciphertexts during decryption — 'auto' (default) accepts them, 'strict' rejects with LEGACY_FORMAT_REJECTED, 'reject' rejects with UNSUPPORTED_FORMAT. New ciphertexts are always produced in v1 format. See Ciphertext Format.
  • pbkdf2Iterations (number): PBKDF2 iteration count for sync key derivation (default: 600000 — matches OWASP 2023+ recommendation for PBKDF2-HMAC-SHA256). The chosen value is embedded in every v1 ciphertext header produced by sync paths so it travels with the ciphertext and decryption remains correct even if you change the default later. Must be a positive integer.
  • legacyPbkdf2Iterations (number): PBKDF2 iteration count assumed when decrypting legacy v0 sync ciphertexts (those produced before the versioned ciphertext format and which carry no embedded iteration count). Default: 100000 — the value baked into every v0 sync ciphertext produced by versions of this library prior to 0.11.0. Override only if you have legacy data that was produced with a non-default iteration count. Has no effect on v1 ciphertexts.
  • skipPasswordValidation (boolean): When true, the constructor skips strength validation of defaultPassphrase only (default: false). This does not disable encryption-time password validation, and does not disable Unicode NFC normalisation — use it solely to construct a manager for decrypting legacy data whose password predates the current strength rules. See Password Requirements.
  • decryptKdfLimits (object): Ceilings — and optional floors — on the KDF cost this manager will honour from a ciphertext's own header when decrypting. A ciphertext carries the Argon2id/PBKDF2 parameters that produced it, and those bytes are not authenticated until after a key has been derived, so they control how much work an attacker can make you do before you can reject their input. Defaults to a generous but bounded budget (Node: maxMemoryCost: 2 ** 19 = 512 MiB, maxTimeCost: 10, maxParallelism: 16, maxWork: 2 ** 22 KiB-passes, maxPbkdf2Iterations: 2_000_000; the browser build is tighter still at 2 ** 18 / 2 ** 20). Each omitted ceiling widens to this instance's own cost, so a manager can always decrypt its own output. See Decrypting untrusted ciphertext.
  • legacyHeaderAad (boolean): Backward-compat shim for v1 ciphertexts produced by v1.0.0 (default: false). When true, v1 ciphertext AAD reverts to the v1.0.0 format (just aad, header bytes not bound) so v1.0.0-produced ciphertexts still decrypt; the default false binds the header bytes into the AAD. Affects v1 ciphertexts only (v0 always uses aad alone). Leave false for new code; use only as a temporary migration aid. See Migration: v1.0.0 → v1.1.0.

Every option is validated in the constructor, so a misconfiguration fails at construction rather than at first use. Each rejection is a CryptoError with a specific code:

| Rejected value | code | type | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------ | | memoryCost not a positive integer / above 2 ** 22 / below 8 * parallelism | INVALID_MEMORY_COST / MEMORY_COST_TOO_LARGE / MEMORY_COST_TOO_SMALL | INVALID_INPUT | | timeCost not a positive integer / above 100 | INVALID_TIME_COST / TIME_COST_TOO_LARGE | INVALID_INPUT | | parallelism not a positive integer / above 64 | INVALID_PARALLELISM / PARALLELISM_TOO_LARGE | INVALID_INPUT | | pbkdf2Iterations not a positive integer / above 10_000_000 | INVALID_PBKDF2_ITERATIONS / PBKDF2_ITERATIONS_TOO_LARGE | INVALID_INPUT | | legacyPbkdf2Iterations not a positive integer / above 10_000_000 | INVALID_LEGACY_PBKDF2_ITERATIONS / LEGACY_PBKDF2_ITERATIONS_TOO_LARGE | INVALID_INPUT | | legacyMode not one of 'auto'/'strict'/'reject' | INVALID_LEGACY_MODE | INVALID_INPUT | | aad not a string | INVALID_AAD | INVALID_INPUT | | defaultPassphrase too weak (unless skipPasswordValidation: true) | WEAK_PASSWORD | INVALID_PASSWORD | | decryptKdfLimits not a plain object / a ceiling that is not a positive integer / a floor that is negative / a floor above the largest value its ceilings can actually reach (min(maxWork, maxMemoryCost × maxTimeCost), so an unsatisfiable policy is caught at construction rather than on first decrypt) | INVALID_DECRYPT_KDF_LIMITS | INVALID_INPUT | | any decryptKdfLimits field — ceiling or floor — above the corresponding wire-format cap (no header could carry such a value, so the limit could never bind) | DECRYPT_KDF_LIMIT_TOO_LARGE | INVALID_INPUT |

The memoryCost >= 8 * parallelism floor is RFC 9106 §3.1 and is checked on the resolved values, after defaults are applied — so new CryptoManager({ memoryCost: 256, parallelism: 64 }) is rejected with MEMORY_COST_TOO_SMALL (256 < 512), while raising parallelism alone is fine because the 128 MiB default clears the floor for every legal parallelism. The three upper bounds mirror the wire-format DoS caps enforced by parseHeader: a value above them would produce a ciphertext this library then refuses to decrypt.

Methods

encryptText(text: string, password?: string): Promise<string>

Encrypts text with a password using Argon2id key derivation. If no password is provided and a default passphrase is set, the default passphrase will be used.

const encrypted = await crypto.encryptText(
  'Hello World',
  'MySecureP@ssw0rd123!'
);
// Returns: base64url encoded string

// With default passphrase
const crypto = new CryptoManager({ defaultPassphrase: 'MySecureP@ssw0rd123!' });
const encrypted = await crypto.encryptText('Hello World');
decryptText(encryptedText: string, password?: string): Promise<string>

Decrypts text with a password. If no password is provided and a default passphrase is set, the default passphrase will be used.

const decrypted = await crypto.decryptText(encrypted, 'MySecureP@ssw0rd123!');
// Returns: original text

// With default passphrase
const crypto = new CryptoManager({ defaultPassphrase: 'MySecureP@ssw0rd123!' });
const decrypted = await crypto.decryptText(encrypted);
encryptBytes(data: Uint8Array, password?: string): Promise<Uint8Array>

Isomorphic in-memory encryption — the same method (and the same output bytes) runs in Node and the browser. Encrypts raw bytes with Argon2id key derivation, producing a v1 ciphertext in the text byte layout [header:22][salt:32][iv:12][tag:16][ciphertext]. encryptText is a thin base64url wrapper over this method, so there is exactly one wire format. The empty Uint8Array is accepted and produces a valid authenticated ciphertext; the caller's data buffer is never mutated or scrubbed. See Isomorphic API & Browser Support.

const bytes = new TextEncoder().encode('Hello World');
const ct = await crypto.encryptBytes(bytes, 'MySecureP@ssw0rd123!');
// Returns: Uint8Array (v1 ciphertext, HPCR magic)
decryptBytes(data: Uint8Array, password?: string): Promise<Uint8Array>

Isomorphic counterpart of encryptBytes. Decrypts v1 (Argon2id) or legacy v0 ciphertext bytes, mirroring decryptText's byte path exactly (header parse, embedded-parameter override, header-bound AAD, and the legacyMode v0 fallback). Every confidentiality-relevant failure (wrong password, tampering) surfaces as the generic DECRYPTION_FAILED.

const back = await crypto.decryptBytes(ct, 'MySecureP@ssw0rd123!');
console.log(new TextDecoder().decode(back)); // 'Hello World'
encryptTextSync(text: string, password?: string): string

Synchronous version of text encryption. Uses PBKDF2 for key derivation instead of Argon2id for synchronous operation.

const encrypted = crypto.encryptTextSync('Hello World', 'MySecureP@ssw0rd123!');
// Returns: base64url encoded string

// With default passphrase
const crypto = new CryptoManager({ defaultPassphrase: 'MySecureP@ssw0rd123!' });
const encrypted = crypto.encryptTextSync('Hello World');
decryptTextSync(encryptedText: string, password?: string): string

Synchronous version of text decryption. Uses PBKDF2 for key derivation instead of Argon2id for synchronous operation.

const decrypted = crypto.decryptTextSync(encrypted, 'MySecureP@ssw0rd123!');
// Returns: original text

// With default passphrase
const crypto = new CryptoManager({ defaultPassphrase: 'MySecureP@ssw0rd123!' });
const decrypted = crypto.decryptTextSync(encrypted);
encryptFile(inputPath: string, outputPath: string, password?: string, progress?: ProgressCallback): Promise<void>

Encrypts a file with a password. Uses streaming, so peak memory is bounded by the stream's high-water mark regardless of input size. Output is written to a sibling temp file (${outputPath}.<random>.tmp) and atomically renamed to outputPath only on full success — readers of outputPath therefore never observe a half-written ciphertext, and any pre-existing file at outputPath is preserved if encryption errors out. If no password is provided and a default passphrase is set, the default passphrase will be used. Automatically creates the output directory if it doesn't exist. The optional progress callback receives (bytesProcessed, totalBytes) events — see Progress callbacks for file ops for the contract.

await crypto.encryptFile('input.txt', 'output.enc', 'MySecureP@ssw0rd123!');

// With default passphrase
const crypto = new CryptoManager({ defaultPassphrase: 'MySecureP@ssw0rd123!' });
await crypto.encryptFile('input.txt', 'output.enc');

// With progress callback
await crypto.encryptFile(
  'input.txt',
  'output.enc',
  'MySecureP@ssw0rd123!',
  (processed, total) => {
    const pct = total === 0 ? 100 : Math.round((processed / total) * 100);
    console.log(`encrypt ${pct}% (${processed}/${total} bytes)`);
  }
);
decryptFile(inputPath: string, outputPath: string, password?: string, progress?: ProgressCallback): Promise<void>

Decrypts a file with a password. Streams the ciphertext through crypto.createDecipheriv() so the full ciphertext never sits in memory at once — multi-GiB ciphertexts decrypt with bounded memory. Output is written to a sibling temp file and atomically renamed to outputPath only after decipher.final() validates the GCM auth tag. Both v0 (legacy, no header) and v1 (preferred, 22-byte header) ciphertext layouts are supported (subject to the constructor's legacyMode for v0). If no password is provided and a default passphrase is set, the default passphrase will be used. Automatically creates the output directory if it doesn't exist. The optional progress callback receives (bytesProcessed, totalBytes) events where both values are denominated in input ciphertext bytes — see Progress callbacks for file ops.

await crypto.decryptFile('output.enc', 'decrypted.txt', 'MySecureP@ssw0rd123!');

// With default passphrase
const crypto = new CryptoManager({ defaultPassphrase: 'MySecureP@ssw0rd123!' });
await crypto.decryptFile('output.enc', 'decrypted.txt');

// With progress callback
await crypto.decryptFile(
  'output.enc',
  'decrypted.txt',
  'MySecureP@ssw0rd123!',
  (processed, total) => console.log(`decrypt ${processed}/${total} bytes`)
);
encryptFileSync(inputPath: string, outputPath: string, password?: string, progress?: ProgressCallback): void

Synchronous version of file encryption. Uses PBKDF2 for key derivation instead of Argon2id for synchronous operation. Like the async path, output is staged to a sibling temp file and atomically renamed to outputPath only on success; pre-existing files at outputPath are preserved on error. The optional progress callback fires twice — once before encryption (0/totalBytes) and once after the rename succeeds (totalBytes/totalBytes); the input is streamed internally in fixed 64 KiB chunks, but the synchronous encrypt path emits no per-chunk progress events between the two. See Progress callbacks for file ops.

crypto.encryptFileSync('input.txt', 'output.enc', 'MySecureP@ssw0rd123!');

// With default passphrase
const crypto = new CryptoManager({ defaultPassphrase: 'MySecureP@ssw0rd123!' });
crypto.encryptFileSync('input.txt', 'output.enc');

// With progress callback
crypto.encryptFileSync(
  'input.txt',
  'output.enc',
  'MySecureP@ssw0rd123!',
  (processed, total) => console.log(`encrypt ${processed}/${total} bytes`)
);
decryptFileSync(inputPath: string, outputPath: string, password?: string, progress?: ProgressCallback): void

Synchronous version of file decryption. Streams the ciphertext through crypto.createDecipheriv() in fixed 64 KiB chunks, read with fs.readSync and written with fs.writeFileSync(fd, chunk), so peak memory is bounded regardless of input size. Uses PBKDF2 for key derivation instead of Argon2id for synchronous operation. Both v0 (legacy) and v1 (preferred) ciphertext layouts are supported (subject to legacyMode). Output is staged to a sibling temp file and atomically renamed only after decipher.final() validates the GCM auth tag. The optional progress callback fires once before the body loop, once per body chunk, and once after the rename succeeds — see Progress callbacks for file ops.

crypto.decryptFileSync('output.enc', 'decrypted.txt', 'MySecureP@ssw0rd123!');

// With default passphrase
const crypto = new CryptoManager({ defaultPassphrase: 'MySecureP@ssw0rd123!' });
crypto.decryptFileSync('output.enc', 'decrypted.txt');

// With progress callback
crypto.decryptFileSync(
  'output.enc',
  'decrypted.txt',
  'MySecureP@ssw0rd123!',
  (processed, total) => console.log(`decrypt ${processed}/${total} bytes`)
);
encryptContainer(data: Uint8Array, password?: string, meta?: ContainerMetadataInput): Promise<Uint8Array>

Isomorphic (Node + browser). Encrypts data into a self-describing, authenticated v2 container — an additive envelope format (magic HPCR, version 0x02) that is separate from the v1 text/file ciphertext. It wraps a random per-message data-encryption key under an Argon2id key-encryption key, stores optional filename/mime metadata confidentially (encrypted, never in cleartext), and embeds the plaintext SHA-256 for an end-to-end integrity check. See Container Mode.

const data = new TextEncoder().encode('report contents');
const container = await crypto.encryptContainer(data, 'MySecureP@ssw0rd123!', {
  filename: 'report.txt',
  mime: 'text/plain',
});
// Returns: Uint8Array (v2 container). In Node: writeFileSync('report.hpcr', container).
decryptContainer(container: Uint8Array, password?: string): Promise<DecryptedContainer>

Isomorphic counterpart of encryptContainer. Returns { data, meta } where meta is the authenticated { filename?, mime?, size }. The embedded SHA-256 is re-verified before returning; a mismatch throws CryptoError with code CONTAINER_INTEGRITY_FAILED. A v0/v1 blob (no HPCR magic, or a version byte other than 0x02) is rejected before any key derivation runs, so decryptContainer never accepts a non-container.

const { data, meta } = await crypto.decryptContainer(
  container,
  'MySecureP@ssw0rd123!'
);
console.log(meta); // { size: 15, filename: 'report.txt', mime: 'text/plain' }
console.log(new TextDecoder().decode(data)); // 'report contents'
validatePassword(password: string): boolean

Validates password strength.

const isValid = crypto.validatePassword('MySecureP@ssw0rd123!');
// Returns: boolean
generateSecureRandom(length: number): Buffer

Generates cryptographically secure random bytes.

const random = crypto.generateSecureRandom(32);
// Returns: Buffer
deriveKey(password: string, salt: Buffer, overrides?: { memoryCost: number; timeCost: number; parallelism: number }): Promise<Buffer>

Derives an encryption key from a password using Argon2id. The salt must be a 32-byte Buffer (INVALID_SALT otherwise). overrides replaces this instance's Argon2id parameters for a single call — it is how the decrypt paths honour the parameters embedded in a v1 header rather than the constructor's defaults; omit it for normal use.

[!WARNING] overrides is not bounded by decryptKdfLimits. This is a low-level primitive, so its cost parameters are yours rather than a ciphertext's and are deliberately unpoliced. Passing header-derived parameters straight through, as the second example below does, re-creates the pre-1.8.0 amplification in your own code. That example is safe only because the ciphertext is your own; on untrusted input call assertKdfWithinDecryptLimits(header.params, cm.getDecryptKdfLimits()) first, or use the high-level decrypt methods, which do it for you. See The low-level primitives are outside this policy.

const salt = crypto.generateSecureRandom(32);
const key = await crypto.deriveKey('MySecureP@ssw0rd123!', salt);
// Returns: 32-byte Buffer

// Reproduce the key of an existing ciphertext: read its embedded parameters,
// and pair them with THAT ciphertext's own salt (bytes 22..54 of a v1 record).
const header = crypto.inspectHeader(ciphertext);
if (header !== null && header.params.kind === 'argon2id') {
  const embeddedSalt = Buffer.from(ciphertextBytes.subarray(22, 54));
  const sameKey = await crypto.deriveKey(
    'MySecureP@ssw0rd123!',
    embeddedSalt,
    header.params
  );
}
deriveKeySync(password: string, salt: Buffer, iterations?: number): Buffer

Synchronous version of key derivation using PBKDF2 (default: 600,000 iterations, SHA-256) instead of Argon2id. Pass an explicit iterations argument to override the per-instance default (used internally to apply the iteration count embedded in v1 ciphertext headers, but you can also pass it directly when calling deriveKeySync yourself).

[!WARNING] iterations is not bounded by decryptKdfLimits, and this call blocks the event loop. The same caller-obligation boundary as deriveKey, but sharper: crypto.pbkdf2Sync runs on the calling thread, so an attacker-chosen count stalls the whole process (measured 1.00 s at 2,000,000 iterations, 5,094 ms at the 10,000,000 wire cap) and nothing downstream will refuse it. Apply assertKdfWithinDecryptLimits yourself before passing a header-derived count.

const salt = crypto.generateSecureRandom(32);
// Default iterations (600,000 — current OWASP recommendation)
const key = crypto.deriveKeySync('MySecureP@ssw0rd123!', salt);
// Returns: 32-byte Buffer

// Override iterations explicitly
const customKey = crypto.deriveKeySync('MySecureP@ssw0rd123!', salt, 250000);
encryptData(data: Buffer, key: Buffer, iv: Buffer, aadOverride?: Buffer): EncryptionResult

Low-level AES-256-GCM encryption. Returns { encrypted: Buffer, tag: Buffer }. Every argument is type- and length-checked: INVALID_DATA, INVALID_KEY (32 bytes), INVALID_IV (12 bytes), INVALID_AAD (must be a Buffer when supplied).

aadOverride replaces the instance's configured aad for this one call. It is how the high-level v1 paths bind the on-disk header bytes into the auth tag. If you pass it, you must pass the byte-identical value to decryptData, or authentication fails.

Security: the caller is responsible for ensuring each (key, iv) pair is used at most once. See AES-GCM (key, IV) reuse for the full explanation and recommended pattern. Prefer encryptText / encryptFile for any code that does not have a specific reason to manage IVs by hand.

Throws CryptoError(INVALID_INPUT, 'DATA_TOO_LARGE_FOR_GCM') if data exceeds the AES-GCM per-invocation bound — see The AES-GCM per-invocation size limit.

const key = crypto.generateSecureRandom(32);
const iv = crypto.generateSecureRandom(12); // fresh random IV per message
const { encrypted, tag } = crypto.encryptData(Buffer.from('data'), key, iv);
decryptData(encryptedData: Buffer, key: Buffer, iv: Buffer, tag: Buffer, aadOverride?: Buffer): Buffer

Low-level AES-256-GCM decryption. Returns the decrypted data as a Buffer.

The caller must supply the exact (key, iv, tag) that were produced by encryptData, plus the matching AAD — either the instance's configured aad (the default) or, if encryptData was given an aadOverride, that same override. Tag-check failure surfaces as CryptoError with code DECRYPTION_FAILED regardless of which condition failed (wrong key, wrong IV, wrong AAD, or tampered ciphertext) — the generic message is intentional, to avoid leaking which case applied. A ciphertext past the AES-GCM per-invocation bound is rejected up front with DATA_TOO_LARGE_FOR_GCM, since no such input could have been produced correctly.

const decrypted = crypto.decryptData(encrypted, key, iv, tag);
secureClear(buffer: Uint8Array): void

Securely zeroes a buffer to remove sensitive data from memory. The parameter is Uint8Array (every Node Buffer is a Uint8Array, so existing Buffer call sites are unaffected); a plain Uint8Array — e.g. browser key material — is actually scrubbed rather than silently skipped. Best-effort only: it cannot reach immutable V8 strings or GC-managed copies (see Threat Model).

crypto.secureClear(key);
getParameters(): EncryptionParameters

Gets current encryption parameters.

const params = crypto.getParameters();
// Returns: object with algorithm details
getSecurityLevel(): SecurityLevel

Gets security level based on configuration.

const level = crypto.getSecurityLevel();
// Returns: 'low' | 'medium' | 'high' | 'ultra'
hasDefaultPassphrase(): boolean

Checks if a default passphrase is configured.

const hasDefault = crypto.hasDefaultPassphrase();
// Returns: boolean indicating if default passphrase is set
getLegacyMode(): LegacyMode

Returns the configured legacy-format handling mode — one of 'auto', 'strict', or 'reject' (see the legacyMode constructor option).

const mode = crypto.getLegacyMode();
// Returns: 'auto' | 'strict' | 'reject'
inspectHeader(input: string | Uint8Array): ParsedHeader | null

Parses the v1 ciphertext header without decrypting. Returns a ParsedHeader ({ version, kdfId, params, headerLen }) for a v1 ciphertext, or null when the input lacks the v1 magic bytes (i.e. a legacy v0 ciphertext). Accepts either a base64url string (text-format output) or a Uint8Array (file contents — a Node Buffer is a Uint8Array, so Buffer inputs keep working). String inputs are validated as well-formed base64url before decoding and throw CryptoError with code INVALID_BASE64URL on malformed input — so an invalid string fails fast instead of being mistaken for a v0 ciphertext; byte inputs are read as-is. An empty string, or an argument that is neither a string nor a Uint8Array, throws code INVALID_INPUT instead. A buffer that begins with the v1 magic but is otherwise malformed throws a specific parser CryptoError (e.g. TRUNCATED_HEADER, UNSUPPORTED_KDF, INVALID_HEADER_PARAM, KDF_PARAMS_OUT_OF_BOUNDS). See the worked example under Ciphertext Format (v1).

Inspecting a large ciphertext is cheap: since v1.6.0 only the first 32 base64url characters of a string input are decoded — enough for the 22-byte header with two bytes to spare — so the call allocates 24 bytes rather than a copy of the whole payload, whatever its size. The well-formedness check still scans the entire string (narrowing it would stop rejecting a malformed tail), but it does so in one allocation-free pass.

This is a v1 inspector, not a format sniffer. A v2 container reuses the same HPCR magic and the same 22-byte header shape, so it passes the magic check and is then rejected on its version byte: inspectHeader on a container throws CryptoError with type DECRYPTION_FAILED and code UNSUPPORTED_VERSION. It never returns null for a container, and there is no input for which it reports "v2". If the input may be either format, classify it first — see Telling the formats apart.

Types and Enums

The library exports all types, interfaces, and enums for TypeScript consumers:

import {
  // Error handling
  CryptoError,
  CryptoErrorType,
  // Enums
  SecurityLevel,
  EncryptionAlgorithm,
  // Interfaces
  type CryptoManagerOptions,
  type EncryptionResult, // Node only — names `Buffer`; see the note below
  type EncryptionParameters,
  type ValidationResult,
  type FileInfo,
  type RetryConfig,
  type ProgressCallback,
  // Type alias for the `legacyMode` option and `getLegacyMode()`
  type LegacyMode,
  // Container mode (v2 envelope)
  type ContainerMetadataInput,
  type ContainerMetadata,
  type DecryptedContainer,
  // Wire-format header types (returned by `inspectHeader` / `parseHeader`)
  type ParsedHeader,
  type KdfId,
  type KdfHeaderParams,
  type Argon2idHeaderParams,
  type Pbkdf2HeaderParams,
  // Options bag for `validatePath` (Node only — declared in the Node-only utils)
  type ValidatePathOptions,
} from '@hiprax/crypto';

ParsedHeader is { version, kdfId, params, headerLen }, and params is the discriminated union KdfHeaderParams = Argon2idHeaderParams | Pbkdf2HeaderParams — narrow on params.kind ('argon2id' or 'pbkdf2-sha256') before reading memoryCost / iterations.

EncryptionResult is Node-only. It describes the { encrypted: Buffer, tag: Buffer } return of the low-level encryptData, so it names the Node Buffer global. As of v1.6.0 it lives in crypto-manager.ts rather than types.ts and is exported from @hiprax/crypto (unchanged) and, additionally, from @hiprax/crypto/crypto-manager. It is not part of the browser type surface — nothing on the browser build can produce one (encryptData there throws UNSUPPORTED_IN_BROWSER), and a browser-only TypeScript project would previously have failed to compile on the two Buffer references it carried. Every other name in the list above is available in both runtimes.

Utility Functions

Additional utility functions are also exported:

import {
  validateFile,
  validatePath,
  generateRandomString,
  validatePasswordStrength,
  generateUUID,
  sha256,
  generateRandomHex,
  secureStringCompare,
  formatFileSize,
  getFileExtension,
  isTextFile,
  sanitizeFilename,
  createBackupPath,
  isValidBase64,
  isValidBase64Url,
  createProgressBar,
  sleep,
  retryWithBackoff,
  getFileInfo,
} from '@hiprax/crypto';

// Validate if file exists and is accessible
const fileValidation = await validateFile('path/to/file.txt');

// Validate if path is valid for writing.
// `validatePath` rejects empty input, null bytes, ASCII control characters
// (codepoints `< 0x20` or `0x7F`), Windows-illegal characters
// (`<`, `>`, `:`, `"`, `|`, `?`, `*` — drive-letter prefix excluded),
// and literal `..` traversal segments after `path.normalize`.
const pathValidation = validatePath('path/to/output.txt');

// Optional: enforce that the input path resolves inside an allowed root.
// Useful for catching within-drive cross-traversal that the literal-`..`
// segment check on its own cannot detect (because `path.normalize`
// collapses internal `..` cancel-outs to a clean path). The check is
// segment-aware (no `/etc/sec` ↔ `/etc/secret` collision) and on Windows
// is case-insensitive and forward-slash-tolerant. NOTE: this is a
// syntactic / resolved-string check; it does NOT defend against
// symlink-based escapes.
const inProject = validatePath('/home/user/project/data/file.txt', {
  allowedRoot: '/home/user/project',
});
// inProject.isValid === true

// NOTE: this example is Windows-specific. Both drive-letter accommodations
// (stripping `C:` before the invalid-character scan, and before the `..`
// segment scan) are gated on `process.platform === 'win32'`.
const escape = validatePath('C:\\Users\\..\\Windows', {
  allowedRoot: 'C:\\Users',
});
// On Windows: isValid === false, error === 'Path is outside the allowed root'
// On POSIX:   isValid === false, error === 'File path contains invalid characters'
//             (the `:` is never stripped there, so the earlier scan rejects it)

// Generate secure random string
// (default 32 chars ≈ 190 bits of entropy; request >= 44 chars for a full
// 128-bit post-quantum margin — see the sizing note below this block)
const randomString = generateRandomString(32);

// Validate password strength with detailed feedback
const passwordCheck = validatePasswordStrength('MyPassword123!');
console.log('Score:', passwordCheck.score); // 0-5
console.log('Feedback:', passwordCheck.feedback); // Array of suggestions

// Generate UUID (v4 — 122 random bits; an identifier, NOT a bearer secret)
const uuid = generateUUID();

// Hash string with SHA-256
const hash = sha256('hello world');

// Generate random hex string (each hex char = 4 bits; use >= 64 chars for
// 256-bit bearer secrets — see the sizing note below this block)
const hex = generateRandomHex(16);

// Secure string comparison (constant time)
const isEqual = secureStringCompare('secret', 'secret');

// Format file size. Throws rather than guessing: NEGATIVE_FILE_SIZE for a negative
// count, INVALID_FILE_SIZE for NaN/Infinity/non-number, FILE_SIZE_TOO_LARGE above
// Number.MAX_SAFE_INTEGER. The unit ladder caps at TB.
const size = formatFileSize(1024 * 1024); // "1 MB"

// Get file extension (lowercase)
const ext = getFileExtension('photo.JPG'); // ".jpg"

// Check if file is text file
const isText = isTextFile('document.txt');

// Sanitize filename
const safeName = sanitizeFilename('file<name>.txt'); // "file_name_.txt"

// Create backup path
const backupPath = createBackupPath('file.txt'); // "file_2026-06-30T12-00-00_a1b2c3.backup.txt"

// Validate base64
const isValid = isValidBase64('SGVsbG8gV29ybGQ=');

// Validate base64url (the format used by this library's encrypted output).
// Careful: `isValidBase64Url` (capital U, Node-only, from utils) is a thin
// delegate to `isValidBase64url` (lowercase u, isomorphic, from the codec).
// Identical behaviour, but only the lowercase spelling exists in the browser build.
const isValidUrl = isValidBase64Url('SGVsbG8gV29ybGQ');

// Create progress bar (width defaults to 30; out-of-range inputs are clamped,
// a non-positive/non-integer width falls back to 30, and total <= 0 yields 0%)
const progress = createProgressBar(50, 100); // "[███████████████░░░░░░░░░░░░░░░] 50%"

// Sleep for specified time
await sleep(1000); // Sleep for 1 second

// Retry with exponential backoff
const result = await retryWithBackoff(
  async () => {
    // Some async operation that might fail
    return await someOperation();
  },
  { maxRetries: 3, baseDelay: 1000 }
);

// Get file information
const fileInfo = await getFileInfo('path/to/file.txt');
console.log('Size:', fileInfo.size);
console.log('Extension:', fileInfo.extension);
console.log('Is Text:', fileInfo.isTextFile);

Sizing random secrets for a post-quantum margin. generateRandomString and generateRandomHex draw from the OS CSPRNG, so their strength is purely a function of length. Grover's algorithm halves the effective entropy of a random secret against a quantum adversary, so to preserve a 128-bit post-quantum margin, size bearer secrets (API keys, session tokens, capability URLs) at 256 bits: generateRandomHex(64) (64 hex chars) or generateRandomString(44) (≈262 bits). The defaults (32 chars) are ample for identifiers and classical threat models. generateUUID output carries 122 random bits and is designed as a collision-resistant identifier — do not use it as an unguessable bearer token where post-quantum unpredictability matters. See Post-Quantum Security.

Wire-format and codec exports

Beyond CryptoManager and the utility helpers, both entry points re-export the wire-format constants and the pure byte codecs. They exist so that tooling can classify, inspect and transcode this library's output without decrypting it, and they are covered by the same compatibility promise as the rest of the public API.

import {
  // Password policy, shared with the manager
  isValidPassword,
  SECURITY_THRESHOLDS,

  // Format identity
  MAGIC_BYTES,
  MAGIC_LENGTH,
  VERSION_LENGTH,
  KDF_ID_LENGTH,
  KDF_PARAMS_LENGTH,
  HEADER_LENGTH, // 22
  FORMAT_VERSION, // 0x01 — v1 text/file ciphertext
  CONTAINER_VERSION, // 0x02 — v2 container
  KDF_ID_ARGON2ID, // 0x00
  KDF_ID_PBKDF2_SHA256, // 0x01

  // Header helpers