A AegiFlow
CRITICALCVSS 9.1

GHSA-vh45-f885-3848

sm-crypto: Predictable SM2 key generation in Node.js: default RNG uses Math.random + wall clock

Published
2026-07-24
Modified
2026-07-24
Sources
github-advisory

Summary

## Summary `sm-crypto` (npm package **0.4.0**, the latest release, published 2026-01-20) generates SM2 private keys and signing ephemeral scalars from a single module-wide RNG instance (`src/sm2/utils.js`: `const rng = new SecureRandom()`). `SecureRandom` is jsbn's PRNG, which seeds an **ARC4** stream from `window.crypto.getRandomValues` when available. **In Node.js — sm-crypto's primary runtime — `window` is `undefined`, so the CSPRNG branch is skipped** and the seed pool is instead filled from `Math.random()` (V8 `xorshift128+`, recoverable from a few outputs) plus `new Date().getTime()` (wall clock, attacker-estimable). Node *does* expose Web Crypto as `globalThis.crypto`, but jsbn checks `window.crypto`, not `globalThis.crypto`, so the secure path is never taken. Consequently every SM2 private key produced by the default `sm2.generateKeyPairHex()` and every signing ephemeral scalar is derived from non-cryptographic sources and is **predictable** by an attacker who can observe a few `Math.random()` outputs and estimate the generation time. This is the library's **default** (no-argument) path; no caller-selected parameter or configuration is required to trigger it. It is reproduced end-to-end against the unmodified real npm packages (`[email protected]` + `[email protected]`); the PoC below runs against the real installed package, not a copy. The defect is still present on the latest published version (0.4.0) and is not covered by any existing `JuneAndGreen/sm-crypto` issue (0 afldl issues exist; the most recent issues are unrelated SM3/HKDF/PBKDF2 feature requests). ## Details `[email protected]` `index.js` — RNG pool initialization (fallback taken in Node): ```js if (rng_pool == null) { rng_pool = new Array(); rng_pptr = 0; var t; if (typeof window !== "undefined" && window.crypto) { // >> 8; rng_pool[rng_pptr++] = t & 255; } rng_pptr = 0; rng_seed_time(); // + Date.getTime() } ``` `sm-crypto` `src/sm2/utils.js`: ```js const { SecureRandom } = require('jsbn'); const rng = new SecureRandom(); // single module-wide RNG ... function generateKeyPairHex(a, b, c) { const random = a ? new BigInteger(a, b, c) : new BigInteger(n.bitLength(), rng); // uses rng const d = random.mod(n.subtract(BigInteger.ONE)).add(BigInteger.ONE); // private key ... } ``` The default (no-argument) call path uses `rng`, the jsbn ARC4 instance seeded from `Math.random()` + time. The same `rng` feeds the signing ephemeral scalar during SM2 signing. ## PoC The PoC runs against the real installed npm packages. It pins `Math.random` and `Date` **before** `require('sm-crypto')` so jsbn's seed pool is built from controlled inputs. Three independent fresh Node processes then produce the **same** SM2 private key, proving the key is a pure deterministic function of those non-cryptographic sources. It also prints a probe confirming the fallback branch is taken in Node. ### One-line reproducer ```bash WORK=$(mktemp -d) && cd "$WORK" && npm init -y >/dev/null \ && npm install [email protected] [email protected] >/dev/null \ && export NODE_PATH="$WORK/node_modules" \ && node poc.js probe && node poc.js deterministic && node poc.js deterministic ``` ### `poc.js` ```js /* * PoC for sm-crypto predictable default RNG in Node.js. * * sm-crypto (npm 0.4.0) generates SM2 private keys / ephemeral scalars using * jsbn's SecureRandom. In a browser jsbn seeds ARC4 from window.crypto, but in * Node.js `window` is undefined so the CSPRNG branch is skipped and the pool is * filled from Math.random() plus new Date().getTime(). Both are * non-cryptographic; the time is attacker-estim

Affected packages

EcosystemPackageAffected versionsFixed versions
npmsm-crypto0.5.0

Remediation: Upgrade to 0.5.0 or later.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.