Skip to content

Repository files navigation

sm-cipher

Dependency-free, byte-oriented SM2 / SM3 / SM4 implementation for TypeScript ESM. Every algorithm input and output is a Uint8Array; string and hexadecimal conversion is explicit at the application boundary.

The implementation provides native bigint SM2, typed-array SM3/HMAC-SM3, SM4 ECB/CBC/GCM, strict DER parsing, SM2 ASN.1 ciphertexts, public-key precomputation, and a standalone GHASH window core. It has no runtime dependencies.

Install

npm install sm-cipher

Quickstart

import {
  C1C3C2,
  generateKeyPair,
  sm2Encrypt,
  sm2Decrypt,
  sm2Sign,
  sm2Verify,
  sm3,
  sm4Encrypt,
  sm4Decrypt,
  bytesToHex,
  bytesToUtf8,
  hexToBytes,
  utf8ToBytes,
} from "sm-cipher"

const message = utf8ToBytes("message")
const pair = generateKeyPair()

// SM2 asymmetric encryption (C1C3C2 is the modern default)
const cipher = sm2Encrypt(message, pair.publicKey, C1C3C2)
const plaintext = sm2Decrypt(cipher, pair.privateKey, C1C3C2)

// SM2 signing
const signature = sm2Sign(message, pair.privateKey, { der: true })
const valid = sm2Verify(message, signature, pair.publicKey, { der: true })

// SM3 hashing
const digest = sm3(message)

// SM4 symmetric encryption (ECB by default)
const key = hexToBytes("0123456789abcdeffedcba9876543210")
const sm4Cipher = sm4Encrypt(message, key)
const sm4Plain = sm4Decrypt(sm4Cipher, key)

console.log(
  bytesToUtf8(plaintext),
  bytesToHex(digest),
  valid,
  bytesToUtf8(sm4Plain)
)

API

SM2

Function Signature Notes
generateKeyPair(seed?) (seed?: Uint8Array) => { privateKey, publicKey } Uses a secure random source unless a seed is supplied.
getPublicKeyFromPrivateKey(privateKey) (Uint8Array) => Uint8Array Derives the uncompressed public key.
compressPublicKey(publicKey) (Uint8Array) => Uint8Array Uncompressed to compressed point form.
comparePublicKey(a, b) (Uint8Array, Uint8Array) => boolean Compressed/uncompressed-safe equality.
verifyPublicKey(publicKey) (Uint8Array) => boolean Validates the point is on the curve.
precomputePublicKey(publicKey, windowSize?) (Uint8Array, number) => PrecomputedPublicKey Speeds up repeated encryption/verification with one key.
sm2Encrypt(message, publicKey, mode?, options?) (Uint8Array, PublicKey, SM2Mode, Sm2CipherOptions) => Uint8Array mode defaults to C1C3C2. { asn1: true } for ASN.1 ciphertext.
sm2Decrypt(cipher, privateKey, mode?, options?) (Uint8Array, Uint8Array, SM2Mode, Sm2CipherOptions) => Uint8Array Mirrors sm2Encrypt.
sm2Sign(message, privateKey, options?) (Uint8Array, Uint8Array, options) => Uint8Array { der: true }, { hash: true }, { userId }, { pointPool }.
sm2Verify(message, signature, publicKey, options?) (Uint8Array, Uint8Array, PublicKey, options) => boolean Mirrors sm2Sign options.
encodeDer(raw) / decodeDer(der) (Uint8Array) => Uint8Array Strict SM2 signature DER codec.
getPoint() () => SignaturePoint Precomputes one (k, x1) signature nonce for a pool.
C1C2C3, C1C3C2, SM2CipherMode constants C1C2C3 = 0, C1C3C2 = 1.

SM3

Function Signature Notes
sm3(input, options?) (Uint8Array, { key, mode? }) => Uint8Array Plain digest, or HMAC-SM3 when options.key is given.
sm3Hmac(key, message) (Uint8Array, Uint8Array) => Uint8Array Direct HMAC-SM3.
kdf(z, length) (Uint8Array, number) => Uint8Array SM2 key derivation function.

SM4

Function Signature Notes
sm4Encrypt(input, key, options?) (Uint8Array, Uint8Array, SM4Options) => Uint8Array | GCMResult mode: 'ecb' (default), 'cbc', 'gcm'. Returns { output, tag } for GCM.
sm4Decrypt(input, key, options?) (Uint8Array, Uint8Array, SM4Options) => Uint8Array Pass tag in options for GCM.
sm4Ghash(h, ...segments) (Uint8Array, ...Uint8Array[]) => Uint8Array Standalone GHASH core; each segment is zero-padded to 16 bytes.

SM4 padding is 'pkcs#7' / 'pkcs#5' / 'none' (default 'pkcs#7'), and CBC/GCM require an iv.

Byte / string helpers

Function Signature
hexToBytes(string) (string) => Uint8Array
bytesToHex(Uint8Array) (Uint8Array) => string
utf8ToBytes(string) (string) => Uint8Array
bytesToUtf8(Uint8Array) (Uint8Array) => string
concatBytes(...Uint8Array[]) (...Uint8Array[]) => Uint8Array
xorBytes(a, b) (Uint8Array, Uint8Array) => Uint8Array
equalBytes(a, b) (Uint8Array, Uint8Array) => boolean
copyBytes(input, name?) (Uint8Array, string) => Uint8Array
setRandomSource(source?) (RandomSource?) => void
randomBytes(length) (number) => Uint8Array

Mini Program Runtimes

Uint8Array is a typed view over ArrayBuffer and is supported by current mini-program runtimes. The implementation does not require TextEncoder, TextDecoder, Buffer, Node APIs, or browser DOM APIs.

Native BigInt is required and cannot be lowered by Vite. The output target is therefore ES2020. Test the minimum mini-program base library and device engines used by the application before release.

Browsers and Node use globalThis.crypto.getRandomValues by default. WeChat exposes secure randomness as the asynchronous wx.getRandomValues API instead, so preload a secure pool and install a synchronous source before generating SM2 keys, encryption nonces, or signatures:

import { setRandomSource } from "sm-cipher"

let pool = new Uint8Array(0)
let offset = 0

export function refillRandomPool(length = 65536): Promise<void> {
  return new Promise((resolve, reject) => {
    wx.getRandomValues({
      length,
      success(result) {
        pool = new Uint8Array(result.randomValues)
        offset = 0
        resolve()
      },
      fail: reject,
    })
  })
}

await refillRandomPool()
setRandomSource((length) => {
  if (offset + length > pool.length)
    throw new Error("Secure random pool exhausted")
  const output = pool.slice(offset, offset + length)
  pool.fill(0, offset, offset + length)
  offset += length
  return output
})

Do not replace the source with Math.random().

Performance

npm run bench builds the package and compares its public API with sm-crypto@0.5.7 and sm-crypto-v2@1.15.1. Each implementation is auto-calibrated to approximately 150 ms per sample, execution order rotates between samples, and the median of 5 samples is reported. SM2 and SM3 results are normalized to hexadecimal output, while SM4 uses byte keys and byte output where supported.

Representative operations per second on Node 25.2.1, Windows x64, Intel Core i5-1135G7 @ 2.40GHz are shown below. Higher is better; results depend on the runtime, hardware, and system load.

Case sm-cipher sm-crypto sm-crypto-v2
SM2 generateKeyPair 2,357 101 2,169
SM2 encrypt 552 49 159
SM2 encrypt (precomputed key) 1,328 N/A 907
SM2 decrypt 670 98 246
SM2 sign 1,277 49 1,060
SM2 sign (supplied public key) 2,148 97 1,807
SM2 verify 575 52 227
SM3, hexadecimal output (4 KiB) 34,890 14,432 30,749
SM4 ECB encrypt (4 KiB) 22,571 9,745 14,885
SM4 ECB decrypt (4 KiB) 21,185 9,561 14,116
SM4 CBC encrypt (4 KiB) 19,350 8,395 12,391
SM4 CBC decrypt (4 KiB) 20,712 9,354 12,818
SM4 GCM encrypt (4 KiB) 9,208 N/A 5,572
SM4 GCM decrypt (4 KiB) 9,637 N/A 5,718

Figures above use 15 samples (BENCH_SAMPLES=15) for tighter medians; SM2/SM3 use hexadecimal output and SM4 uses byte keys/output where supported. SM4 throughput improved after switching the block round function to combined S-box/L-transform lookup tables (T-tables), and SM2 improved after specializing Jacobian point doubling for the curve's a ≡ -3 (mod p) parameter — both reduce the number of field multiplications on the hot path without changing any public API or output.

The SM2 base-point table is initialized lazily. It uses 5-bit signed windows with 53 windows and 17 slots per window. Fresh-process measurements with forced garbage collection produced these median results:

Library Import First keypair Warm keypair First-call heap growth Retained heap estimate
sm-cipher 4.16ms 7.96ms 0.12ms 1,037KiB 215KiB
sm-crypto-v2 35.39ms 56.87ms 0.41ms 3,393KiB 1,315KiB

Heap deltas include lazy compilation and allocator effects, so they are process-level estimates rather than exact object sizes. sm-crypto@0.5.7 does not provide GCM. Small differences should be re-measured on the target device.

Run the benchmark yourself:

npm run bench
npm run bench:throughput
npm run bench:startup

BENCH_TIME_MS controls the target duration per throughput sample (default 150, minimum 50). BENCH_SAMPLES and BENCH_COLD_SAMPLES control throughput and fresh-process sample counts respectively; both default to 5.

Vite Library Build

npm install
npm run build
npm run test:bundle

Vite library mode emits dist/smcrypto.js as an ES2020 ESM library. TypeScript also emits dist/*.d.ts. The package exports field points at the entry files. Building requires a Node.js version supported by Vite 8 (Node 20.19+ or 22.12+); the built library targets ES2020.

Persistent Verification

The test and benchmark files are intentionally retained for future changes.

npm test
npm run typecheck
npm run test:bundle
npm run bench

Run the one-million-iteration SM4 standard vector with:

SMCRYPTO_LONG_TESTS=1 npm test

Tests are split into two files:

  • test/crossvalidation.test.mjs is the cross-library consistency suite. sm-crypto@0.5.7 is treated as the baseline oracle: every SM2/SM3/SM4 result produced by this library is checked bidirectionally against it, and sm-crypto-v2@1.15.1 is triangulated against both this library and the baseline (SM4-GCM, which sm-crypto does not support, is checked bidirectionally against sm-crypto-v2 only). Run it alone with npm run test:cross. Set ORIGINAL_SM_CRYPTO to a local repository entry point to run the baseline checks against a source checkout of sm-crypto instead of the pinned dependency.
  • test/test.mjs covers this library's own behavior in isolation: input validation, edge cases, immutability, and known-answer/OpenSSL oracle checks for SM3/HMAC/SM4/GHASH-GCM. Run it alone with npm run test:unit.

npm test runs both files.

Security Boundary

The default random source is cryptographically secure, tags are compared without early exit, and GCM authenticates ciphertext before decryption. Native JavaScript bigint arithmetic is not guaranteed to be constant-time, so this library is not a side-channel-hardened cryptographic module.

Acknowledgements

  • sm-crypto — the de-facto reference implementation of the Chinese national cryptography standards in JavaScript, used as the baseline oracle for this library's cross-validation test suite.
  • sm-crypto-v2 — a faster, actively maintained fork of sm-crypto, triangulated against sm-crypto and this library in the same test suite, including SM4-GCM coverage.
  • @noble/curves and @noble/hashes — audited, dependency-free elliptic-curve and hash primitives that informed API and implementation choices for this library.

Thanks to their authors and maintainers for the reference implementations and prior art that made verifying this library's correctness possible.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages