sm-cipher
v1.0.3
Published
Dependency-free byte-oriented SM2, SM3 and SM4 TypeScript ESM library
Maintainers
Readme
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-cipherQuickstart
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
[email protected] and [email protected]. 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. [email protected]
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:startupBENCH_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:bundleVite 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 benchRun the one-million-iteration SM4 standard vector with:
SMCRYPTO_LONG_TESTS=1 npm testTests are split into two files:
test/crossvalidation.test.mjsis the cross-library consistency suite.[email protected]is treated as the baseline oracle: every SM2/SM3/SM4 result produced by this library is checked bidirectionally against it, and[email protected]is triangulated against both this library and the baseline (SM4-GCM, whichsm-cryptodoes not support, is checked bidirectionally againstsm-crypto-v2only). Run it alone withnpm run test:cross. SetORIGINAL_SM_CRYPTOto a local repository entry point to run the baseline checks against a source checkout ofsm-cryptoinstead of the pinned dependency.test/test.mjscovers 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 withnpm 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 ofsm-crypto, triangulated againstsm-cryptoand this library in the same test suite, including SM4-GCM coverage.@noble/curvesand@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
