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

sm-crypto's default SM2 key generation in Node.js seeds jsbn's ARC4 RNG from Math.random and wall-clock time, making private keys and signing scalars predictable.

- Payout: Research
- Severity: critical
- Program: sm-crypto
- Found by: afldl
- Disclosed: 2026-07-24
- CVE: CVE-2026-73567
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-vh45-f885-3848
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-vh45-f885-3848
- Tags: area:oss domain:oss impact:funds-loss impact:integrity kind:research platform:ghsa sev:critical tech:nodejs technique:default-creds technique:source-review vuln:crypto

**Target:** npm library SM2 key generation / signing RNG (jsbn SecureRandom)
**Root cause:** jsbn's SecureRandom only uses CSPRNG when window.crypto exists; in Node window is undefined so the pool is filled from Math.random() plus Date.getTime(), and sm-crypto uses this single module-wide RNG for private keys and ephemeral scalars.
**Steps:**
1. Install `sm-crypto@0.4.0` and `jsbn@1.1.0` in a temp dir.
2. Before `require('sm-crypto')`, pin `Math.random` to a deterministic LCG and override `Date` to a fixed timestamp.
3. Call `sm2.generateKeyPairHex()` and print `privateKey`.
4. Re-run in a fresh process with the same pinned values; the private key is identical, proving determinism from non-crypto sources.
**How found:** Code review of jsbn's RNG pool init: noticed the CSPRNG branch is gated on `typeof window !== 'undefined' && window.crypto`, which is false in Node despite globalThis.crypto existing, so the Math.random + time fallback is taken; confirmed by pinning entropy sources and reproducing identical keys.
**Exploitation:** Attacker recovers V8 xorshift128+ state from ~4 observed Math.random outputs, estimates generation time, and reproduces the ARC4 seed to derive the SM2 private key; alternatively a predictable ephemeral k leaks the key from a single signature via s = k^-1(e + d·r) mod n.
**Insight:** When auditing crypto libs ported from browser to Node, check whether RNG seeding is gated on `window.crypto` rather than `globalThis.crypto` — the secure branch silently never runs in Node, and pinning Math.random/Date before require proves determinism.

## Proof of concept (copied exactly from the original report)

**PoC script pinning Math.random and Date before require**

```
const MODE = process.argv[2] || 'probe'; // 'probe' | 'deterministic'

if (MODE === 'deterministic') {
  // --- pin entropy sources BEFORE requiring sm-crypto/jsbn ---
  const fixedTime = 1700000000000;
  let s = 0x12345678 >>> 0;
  Math.random = function () {
    // tiny deterministic LCG standing in for the (already non-crypto) Math.random
    s = (Math.imul(s, 1103515245) + 12345) >>> 0;
    return s / 0x100000000;
  };
  const RealDate = globalThis.Date;
  class FixedDate extends RealDate {
    constructor(...a) { super(...(a.length === 0 ? [fixedTime] : a)); }
  }
  FixedDate.now = () => fixedTime;
  globalThis.Date = FixedDate;
}

const sm2 = require('sm-crypto').sm2;
const kp = sm2.generateKeyPairHex();
console.log('PRIVATE=' + kp.privateKey);

if (MODE === 'probe') {
  console.log('--- probe ---');
  console.log('typeof window =', typeof window, '(undefined in Node => jsbn CSPRNG branch skipped)');
  console.log('typeof globalThis.crypto =', typeof globalThis.crypto, '(Node Web Crypto exists but jsbn checks window.crypto, not globalThis.crypto)');
  console.log('Math.random sample =', Math.random());
  console.log('Date.now() =', Date.now(), '(attacker-estimable, mixed into ARC4 seed)');
}
```

**One-line reproducer installing real packages and running PoC**

```
WORK=$(mktemp -d) && cd "$WORK" && npm init -y >/dev/null \
  && npm install sm-crypto@0.4.0 jsbn@1.1.0 >/dev/null \
  && export NODE_PATH="$WORK/node_modules" \
  && node poc.js probe && node poc.js deterministic && node poc.js deterministic
```

