bcrypt, crypto, crypto-js, node-forge, and sjcl address different layers of security in JavaScript applications. crypto is the built-in Node.js module for low-level cryptographic primitives, while bcrypt is the industry standard specifically for password hashing. crypto-js and sjcl are pure JavaScript libraries designed to bring encryption and hashing capabilities to the browser environment where Node.js modules cannot run. node-forge acts as a comprehensive toolkit offering both TLS/SSL implementation and a wide range of crypto primitives that work in both Node.js and browsers, often serving as a bridge between server and client security logic.
Security in JavaScript is not one-size-fits-all. The ecosystem offers tools ranging from low-level OpenSSL wrappers to high-level, audited pure-JS implementations. Selecting the wrong tool can lead to security vulnerabilities, performance bottlenecks, or code that simply won't run in the browser. Let's break down exactly when to use each of these five packages based on real engineering needs.
Password hashing is fundamentally different from encryption. You need a slow, adaptive algorithm to prevent brute-force attacks. Only one package on this list is designed for this specific job.
bcrypt is the industry standard for password hashing in Node.js. It automatically handles salting and uses a cost factor to control computation time.
// bcrypt: Hashing a password
const bcrypt = require('bcrypt');
const saltRounds = 10;
async function hashPassword(password) {
const hash = await bcrypt.hash(password, saltRounds);
return hash;
}
async function verifyPassword(password, hash) {
const match = await bcrypt.compare(password, hash);
return match; // true or false
}
crypto, crypto-js, node-forge, and sjcl should NOT be used for password storage. Using fast hashes like SHA-256 from these libraries makes your user database vulnerable to rainbow table attacks and GPU cracking.
When working on the backend (Node.js), you have access to the native engine. This is usually the fastest and most secure option for general encryption.
crypto is built into Node.js. It wraps OpenSSL, giving you access to robust algorithms like AES-GCM for encryption and SHA-256 for hashing. It is the default choice for server-side tasks.
// crypto: AES-GCM Encryption in Node.js
const crypto = require('crypto');
function encrypt(text, secretKey) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', secretKey, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return { iv: iv.toString('hex'), encrypted, authTag: authTag.toString('hex') };
}
node-forge also works in Node.js but is written in pure JavaScript. You would only choose it over crypto if you need to share the exact same encryption logic with the browser or require specific PKI features like X.509 certificate parsing that crypto makes difficult.
// node-forge: AES Encryption in Node.js (Pure JS)
const forge = require('node-forge');
function encrypt(text, secretKey) {
const iv = forge.random.getBytesSync(16);
const cipher = forge.cipher.createCipher('AES-GCM', secretKey);
cipher.start({ iv: iv });
cipher.update(forge.util.createBuffer(text, 'utf8'));
cipher.finish();
return {
iv: forge.util.bytesToHex(iv),
encrypted: forge.util.bytesToHex(cipher.output.getBytes()),
authTag: forge.util.bytesToHex(cipher.mode.tag)
};
}
The browser does not have the Node.js crypto module. If you need to encrypt data on the client (e.g., securing local storage or encrypting a payload before sending it to the server), you must use a pure JavaScript library.
crypto-js is a popular, easy-to-use library that brings many algorithms to the browser. It is great for general-purpose encryption where extreme cryptographic auditing is not the primary concern.
// crypto-js: AES Encryption in Browser
const CryptoJS = require('crypto-js');
function encrypt(text, secretKey) {
const encrypted = CryptoJS.AES.encrypt(text, secretKey).toString();
return encrypted;
}
function decrypt(ciphertext, secretKey) {
const bytes = CryptoJS.AES.decrypt(ciphertext, secretKey);
return bytes.toString(CryptoJS.enc.Utf8);
}
sjcl (Stanford JavaScript Crypto Library) takes a more rigorous approach. It was built by security researchers to avoid common implementation mistakes. Choose this for high-stakes applications like secure messaging or wallet management where you need Elliptic Curve Cryptography (ECC) or verified AES implementations.
// sjcl: AES Encryption in Browser
const sjcl = require('sjcl');
function encrypt(text, secretKey) {
const cipher = new sjcl.cipher.aes(sjcl.codec.utf8String.toBits(secretKey));
const encrypted = sjcl.codec.base64.fromBits(
sjcl.mode.cbc.encrypt(cipher, sjcl.codec.utf8String.toBits(text))
);
return encrypted;
}
node-forge shines here as well because the same code you write for Node.js can often run in the browser without changes. This reduces the risk of logic drift between your server and client security implementations.
// node-forge: AES Encryption in Browser (Same API as Node)
// Note: Requires bundling for browser usage
const forge = require('node-forge');
function encrypt(text, secretKey) {
// Implementation identical to the Node.js example above
const iv = forge.random.getBytesSync(16);
const cipher = forge.cipher.createCipher('AES-GCM', secretKey);
cipher.start({ iv: iv });
cipher.update(forge.util.createBuffer(text, 'utf8'));
cipher.finish();
return forge.util.bytesToHex(cipher.output.getBytes());
}
Sometimes you need to do more than just encrypt strings. You might need to generate keys, parse certificates, or handle TLS handshakes manually.
node-forge is the only library in this group that provides a full suite of PKI (Public Key Infrastructure) tools. It can generate RSA key pairs, create Certificate Signing Requests (CSRs), and parse X.509 certificates entirely in JavaScript.
// node-forge: Generating an RSA Key Pair
const forge = require('node-forge');
const pki = forge.pki;
const keys = pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 });
const privateKeyPem = pki.privateKeyToPem(keys.privateKey);
const publicKeyPem = pki.publicKeyToPem(keys.publicKey);
console.log('Private Key:', privateKeyPem);
console.log('Public Key:', publicKeyPem);
crypto can generate keys but lacks the high-level helpers for parsing and manipulating certificate structures easily. sjcl focuses on primitives and does not offer PKI management. crypto-js and bcrypt do not support these features at all.
| Feature | bcrypt | crypto (Node) | crypto-js | node-forge | sjcl |
|---|---|---|---|---|---|
| Environment | Node.js Only | Node.js Only | Browser/Node | Browser/Node | Browser/Node |
| Password Hashing | ✅ Best | ❌ No | ❌ No | ⚠️ Possible | ⚠️ Possible |
| Symmetric Encryption | ❌ No | ✅ Fast (OpenSSL) | ✅ Good | ✅ Good | ✅ Audited |
| PKI / Certificates | ❌ No | ⚠️ Limited | ❌ No | ✅ Full Suite | ❌ No |
| Pure JavaScript | ❌ No (C++) | ❌ No (C++) | ✅ Yes | ✅ Yes | ✅ Yes |
Choosing the right library comes down to where your code runs and what you are protecting.
bcrypt on the server. Never roll your own password hashing with other tools.crypto module. It is faster and maintained by the Node.js core team.crypto-js for simplicity or sjcl for high-security needs. If you need to share logic between server and client, node-forge is your best bridge.node-forge is the undisputed leader for managing PKI assets in JavaScript.Final Thought: Security libraries are foundational. Using crypto-js for passwords or bcrypt in the browser are critical architectural errors. Match the tool to the environment and the specific security primitive you need.
Choose bcrypt exclusively for hashing user passwords in Node.js environments. It implements the bcrypt algorithm with built-in salting and adaptive cost factors, making it resistant to brute-force attacks. Do not use it for general encryption or in the browser due to its reliance on native C++ bindings and synchronous blocking behavior at high cost factors.
Choose the built-in crypto module for server-side Node.js applications requiring high-performance symmetric encryption, signing, or hashing. It wraps OpenSSL, providing access to industry-standard algorithms like AES-GCM and RSA without external dependencies. Avoid this package for frontend code as it is not available in browser environments.
Choose crypto-js when you need a straightforward, drop-in solution for encryption and hashing directly in the browser. It supports a wide variety of algorithms including AES, RSA, and SHA-3 in a pure JavaScript implementation. It is ideal for client-side data protection tasks like encrypting local storage or generating checksums before upload, but avoid it for password hashing.
Choose node-forge if you need a versatile library that runs in both Node.js and browsers with support for advanced protocols like TLS, X.509 certificate parsing, and PKI. It is the best fit for complex security requirements such as generating CSR files, handling digital signatures, or implementing custom secure channels where standard APIs fall short.
Choose sjcl (Stanford JavaScript Crypto Library) for high-security frontend applications requiring audited, robust implementations of elliptic curve cryptography (ECC) and AES. It is designed by security experts to prevent common implementation pitfalls and is suitable for sensitive operations like client-side key derivation or secure messaging where trust in the library's mathematical correctness is paramount.
A library to help you hash passwords.
You can read about bcrypt in Wikipedia as well as in the following article: How To Safely Store A Password
Please verify that the NodeJS version you are using is a stable version; Unstable versions are currently not supported and issues created while using an unstable version will be closed.
If you are on a stable version of NodeJS, please provide a sufficient code snippet or log files for installation issues. The code snippet does not require you to include confidential information. However, it must provide enough information so the problem can be replicable, or it may be closed without an explanation.
Please upgrade to atleast v5.0.0 to avoid security issues mentioned below.
| Node Version | Bcrypt Version |
|---|---|
| 0.4 | <= 0.4 |
| 0.6, 0.8, 0.10 | >= 0.5 |
| 0.11 | >= 0.8 |
| 4 | <= 2.1.0 |
| 8 | >= 1.0.3 < 4.0.0 |
| 10, 11 | >= 3 |
| 12 onwards | >= 3.0.6 |
node-gyp only works with stable/released versions of node. Since the bcrypt module uses node-gyp to build and install, you'll need a stable version of node to use bcrypt. If you do not, you'll likely see an error that starts with:
gyp ERR! stack Error: "pre" versions of node cannot be installed, use the --nodedir flag instead
Per bcrypt implementation, only the first 72 bytes of a string are used. Any extra bytes are ignored when matching passwords. Note that this is not the first 72 characters. It is possible for a string to contain less than 72 characters, while taking up more than 72 bytes (e.g. a UTF-8 encoded string containing emojis). If a string is provided, it will be encoded using UTF-8.
As should be the case with any security tool, anyone using this library should scrutinise it. If you find or suspect an issue with the code, please bring it to the maintainers' attention. We will spend some time ensuring that this library is as secure as possible.
Here is a list of BCrypt-related security issues/concerns that have come up over the years.
< 5.0.0 suffer from bcrypt wrap-around bug and will truncate passwords >= 255 characters leading to severely weakened passwords. Please upgrade at earliest. See this wiki page for more details.< 5.0.0 do not handle NUL characters inside passwords properly leading to all subsequent characters being dropped and thus resulting in severely weakened passwords. Please upgrade at earliest. See this wiki page for more details.This library supports $2a$ and $2b$ prefix bcrypt hashes. $2x$ and $2y$ hashes are specific to bcrypt implementation developed for John the Ripper. In theory, they should be compatible with $2b$ prefix.
Compatibility with hashes generated by other languages is not 100% guaranteed due to difference in character encodings. However, it should not be an issue for most cases.
Hashes generated in earlier version of bcrypt remain 100% supported in v2.x.x and later versions. In most cases, the migration should be a bump in the package.json.
Hashes generated in v2.x.x using the defaults parameters will not work in earlier versions.
node-gypOpenSSL - This is only required to build the bcrypt project if you are using versions <= 0.7.7. Otherwise, we're using the builtin node crypto bindings for seed data (which use the same OpenSSL code paths we were, but don't have the external dependency).npm install bcrypt
Note: OS X users using Xcode 4.3.1 or above may need to run the following command in their terminal prior to installing if errors occur regarding xcodebuild: sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer
Pre-built binaries for various NodeJS versions are made available on a best-effort basis.
Only the current stable and supported LTS releases are actively tested against.
There may be an interval between the release of the module and the availabilty of the compiled modules.
Currently, we have pre-built binaries that support the following platforms:
If you face an error like this:
node-pre-gyp ERR! Tried to download(404): https://github.com/kelektiv/node.bcrypt.js/releases/download/v1.0.2/bcrypt_lib-v1.0.2-node-v48-linux-x64.tar.gz
make sure you have the appropriate dependencies installed and configured for your platform. You can find installation instructions for the dependencies for some common platforms in this page.
const bcrypt = require('bcrypt');
const saltRounds = 10;
const myPlaintextPassword = 's0/\/\P4$$w0rD';
const someOtherPlaintextPassword = 'not_bacon';
Technique 1 (generate a salt and hash on separate function calls):
bcrypt.genSalt(saltRounds, function(err, salt) {
bcrypt.hash(myPlaintextPassword, salt, function(err, hash) {
// Store hash in your password DB.
});
});
Technique 2 (auto-gen a salt and hash):
bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) {
// Store hash in your password DB.
});
Note that both techniques achieve the same end-result.
// Load hash from your password DB.
bcrypt.compare(myPlaintextPassword, hash, function(err, result) {
// result == true
});
bcrypt.compare(someOtherPlaintextPassword, hash, function(err, result) {
// result == false
});
bcrypt uses whatever Promise implementation is available in global.Promise. NodeJS >= 0.12 has a native Promise implementation built in. However, this should work in any Promises/A+ compliant implementation.
Async methods that accept a callback, return a Promise when callback is not specified if Promise support is available.
bcrypt.hash(myPlaintextPassword, saltRounds).then(function(hash) {
// Store hash in your password DB.
});
// Load hash from your password DB.
bcrypt.compare(myPlaintextPassword, hash).then(function(result) {
// result == true
});
bcrypt.compare(someOtherPlaintextPassword, hash).then(function(result) {
// result == false
});
This is also compatible with async/await
async function checkUser(username, password) {
//... fetch user from a db etc.
const match = await bcrypt.compare(password, user.passwordHash);
if(match) {
//login
}
//...
}
import bcrypt from "bcrypt";
// later
await bcrypt.compare(password, hash);
const bcrypt = require('bcrypt');
const saltRounds = 10;
const myPlaintextPassword = 's0/\/\P4$$w0rD';
const someOtherPlaintextPassword = 'not_bacon';
Technique 1 (generate a salt and hash on separate function calls):
const salt = bcrypt.genSaltSync(saltRounds);
const hash = bcrypt.hashSync(myPlaintextPassword, salt);
// Store hash in your password DB.
Technique 2 (auto-gen a salt and hash):
const hash = bcrypt.hashSync(myPlaintextPassword, saltRounds);
// Store hash in your password DB.
As with async, both techniques achieve the same end-result.
// Load hash from your password DB.
bcrypt.compareSync(myPlaintextPassword, hash); // true
bcrypt.compareSync(someOtherPlaintextPassword, hash); // false
We recommend using async API if you use bcrypt on a server. Bcrypt hashing is CPU intensive which will cause the sync APIs to block the event loop and prevent your application from servicing any inbound requests or events. The async version uses a thread pool which does not block the main event loop.
BCrypt.
genSaltSync(rounds, minor)
rounds - [OPTIONAL] - the cost of processing the data. (default - 10)minor - [OPTIONAL] - minor version of bcrypt to use. (default - b)genSalt(rounds, minor, cb)
rounds - [OPTIONAL] - the cost of processing the data. (default - 10)minor - [OPTIONAL] - minor version of bcrypt to use. (default - b)cb - [OPTIONAL] - a callback to be fired once the salt has been generated. uses eio making it asynchronous. If cb is not specified, a Promise is returned if Promise support is available.
err - First parameter to the callback detailing any errors.salt - Second parameter to the callback providing the generated salt.hashSync(data, salt)
data - [REQUIRED] - the data to be encrypted.salt - [REQUIRED] - the salt to be used to hash the password. if specified as a number then a salt will be generated with the specified number of rounds and used (see example under Usage).hash(data, salt, cb)
data - [REQUIRED] - the data to be encrypted.salt - [REQUIRED] - the salt to be used to hash the password. if specified as a number then a salt will be generated with the specified number of rounds and used (see example under Usage).cb - [OPTIONAL] - a callback to be fired once the data has been encrypted. uses eio making it asynchronous. If cb is not specified, a Promise is returned if Promise support is available.
err - First parameter to the callback detailing any errors.encrypted - Second parameter to the callback providing the encrypted form.compareSync(data, encrypted)
data - [REQUIRED] - data to compare.encrypted - [REQUIRED] - data to be compared to.compare(data, encrypted, cb)
data - [REQUIRED] - data to compare.encrypted - [REQUIRED] - data to be compared to.cb - [OPTIONAL] - a callback to be fired once the data has been compared. uses eio making it asynchronous. If cb is not specified, a Promise is returned if Promise support is available.
err - First parameter to the callback detailing any errors.same - Second parameter to the callback providing whether the data and encrypted forms match [true | false].getRounds(encrypted) - return the number of rounds used to encrypt a given hash
encrypted - [REQUIRED] - hash from which the number of rounds used should be extracted.A note about the cost: when you are hashing your data, the module will go through a series of rounds to give you a secure hash. The value you submit is not just the number of rounds the module will go through to hash your data. The module will use the value you enter and go through 2^rounds hashing iterations.
From @garthk, on a 2GHz core you can roughly expect:
rounds=8 : ~40 hashes/sec
rounds=9 : ~20 hashes/sec
rounds=10: ~10 hashes/sec
rounds=11: ~5 hashes/sec
rounds=12: 2-3 hashes/sec
rounds=13: ~1 sec/hash
rounds=14: ~1.5 sec/hash
rounds=15: ~3 sec/hash
rounds=25: ~1 hour/hash
rounds=31: 2-3 days/hash
Because it's come up multiple times in this project and other bcrypt projects, it needs to be said. The bcrypt library is not susceptible to timing attacks. From codahale/bcrypt-ruby#42:
One of the desired properties of a cryptographic hash function is preimage attack resistance, which means there is no shortcut for generating a message which, when hashed, produces a specific digest.
A great thread on this, in much more detail can be found @ codahale/bcrypt-ruby#43
If you're unfamiliar with timing attacks and want to learn more you can find a great writeup @ A Lesson In Timing Attacks
However, timing attacks are real. And the comparison function is not time safe. That means that it may exit the function early in the comparison process. Timing attacks happen because of the above. We don't need to be careful that an attacker will learn anything, and our comparison function provides a comparison of hashes. It is a utility to the overall purpose of the library. If you end up using it for something else, we cannot guarantee the security of the comparator. Keep that in mind as you use the library.
The characters that comprise the resultant hash are ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$.
Resultant hashes will be 60 characters long and they will include the salt among other parameters, as follows:
$[algorithm]$[cost]$[salt][hash]
"$2a$" or "$2b$" indicates BCryptExample:
$2b$10$nOUIs5kJ7naTuTFkBy1veuK0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa
| | | |
| | | hash-value = K0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa
| | |
| | salt = nOUIs5kJ7naTuTFkBy1veu
| |
| cost-factor => 10 = 2^10 rounds
|
hash-algorithm identifier => 2b = BCrypt
If you create a pull request, tests better pass :)
npm install
npm test
The code for this comes from a few sources:
Unless stated elsewhere, file headers or otherwise, the license as stated in the LICENSE file.