Skip to content

Security: Hiprax/crypto

SECURITY.md

Security Policy

@hiprax/crypto is a cryptography library. Bugs here can compromise the confidentiality, integrity, or authenticity of user data, so we treat security reports as a top priority and would rather hear about a suspected issue than miss a real one. Thanks for taking the time to investigate.

Supported Versions

The package is post-1.0 stable. Only the current 1.x major line receives security updates; pre-1.0 (0.x) lines are end-of-life and will not be patched. The next major bump (whenever 2.0.0 ships) will establish whether the previous major continues to receive security fixes; until then, the single supported line is 1.x.

Version Supported
1.x ✅
0.17.x ❌
0.16.x ❌
0.15.x ❌
< 0.15 ❌

If you need a fix backported to an unsupported line because you cannot upgrade, include the constraint in your report and we will evaluate it on a case-by-case basis. There is no guarantee of a backport.

Reporting a Vulnerability

Please do not file public issues, pull requests, or discussions for security problems. Public disclosure before a patch is ready puts every other user of the library at risk.

Use one of these private channels instead:

  1. GitHub Security Advisories (preferred). Use the Report a vulnerability button on the repository's Security tab. Private vulnerability reporting is enabled, so this form is open to anyone with a GitHub account, not just maintainers. It is private, free, and gives us a structured workflow for issuing fixes and CVEs. This is the channel we will use to coordinate the fix and disclosure with you.
  2. Email. If GitHub's advisory flow is not available to you, send the report to security@hiprax.com. Please include the same information as a GitHub advisory would: affected version(s), a clear reproduction (or proof of concept), the impact, and any suggested mitigations or patches.

If you would like to encrypt your email, request our PGP key in your initial contact and we will respond with the current key fingerprint and material.

Expected Response Times

Stage Target
Initial acknowledgement Within 72 hours of receipt
Triage and severity decision Within 7 days of acknowledgement
Fix or mitigation timeline Communicated alongside triage
Public disclosure Coordinated with the reporter once fix ships

If you have not heard back within 72 hours of submitting a private advisory, please follow up — it usually means the notification was missed, not ignored.

We will credit the reporter in the advisory and the release notes unless you prefer to remain anonymous. Please tell us in the report which you prefer.

Coordinated Disclosure

We follow a standard coordinated-disclosure model:

  1. You report the issue privately.
  2. We acknowledge, triage, and develop a fix in a private advisory branch.
  3. We publish a patched release with the version bumped per semver and the CHANGELOG updated.
  4. We publish the GitHub Security Advisory (with CVE if appropriate) once the patched version is available, naming the reporter unless they have asked to remain anonymous.

We aim to keep the embargo window short — usually no more than 90 days — and will let you know if we need more time.

Scope

The following classes of issue are in scope for this policy and will be treated as security incidents:

  • Cryptographic correctness. Wrong algorithm parameters, incorrect padding, authentication-tag verification bypasses, malleable ciphertexts, KDF usage bugs, or any deviation from the documented constructions (AES-256-GCM, Argon2id, PBKDF2-HMAC-SHA256).
  • Key handling. Keys derived from the wrong inputs, key material reused unsafely (e.g. AES-GCM (key, iv) reuse from the high-level API), keys leaking through error messages or unexpected return values, or key material not being cleared from buffers when documented to be cleared.
  • Side channels. Timing oracles in password / tag / key comparisons, error messages that distinguish wrong-password from corrupt-ciphertext (the generic DECRYPTION_FAILED is intentional — see the README "Error Handling" section).
  • Authenticity bypass. Any path that lets ciphertext be modified without the GCM auth tag flagging it, including header tampering on v1 ciphertexts.
  • Format / parser issues. Bugs in the ciphertext header parsers — the v1 header parser (format.ts / the pure format-core.ts) and the v2 container parser (core.ts) — that allow malformed input to cause crashes, infinite loops, out-of-bounds reads, or the wrong KDF / parameters being applied.
  • Resource amplification from unauthenticated ciphertext parameters. A ciphertext carries the KDF parameters that produced it, and they cannot be authenticated until after a key has been derived. A small attacker-supplied blob that causes disproportionate CPU, memory, thread or event-loop consumption before authentication is in scope, and is bounded by the wire-format caps together with the per-instance decryptKdfLimits budget. This is distinct from the large-input exclusion below: the concern is amplification relative to input size, not cost proportional to it.
  • Path traversal in the file APIs. Any path that lets an attacker-supplied string write outside the intended directory tree, beyond the documented syntactic guarantees of validatePath.
  • Vulnerabilities in argon2 or other runtime dependencies that materially weaken the library's guarantees. These will be fixed by bumping the affected dep; please report them privately the same way. Note that as of v1.7.0 the Node build's Argon2id can come from any of three providers (see "Argon2id providers" below): a defect in the optional argon2 or hash-wasm packages is in scope here, while a defect in Node's own crypto.argon2 belongs to the Node.js project and should be reported through https://github.com/nodejs/node/security. If it affects how this library uses the built-in — wrong parameters, a mishandled error, a key not cleared — that part is ours; tell us as well.

Out of Scope

The following are not treated as security issues by this project. Please file them as regular GitHub issues instead — we still want the bug reports, just through the normal channel:

  • Denial of service via large legitimate input. Encrypting a 100 GiB file is going to use a lot of disk and memory; that's not a vulnerability, that's how encryption works. The library streams inputs where possible (see the README "File Encryption" section), but a caller that passes pathologically large inputs is responsible for their own resource limits. This exclusion is about cost proportional to input size only. Resource amplification — a small attacker-controlled ciphertext causing disproportionate work before authentication — is explicitly in scope; see the Scope section above. Three things that remain caller responsibilities even so, and are documented rather than treated as defects: the synchronous PBKDF2 paths run on the calling thread, so they are unsuitable for untrusted input at any iteration count; the async Argon2id paths share the libuv threadpool when the resolved provider is the native addon or the runtime's built-in, so concurrency control and rate limiting are yours; and when the resolved provider is hash-wasm the async paths do not use that pool at all but derive on the calling thread, blocking the event loop for the duration (measured 694 ms at the 128 MiB default and 6,309 ms at the Node decrypt budget's ceiling, with zero timer ticks in both). Since both Argon2id packages are optional and a failed native build does not fail an install, a deployment can be in that state silently; getArgon2Provider() reports which provider answered so a service can refuse to start on the blocking one.
  • Vulnerabilities in transitive dev dependencies. Issues in jest, eslint, rimraf, typescript etc. that only affect the development toolchain and not the published package are tracked through GitHub Issues and npm audit. If a transitive dep ends up shipped in dist/, it becomes in scope.
  • Brute-force / password-guessing attacks against weak passwords. The library validates password strength on encryption (see README "Password Requirements"), but if a caller bypasses that or chooses a weak password, the resulting ciphertext is no stronger than the password. This is a limitation of the underlying construction, not a bug.
  • Theoretical attacks against AES-256-GCM, Argon2id, or PBKDF2-HMAC-SHA256 themselves. If a generic break of one of these primitives is published, we will respond. Generic "quantum computers will eventually break this" reports are answered by the documented post-quantum posture below and are not actionable unless they present a concrete new attack that invalidates it.
  • Missing best-practice hardening that has no documented impact (e.g. "the library should also support XChaCha20"). File those as feature requests through GitHub Issues.

Post-quantum posture

The library's position on quantum computing is documented here so security reports and audits can reference it directly.

  1. There is no public-key cryptography in this library. No RSA, no elliptic curves, no key exchange, no signatures, no certificates. Shor's algorithm — the quantum attack that catastrophically breaks public-key cryptography and motivates the NIST PQC standards (FIPS 203 ML-KEM, FIPS 204 ML-DSA, FIPS 205 SLH-DSA) — has no target here. Consequently there is no post-quantum migration to perform: those standards replace primitives this library does not use.
  2. Every primitive is treated as quantum-resistant by the relevant standards bodies at the parameter sizes used. AES-256-GCM sits at NIST post-quantum security Category 5 and is retained by NSA CNSA 2.0 for National Security Systems up to TOP SECRET; NIST IR 8547 explicitly excludes the symmetric standards (block ciphers, hash functions, HMAC, KDFs) from the PQC transition; BSI TR-02102-1 recommends 256-bit symmetric keys for long-term protection, which is what the library uses. Grover's algorithm yields only a quadratic speedup that parallelizes poorly and is gate-depth limited — NIST's own FAQ states it will provide "little or no advantage in attacking AES". The browser build introduces no new primitive: it uses the same AES-256-GCM, Argon2id, and SHA-256 (via Web Crypto and WebAssembly hash-wasm) over the same wire format, so this posture applies unchanged across runtimes. The only cross-runtime difference is the Argon2id default memory cost (32 MiB in the browser vs 128 MiB in Node), which affects brute-force cost, not post-quantum standing.
  3. The residual quantum-relevant risk is password entropy, not the cryptography. Grover halves the effective entropy of an offline password search. Mitigations in the design: per-message 32-byte random salts (no cross-ciphertext amortization) and the memory-hard Argon2id KDF, whose evaluation cost provably carries over into the reversible circuits a quantum attacker must build (Blocki-Holman-Lee, TCC 2022). Callers with harvest-now-decrypt-later threat models should use the async (Argon2id) path with a high-entropy passphrase — see the README "Post-Quantum Security" section for caller-facing guidance.
  4. Crypto-agility is built into the wire format. Each v1 ciphertext embeds its KDF identifier and full KDF parameters, so defaults can be raised (or a KDF added) in a minor release while old ciphertexts remain decryptable; the header's version byte reserves a clean escape hatch should guidance ever require a cipher change — 0x01 is the v1 ciphertext and 0x02 the additive v2 container, leaving 0x03 onward free. None is needed or foreseen.
  5. Hybrid PQC modes (ML-KEM/ML-DSA) are deliberately absent because the library performs no key exchange and no signing; there is nothing to hybridize, and adding those primitives would add attack surface without adding security.

The distribution channel (npm provenance via Sigstore, registry TLS) relies on the ecosystem's classical public-key infrastructure. That layer is outside this package's control, is a real-time integrity mechanism rather than harvestable ciphertext, and does not affect the confidentiality of data encrypted with this library.

Primary sources: NIST PQC FAQ (https://csrc.nist.gov/projects/post-quantum-cryptography/faqs); NIST IR 8547 (https://nvlpubs.nist.gov/nistpubs/ir/2024/NIST.IR.8547.ipd.pdf); NSA CNSA 2.0 FAQ (https://media.defense.gov/2022/Sep/07/2003071836/-1/-1/0/CSI_CNSA_2.0_FAQ_.PDF); BSI TR-02102-1; Kaplan et al., CRYPTO 2016 (https://arxiv.org/abs/1602.05973), whose Simon's-algorithm forgery applies only in the superposition-query (Q2) model and not to data at rest; Blocki, Holman & Lee, TCC 2022 (https://arxiv.org/abs/2110.04191).

Reports claiming quantum vulnerability are in scope only if they present a concrete attack that invalidates one of the five points above under a realistic (classical-query, Q1) threat model.

Browser build (threat-model notes)

As of v1.5.0 the library is isomorphic: the same in-memory async API (encryptBytes/decryptBytes/encryptText/decryptText/ encryptContainer/decryptContainer) runs in Node and in the browser over one wire format. The browser build uses Web Crypto (SubtleCrypto) for AES-256-GCM/SHA-256/CSPRNG and WebAssembly hash-wasm for Argon2id — the same primitives as Node, so the cryptographic guarantees above are unchanged. The differences that are relevant to a threat model are documented here so reports and audits can reference them:

  1. Weaker memory hygiene than Node. Web Crypto importKey copies the raw AES key bytes into an opaque CryptoKey object that JavaScript cannot reach or zero — so secureClear cannot scrub it (the engine does zero the transient raw-key copy it owns immediately after import). Combined with the immutable, GC-managed nature of V8 strings (passwords, decrypted text), the browser cannot forensically wipe key/plaintext material. Treat in-browser secret residency as bounded by GC, not by secureClear. This is a stronger form of the "String-copy memory leaks via V8" caveat below and is out of scope as a reportable vulnerability, the same as the Node V8 caveat.
  2. Secure-context requirement. crypto.subtle is only exposed in a secure context (HTTPS or localhost). On an insecure origin the engine cannot run and throws; serving the app over plain HTTP is a deployment error, not a library bug.
  3. Content-Security-Policy. The browser Argon2id path compiles WebAssembly, which a strict CSP blocks unless script-src includes 'wasm-unsafe-eval' (strictly narrower than 'unsafe-eval' — WASM only). hash-wasm instantiates from inline bytes, so no connect-src entry and no network fetch are involved; nothing leaves the browser. See the README "Content-Security-Policy (WASM)" subsection for the exact header.
  4. No node: in the browser graph. The browser entry (dist/index.browser.js) imports zero node: builtins and references no Buffer/process global; this isolation is enforced continuously (in CI and at publish) by three complementary static gates, which cover three disjoint defect classes: an esbuild platform:'browser' bundle gate (npm run check:browser) that fails on any node: specifier reaching the browser graph; an ESLint no-restricted-globals (Buffer/process) + no-restricted-imports (node:*) override on the isomorphic source files that catches a bare Buffer/process value esbuild cannot see; and npm run check:types:browser, which type-checks a browser-only consumer against the built dist/ through the browser export condition with no Node types available. Only the third can see a type-position Node reference: no-restricted-globals inspects value references only, and esbuild erases types before it sees them, so a Node type reaching dist/index.browser.d.ts or anything it re-exports fails the build too. So node:crypto/node:fs/node:stream can never enter a consumer's bundle.
  5. Node-only methods are unavailable, not silently degraded. The synchronous (PBKDF2), streaming-file, and Buffer-typed low-level methods throw CryptoError(INVALID_INPUT, 'UNSUPPORTED_IN_BROWSER') in the browser build rather than falling back to a weaker construction — there is no silent security downgrade.

Security-relevant configuration defaults

For reference, the library currently ships with the following security defaults. If a reported issue is mitigated by changing one of these, please call that out explicitly in the report.

  • Default Argon2id parameters (async paths, Node build): memoryCost = 2 ** 17 (128 MiB), timeCost = 3, parallelism = 1. This is roughly 2.8x the memory of the highest-memory Argon2id configuration OWASP's Password Storage Cheat Sheet lists (m=47104 / 46 MiB) and far above its stated minimum of 19 MiB at t=2 / p=1. OWASP designates no "first choice" among its five listed configurations; they are described as providing "an equal level of defense", differing only in a CPU/RAM trade-off. This library's default is a deliberately conservative choice, not a quotation from that list.

  • Default Argon2id parameters (browser build): memoryCost = 2 ** 15 (32 MiB), timeCost = 3, parallelism = 1 — a lighter default to avoid OOM on memory-constrained mobile browsers, and still strictly above OWASP's stated minimum configuration on both axes: ≈1.68× its 19 MiB of memory, at t=3 against its t=2. Note the library classifies this profile MEDIUM, whose threshold (16 MiB / t=2) does sit below that minimum; the browser default itself does not. This is a runtime-specific default, not a format change; each ciphertext embeds the exact KDF parameters used, so a ciphertext decrypts anywhere that can afford its embedded memoryCost.

  • Default PBKDF2 iterations (sync paths, Node only): 600000. Matches the OWASP 2023+ recommendation for PBKDF2-HMAC-SHA256, still current in 2026.

  • Ciphertext format: v1 (22-byte versioned header with embedded KDF parameters). Legacy v0 ciphertexts are accepted under legacyMode: 'auto' (the default) and rejected under 'strict' / 'reject'. The optional v2 container format (magic HPCR, version 0x02) is a separate, additive envelope — a two-layer KEK/DEK hierarchy, confidential encrypted metadata, and an embedded plaintext SHA-256 that is re-verified on decrypt (CONTAINER_INTEGRITY_FAILED on mismatch). It does not touch the v0/v1 paths and each format rejects the other's blobs.

  • AAD: "secure-crypto-tool-v2" by default, configurable per instance.

  • Argon2id providers (async paths, Node build): three, tried in a fixed order — the optional native argon2 addon, then Node's own crypto.argon2 (built into the runtime from Node 24.7.0), then the optional pure-WebAssembly hash-wasm. All three implement the 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. Six tuples are pinned as standing regression tests, alongside a multi-byte non-ASCII password in two normalisation forms, each checked against every provider the host has (all three here, and never fewer than two); two of the six are literally among the nine and the rest are neighbours chosen to reach the same corners, including the timeCost and parallelism values @types/node documents as out of range while this library permits them. Either way, which one answers is a performance and packaging question and has no effect on the strength or the portability of a ciphertext. There is deliberately no way to select one: interchangeable providers make a switch a way to choose a slower one and nothing else. Two consequences are worth stating for a threat model. First, the supply-chain surface is now optional: on Node >= 24.7.0, npm i @hiprax/crypto --omit=optional gives a fully working async API with no third-party Argon2id code in the dependency tree at all, which is the smallest attack surface this library can be installed with. Second, on Node 22 — the engines.node floor — there is no built-in, so one of the two optional packages remains required for any async path. If none of the three is available, the async paths refuse with CryptoError(MEMORY_ERROR, 'ARGON2_NOT_AVAILABLE'); they never silently downgrade to PBKDF2 or to a weaker profile. The browser build is unaffected by this: it uses hash-wasm unconditionally, because Web Crypto has no Argon2id.

  • Decrypt-side KDF budget (decryptKdfLimits, since v1.8.0): the ceilings this library will honour from a ciphertext's own header. Node defaults to maxMemoryCost: 2 ** 19 (512 MiB), maxTimeCost: 10, maxParallelism: 16, maxWork: 2 ** 22 KiB-passes and maxPbkdf2Iterations: 2_000_000; the browser build is tighter on exactly two of those, maxMemoryCost: 2 ** 18 and maxWork: 2 ** 20, while maxTimeCost, maxParallelism and maxPbkdf2Iterations are identical to Node's. It is tighter because its only Argon2id provider blocks the calling thread and enforces no memory ceiling of its own. Note that the Node ceilings bound ONE invocation: with the four-slot libuv threadpool the process worst case is four concurrent derivations. Each omitted ceiling widens to the instance's own configured cost, so a manager can always decrypt its own output. The optional minWork / minPbkdf2Iterations floors default to off.

See the README for the full parameter reference.

Memory-retention caveats

secureClear zeroes Buffer-backed allocations holding key material and plaintext after use, on a best-effort basis. Two known retention paths are NOT closed by secureClear and are documented here so callers can make informed decisions:

  • V8 internal string copies. Password and plaintext strings passed to the library (and any intermediate copies V8 creates for hashing, interning, or deoptimisation) live in V8's heap as immutable string objects. Buffer.fill(0) does not reach those copies. They are reclaimed only by the V8 garbage collector on its own schedule. This is documented in the README "Threat Model" under "String-copy memory leaks via V8".
  • defaultPassphrase retention. The defaultPassphrase constructor option is stored on the CryptoManager instance as a regular V8 string field. This is a deliberate design trade-off (convenience over scrubability) and the library cannot zero it. The passphrase remains resident for the full lifetime of the manager instance, plus any unbounded GC tail for V8-internal copies. For sensitive workloads, prefer passing the password explicitly to each encrypt/decrypt call rather than configuring a defaultPassphrase — that bounds the password's V8-string lifetime to the call frame. See the README "Default Passphrase" / "Password Requirements" sections for the caller-facing version of this guidance.

Reports about additional retention paths (e.g. compiler-emitted register spills, OS-level swap leaking sensitive pages to disk, debugger attachment exfiltrating heap snapshots) are welcome but generally fall under the "side channels" category and are bounded by what the host environment exposes — see "Side channels" in the Scope section above.

Learn more about advisories related to Hiprax/crypto in the GitHub Advisory Database