@rohsyl/gorypto
v0.2.0
Published
node-forge reimplemented in Go via CGo + N-API
Readme
[!WARNING] Experimental — do not use in production. Generated with AI for experimentation purposes only. Not audited, not maintained, no security guarantees.
gorypto
Drop-in replacement for node-forge — exact same TypeScript API, significantly faster.
Hybrid architecture: Node.js native crypto for symmetric primitives, Go (CGo + N-API) for asymmetric crypto and PKI. Every operation is faster than node-forge.
Requires Node.js ≥ 18.
Performance vs node-forge
Benchmarked on the same machine (1.5 s time-box per operation):
| Operation | node-forge | gorypto | speedup | |-----------|-----------|---------|---------| | SHA-256 1 KB | 62k ops/s | 516k | 8.4× | | SHA-256 64 KB | 1.7k | 25k | 15× | | HMAC-SHA256 1 KB | 73k | 447k | 6.1× | | AES-256-GCM encrypt 1 KB | 5k | 128k | 27× | | AES-256-GCM decrypt 1 KB | 16k | 210k | 13× | | AES-256-CBC encrypt 1 KB | 6.5k | 144k | 22× | | RSA-2048 encrypt | 1.5k | 50k | 34× | | RSA-2048 decrypt | 51 | 2k | 39× | | RSA-2048 sign | 51 | 1.9k | 37× | | PBKDF2-SHA256 10k iters | 27 | 715 | 26× | | Random 32 B | 15k | 1.1M | 75× | | Base64 encode 1 KB | 153k | 2.2M | 14× | | ByteStringBuffer 256 put+get | 222k | 428k | 1.9× |
Run task bench to reproduce.
Architecture
require('gorypto')
│
▼
index.js ← hybrid router
┌─────────────────────────────────────────────┐
│ JS / Node.js native crypto (fast for small │
│ data; no N-API overhead per call) │
│ forge.md → crypto.createHash() │
│ forge.hmac → crypto.createHmac() │
│ forge.cipher → crypto.createCipheriv() │
│ forge.util → pure-JS ByteStringBuffer │
│ forge.pkcs5 → crypto.pbkdf2Sync() │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Go (CGo + N-API) gorypto.node │
│ forge.pki.rsa → crypto/rsa 34-39× │
│ forge.pki.ed25519 → crypto/ed25519 │
│ forge.pki (X.509, CSR, PKCS#8) │
│ forge.pkcs7/12 → go.mozilla.org/pkcs7 │
│ forge.random → crypto/rand 75× │
│ forge.pbe → MD5-KDF 1.3× │
│ forge.asn1, pem, kem, jsbn, tls, ssh … │
└─────────────────────────────────────────────┘Why hybrid? N-API carries ~50 µs overhead per call. For operations that finish in < 5 µs (SHA-256 of 1 KB in Go ≈ 2 µs), bridge overhead dominates. Node.js's native crypto module has zero per-call overhead. For large/slow operations (RSA, large-data cipher, PBKDF2, randomness), Go wins by 26–75×.
Binary string encoding: forge's Bytes = string is latin1 (ISO-8859-1). All binary data crossing the Go/JS boundary uses napi_create_string_latin1 / napi_get_value_string_latin1 to prevent UTF-8 mangling of byte values > 127.
Usage
import forge from 'gorypto'
// or: const forge = require('gorypto')Same API as node-forge. Every example below is a drop-in.
Message digests
const hash = forge.md.sha256.create()
hash.update('hello world', 'utf8')
console.log(hash.digest().toHex())
// 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824HMAC
const hmac = forge.hmac.create()
hmac.start('sha256', 'secret-key')
hmac.update('message')
console.log(hmac.digest().toHex())AES-GCM encryption
const key = forge.random.getBytesSync(32)
const iv = forge.random.getBytesSync(12)
const enc = forge.cipher.createCipher('AES-GCM', key)
enc.start({ iv, tagLength: 128 })
enc.update(forge.util.createBuffer('secret message'))
enc.finish()
const ciphertext = enc.output.getBytes()
const tag = enc.mode.tag.getBytes()
const dec = forge.cipher.createDecipher('AES-GCM', key)
dec.start({ iv, tagLength: 128, tag: forge.util.createBuffer(tag) })
dec.update(forge.util.createBuffer(ciphertext))
const ok = dec.finish() // false if auth tag doesn't match
console.log(dec.output.getBytes()) // 'secret message'RSA key generation, sign/verify
// Async (recommended)
forge.pki.rsa.generateKeyPair({ bits: 2048 }, (err, kp) => {
const pem = forge.pki.privateKeyToPem(kp.privateKey)
const restored = forge.pki.privateKeyFromPem(pem)
const md = forge.md.sha256.create()
md.update('payload', 'utf8')
const signature = kp.privateKey.sign(md) // md can be JS or Go md — both work
kp.publicKey.verify(md.digest().getBytes(), signature)
})X.509 self-signed certificate
const kp = forge.pki.rsa.generateKeyPair(2048)
const cert = forge.pki.createCertificate()
cert.publicKey = kp.publicKey
cert.serialNumber = '01'
cert.validity.notBefore = new Date()
cert.validity.notAfter = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
cert.setSubject([{ name: 'commonName', value: 'example.com' }])
cert.setIssuer([{ name: 'commonName', value: 'example.com' }])
cert.setExtensions([
{ name: 'basicConstraints', cA: true },
{ name: 'keyUsage', keyCertSign: true, digitalSignature: true },
])
cert.sign(kp.privateKey, forge.md.sha256.create())
const pem = forge.pki.certificateToPem(cert)Ed25519
const kp = forge.pki.ed25519.generateKeyPair()
const sig = forge.pki.ed25519.sign({ privateKey: kp.privateKey, message: 'hello', encoding: 'utf8' })
const ok = forge.pki.ed25519.verify({ publicKey: kp.publicKey, signature: sig, message: 'hello', encoding: 'utf8' })PBKDF2
const key = forge.pkcs5.pbkdf2('password', 'salt', 10000, 32, 'sha256')
console.log(forge.util.bytesToHex(key))ByteStringBuffer
const buf = forge.util.createBuffer()
buf.putInt32(0xdeadbeef)
buf.putBytes('hello')
console.log(buf.toHex()) // deadbeef68656c6c6f
console.log(buf.length()) // 9
console.log(buf.getBytes(4)) // reads 4 bytes, advances pointerUtilities
forge.util.encode64(binaryStr) // Base64 encode
forge.util.decode64(b64Str) // Base64 decode
forge.util.hexToBytes('deadbeef') // hex → binary string
forge.util.bytesToHex(binaryStr) // binary string → hex
forge.util.encodeUtf8('Hello 世界') // string → UTF-8 bytes
forge.util.decodeUtf8(utf8Bytes) // UTF-8 bytes → stringAPI compatibility
Exact same TypeScript interface as node-forge. See specs.md for the full API reference and index.d.ts for TypeScript types.
Modules implemented: util, random, prime, jsbn, md, hmac, cipher, rc2, pss, mgf, pem, asn1, pki (RSA + Ed25519 + X.509 + CSR + PKCS#8), kem, pkcs5, pkcs7, pkcs12, pbe, tls (constants + stubs), http, ssh, log.
Build
Prerequisites
- Go ≥ 1.21
- Node.js ≥ 18
- Task (
go install github.com/go-task/task/v3/cmd/task@latest) - GCC / Clang (CGo)
Build
task build # Build gorypto.node
task test # Build + run 129 vitest tests
task bench # Build + run benchmark vs node-forge
task tidy # go mod tidy
task clean # Remove gorypto.node
task rebuild # clean + buildManual build
export NODE_INCLUDE=$(node -e "process.stdout.write(require('path').join(process.execPath,'../../include/node'))")
CGO_CFLAGS="-I$NODE_INCLUDE" go build -buildmode=c-shared -o gorypto.node .First-time setup (symlink for IDE / go vet)
ln -sf "$(node -e "process.stdout.write(require('path').join(process.execPath,'../../include/node'))")" includeProject layout
index.js hybrid entry point (routes to JS or Go per module)
lib/
bsb.js pure-JS ByteStringBuffer
md.js Node.js crypto hash wrapper
hmac.js Node.js crypto HMAC wrapper
cipher.js Node.js crypto cipher wrapper
*.go Go source (N-API bridge + crypto modules)
gorypto.node compiled native addon (gitignored, built by task build)
index.d.ts TypeScript declarations
specs.md full node-forge API specification
tests/ vitest test suite (129 tests)
benchmark/ benchmark runner (gorypto vs node-forge)Distribution
Consumers install via npm. On npm install:
scripts/install.jsdownloads the prebuiltgorypto.nodefor the current platform from the GitLab release.- No Go or gcc required on client machines.
- Set
GORYPTO_BUILD_FROM_SOURCE=1to skip the download and compile locally instead.
CI builds for linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64 are defined in .gitlab-ci.yml. Triggered by pushing a version tag (git tag v0.1.0 && git push --tags).
