mrkeyoor.com_
Thu 06 Aug 02:40 UTC
npmSecurityupdated 06 Aug 2026

node-forge

node-forge is a pure JavaScript implementation of a large slice of the TLS and PKI stack: AES, 3DES and RC2 ciphers, SHA and MD5 digests, HMAC, PBKDF2, RSA and Ed25519, plus ASN.1 parsing and the PKCS standards (5, 7, 8, 10, 12) and X.509 certificates. It runs identically in Node and in a browser because it implements every algorithm itself rather than calling out to OpenSSL or WebCrypto. It has zero runtime dependencies and represents binary data as JavaScript binary strings through its own ByteBuffer type, which is the single biggest source of confusion for people arriving from Node Buffers.

Verdict

Reach for node-forge only for the parts platform crypto does not cover: X.509, CSRs, PKCS#12, and ASN.1. For hashing, symmetric encryption, and signatures, node:crypto or WebCrypto are faster, smaller, and have a much better security history than a JavaScript reimplementation that shipped four HIGH severity fixes in a single 2026 release.

API stability5/5The forge.pki, forge.cipher, forge.md, and forge.util surfaces have been unchanged since 1.0 in 2021, and 1.4.0 was a security and OID release with no breaking API changes.
Docs3/5The README is a single very long page with runnable snippets for nearly every module, which is genuinely useful, but there is no searchable API reference, no guidance on ByteBuffer versus Buffer conversion, and no explanation of which defaults are unsafe.
Maintenance2/5Roughly one release every few years (1.3.1 in 2022, 1.4.0 in March 2026, which was security-driven), 402 open issues untriaged, and whole subsystems such as the TLS, HTTP, and Flash socket layers left in place without upkeep.
Ecosystem4/536.9M weekly downloads, mostly transitive through selfsigned, webpack dev tooling, and cloud SDKs rather than direct use; @types/node-forge covers typing, but few new projects choose it deliberately.

Use it if

  • You need to parse, build, or sign X.509 certificates and CSRs in JavaScript: node:crypto and WebCrypto both stop at keys and give you nothing for certificates
  • You need to read or write PKCS#12 (.p12/.pfx) bundles, PKCS#7 signed messages, or raw ASN.1, which no platform API exposes
  • The same code has to run in a browser and in Node with identical behavior, including in contexts where WebCrypto is unavailable because the page is not on a secure origin
  • You are maintaining existing code that already speaks forge ByteBuffers and switching representations would touch everything
Skip it if

Setup reality

npm install node-forge and require('node-forge') gives you everything; there are no native builds and no dependencies, and prebuilt UMD bundles ship for browsers and CDNs. The pain is the data model. Forge represents bytes as binary strings and its own forge.util.ByteBuffer, so you convert at every boundary: Buffer.from(forgeBytes, 'binary') going out and buf.toString('binary') coming in, and forgetting the 'binary' encoding corrupts data silently rather than throwing. Bundle cost is real at roughly 72 KB gzipped for the full build, so browser projects should build a custom bundle with only the modules they use. Types come from the separate @types/node-forge package. Pin at least 1.4.0: everything below it has known signature forgery bugs.

Patterns

Generate random bytes and convert themrandom-bytes

const forge = require('node-forge')

const bytes = forge.random.getBytesSync(32)   // binary string, not Buffer
const hex = forge.util.bytesToHex(bytes)
const b64 = forge.util.encode64(bytes)
const nodeBuf = Buffer.from(bytes, 'binary')  // 'binary' is mandatory

Everything forge returns is a binary string. Buffer.from(bytes) without 'binary' reinterprets it as UTF-8 and quietly mangles any byte above 0x7F.

AES-GCM authenticated encryptionaes-gcm-encrypt

const key = forge.random.getBytesSync(32)   // 32 bytes = AES-256
const iv = forge.random.getBytesSync(12)

const cipher = forge.cipher.createCipher('AES-GCM', key)
cipher.start({ iv, tagLength: 128 })
cipher.update(forge.util.createBuffer('secret payload', 'utf8'))
cipher.finish()
const ciphertext = cipher.output.getBytes()
const tag = cipher.mode.tag.getBytes()

Store iv and tag next to the ciphertext; neither is secret. Reusing an iv with the same key breaks GCM completely. Key length picks the variant: 16, 24, or 32 bytes.

Decrypt AES-GCM and actually check the resultaes-gcm-decrypt

const decipher = forge.cipher.createDecipher('AES-GCM', key)
decipher.start({ iv, tagLength: 128, tag: forge.util.createBuffer(tag) })
decipher.update(forge.util.createBuffer(ciphertext))

if (!decipher.finish()) throw new Error('authentication failed')
const plaintext = decipher.output.toString()

finish() returns a boolean instead of throwing. Ignore the return value and you accept tampered ciphertext as valid, which is the most common way people misuse this API.

Derive a key from a passwordderive-key-pbkdf2

const salt = forge.random.getBytesSync(16)
const key = forge.pkcs5.pbkdf2('correct horse', salt, 600000, 32,
  forge.md.sha256.create())

// async form keeps the event loop free
forge.pkcs5.pbkdf2('correct horse', salt, 600000, 32,
  forge.md.sha256.create(), (err, derived) => { /* ... */ })

The digest argument is optional and defaults to SHA-1; always pass sha256. The sync form blocks for the whole iteration count, which at realistic iteration counts freezes a browser tab.

SHA-256 digest and HMAChash-and-hmac

const md = forge.md.sha256.create()
md.update('the quick brown fox', 'utf8')
console.log(md.digest().toHex())

const hmac = forge.hmac.create()
hmac.start('sha256', secretKey)
hmac.update('message')
console.log(hmac.digest().toHex())

The 'utf8' second argument to update() matters: without it forge treats the string as raw bytes and any non-ASCII character hashes differently from every other implementation.

Generate an RSA key pair without blockingrsa-keypair

forge.pki.rsa.generateKeyPair({ bits: 2048, workers: -1 }, (err, keypair) => {
  if (err) throw err
  const privatePem = forge.pki.privateKeyToPem(keypair.privateKey)
  const publicPem = forge.pki.publicKeyToPem(keypair.publicKey)
})

The synchronous generateKeyPair({bits: 2048}) can take seconds and stalls the event loop or the UI thread. workers: -1 estimates the core count and uses web workers or the native Node API when available.

Sign and verify with RSArsa-sign-verify

const md = forge.md.sha256.create()
md.update('sign this', 'utf8')
const signature = privateKey.sign(md)          // PKCS#1 v1.5 by default

const check = forge.md.sha256.create()
check.update('sign this', 'utf8')
const ok = publicKey.verify(check.digest().bytes(), signature)

Every README example uses SHA-1; do not copy that. PKCS#1 v1.5 verification here has a history of forgery bugs (CVE-2022-24771, CVE-2026-33894), so prefer PSS via forge.pss.create and stay on 1.4.0 or newer.

Encrypt with RSA-OAEP rather than the defaultrsa-oaep-encrypt

const encrypted = publicKey.encrypt(plaintextBytes, 'RSA-OAEP', {
  md: forge.md.sha256.create(),
})

const decrypted = privateKey.decrypt(encrypted, 'RSA-OAEP', {
  md: forge.md.sha256.create(),
})

encrypt() with no scheme argument silently uses RSAES PKCS#1 v1.5, which is padding-oracle prone. RSA can only encrypt a few hundred bytes, so encrypt a symmetric key, not your data.

Parse a certificate and read its fieldsread-certificate-pem

const cert = forge.pki.certificateFromPem(pem)

console.log(cert.subject.getField('CN').value)
console.log(cert.issuer.attributes.map((a) => `${a.shortName}=${a.value}`).join(','))
console.log(cert.validity.notAfter)

const san = cert.getExtension('subjectAltName')
console.log(san && san.altNames.map((n) => n.value))

getField returns undefined rather than throwing when the attribute is absent, and plenty of real certificates have no CN at all, only SANs.

Create a self-signed certificateself-signed-certificate

const keys = forge.pki.rsa.generateKeyPair(2048)
const cert = forge.pki.createCertificate()

cert.publicKey = keys.publicKey
cert.serialNumber = '01'
cert.validity.notBefore = new Date()
cert.validity.notAfter = new Date(Date.now() + 365 * 24 * 3600 * 1000)

const attrs = [{ name: 'commonName', value: 'example.org' }]
cert.setSubject(attrs)
cert.setIssuer(attrs)
cert.setExtensions([
  { name: 'basicConstraints', cA: false },
  { name: 'keyUsage', digitalSignature: true, keyEncipherment: true },
  { name: 'subjectAltName', altNames: [{ type: 2, value: 'example.org' }] },
])
cert.sign(keys.privateKey, forge.md.sha256.create())

const pem = forge.pki.certificateToPem(cert)

Pass sha256 explicitly: cert.sign(key) alone signs with SHA-1, which every modern client rejects. serialNumber is a hex string and must not start with a high bit, so prefix '00' if it does.

Build a certificate signing requestcreate-csr

const csr = forge.pki.createCertificationRequest()
csr.publicKey = keys.publicKey
csr.setSubject([{ name: 'commonName', value: 'api.example.org' }])
csr.setAttributes([
  {
    name: 'extensionRequest',
    extensions: [
      { name: 'subjectAltName', altNames: [{ type: 2, value: 'api.example.org' }] },
    ],
  },
])
csr.sign(keys.privateKey, forge.md.sha256.create())

const pem = forge.pki.certificationRequestToPem(csr)
console.log(csr.verify())

SAN entries live inside the extensionRequest attribute, not on the subject. A CSR without them gets rejected by most CAs, including ACME servers.

Open a .p12 bundle and pull out the key and certread-pkcs12

const der = forge.util.decode64(p12Base64)
const asn1 = forge.asn1.fromDer(der)
const p12 = forge.pkcs12.pkcs12FromAsn1(asn1, 'password')

const keyBag = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag })[
  forge.pki.oids.pkcs8ShroudedKeyBag
][0]
const certBag = p12.getBags({ bagType: forge.pki.oids.certBag })[
  forge.pki.oids.certBag
][0]

const keyPem = forge.pki.privateKeyToPem(keyBag.key)
const certPem = forge.pki.certificateToPem(certBag.cert)

Files exported by OpenSSL with no password need '' while Apple push certificates often need the password argument omitted entirely; those are two different code paths. If ASN.1 parsing fails, retry with pkcs12FromAsn1(asn1, false, password) for non-strict mode.

Alternatives

PackageRegistryPick it when
pkijsnpmYou need X.509, CMS, and PKCS work but want it layered on WebCrypto instead of a JavaScript reimplementation of every primitive
@peculiar/x509npmCertificate and CSR creation or parsing is all you need, with a modern async API over WebCrypto
josenpmThe actual job is JWT, JWS, JWE, or JWK rather than certificates; jose uses the platform crypto and is far smaller
jsrsasignnpmYou need an all-in-one pure-JS crypto and PKI toolkit and want a second option in the same category