crypto-js
crypto-js is a pure JavaScript collection of crypto primitives from the era before browsers had a usable native crypto API: MD5 through SHA-3 hashes, HMAC, PBKDF2, and ciphers including AES, TripleDES, RC4, Rabbit, and (since 4.2.0) Blowfish, plus hex, Base64, and UTF-8 encoders. Everything flows through its own WordArray type instead of typed arrays, and its cipher output is OpenSSL-compatible, which is why so many cross-language tutorials use it. It is officially discontinued: the first section of the README is titled Discontinued, says further development would only wrap the native Crypto module, and tells you to use that instead. npm prints a deprecation notice for every version. It survives on roughly 19 million weekly downloads of legacy code that still depends on its output formats.
Nineteen million weekly downloads of pure inertia behind a library whose own README tells you to stop using it. Keep it strictly to read data it already wrote or to match a legacy OpenSSL format; for everything new, use the native crypto module or the noble libraries.
Use it if
- You maintain existing code that stored crypto-js output (the U2FsdGVkX1 OpenSSL-format ciphertext, PBKDF2-derived keys, its hex digests) and need to keep reading and verifying that data
- You must interoperate with a legacy backend that speaks OpenSSL EVP_BytesToKey passphrase encryption, a format WebCrypto does not implement and crypto-js reproduces exactly
- You need synchronous hashing or HMAC in the browser: crypto.subtle is promise-only, and refactoring a hot synchronous path to async is sometimes the more expensive change
- You need MD5, RC4, or another legacy algorithm for an old protocol checksum; WebCrypto deliberately refuses to ship these and crypto-js has them all
- The project is dead and its own maintainer says so: the README opens with a Discontinued section pointing you to the native crypto module, npm marks every version deprecated, last push was August 2024, and 278 issues and PRs sit open with nobody triaging. No future releases means no future security patches for a security library
- You are writing anything new in Node or a modern browser: node:crypto and WebCrypto cover hashing, HMAC, PBKDF2, and AES-GCM natively, at native speed, with zero bundle cost, while crypto-js adds 22.8 KB gzip of slower pure JS
- You need real encryption: there is no authenticated mode at all (no GCM), AES defaults to CBC with PKCS7 and no MAC, so ciphertext is malleable, and passing a string key silently derives key and IV via single-iteration MD5 EVP_BytesToKey
- Its history of dangerous defaults worries you, and it should: PBKDF2 defaulted to SHA-1 with one iteration until 4.2.0 (CVE-2023-46233), and 3.x WordArray.random() was a Math.random-seeded PRNG whose output space was small enough to brute-force; the repo's 2026 Ill Bloom advisory documents about $5M drained from wallets that used it for recovery phrases
Setup reality
npm install crypto-js itself is painless: zero dependencies, no native builds, runs anywhere including old browsers. The friction is around the edges. Every install logs the deprecation warning, which you will explain to teammates and auditors forever. The package is CommonJS only with no ESM exports field, so tree-shaking is poor and you import submodules like crypto-js/sha256 by path to avoid the full 22.8 KB gzip. TypeScript types live in a separate @types/crypto-js package. Version pinning genuinely matters: 4.2.0 changed the PBKDF2 defaults, so code that relied on defaults derives different keys on either side of that upgrade, and 4.0.0 started requiring a native crypto module for randomness, which broke IE 10 and old React Native environments without a polyfill.
Patterns
AES-encrypt a string with a passphraseaes-encrypt-passphrase
const CryptoJS = require("crypto-js");
const ciphertext = CryptoJS.AES.encrypt("my message", "secret passphrase").toString();
// "U2FsdGVkX1..." base64, OpenSSL Salted__ formatA string key triggers passphrase mode: key and IV are derived with OpenSSL's EVP_BytesToKey using MD5 and a single iteration over a random 8-byte salt. That makes the output OpenSSL-compatible but leaves the passphrase cheap to brute-force. Treat this as obfuscation, not protection for real secrets.
Decrypt passphrase-mode ciphertextaes-decrypt-passphrase
const bytes = CryptoJS.AES.decrypt(ciphertext, "secret passphrase");
const plain = bytes.toString(CryptoJS.enc.Utf8);
if (!plain) throw new Error("wrong passphrase or corrupted ciphertext");A wrong passphrase does not fail cleanly: decrypt returns garbage bytes, and toString(Utf8) either yields an empty string or throws "Malformed UTF-8 data" depending on the garbage. Always check for empty output and wrap toString in a try/catch on untrusted input.
AES-CBC with an explicit key and random IVaes-encrypt-explicit-key
const key = CryptoJS.enc.Hex.parse("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
const iv = CryptoJS.lib.WordArray.random(16);
const enc = CryptoJS.AES.encrypt("my message", key, { iv: iv });
const payload = iv.toString() + ":" + enc.ciphertext.toString(CryptoJS.enc.Base64);Passing a WordArray key skips the weak passphrase KDF; a string key is always treated as a passphrase no matter how long it is. You must ship the IV alongside the ciphertext yourself. Mode is CBC with PKCS7 padding and no authentication tag, so anyone who can flip ciphertext bits can flip plaintext bits.
Decrypt when you stored raw ciphertext and IV separatelyaes-decrypt-explicit-key
const [ivHex, ctB64] = payload.split(":");
const params = CryptoJS.lib.CipherParams.create({
ciphertext: CryptoJS.enc.Base64.parse(ctB64)
});
const plain = CryptoJS.AES.decrypt(params, key, {
iv: CryptoJS.enc.Hex.parse(ivHex)
}).toString(CryptoJS.enc.Utf8);AES.decrypt parses plain strings as the OpenSSL Salted__ format. If you stored bare base64 ciphertext without that header, wrap it in a CipherParams object as shown, or decryption quietly proceeds with the wrong bytes and produces garbage.
Compute a SHA-256 digesthash-sha256
const hashHex = CryptoJS.SHA256("Message").toString();
const hashB64 = CryptoJS.SHA256("Message").toString(CryptoJS.enc.Base64);The result is a WordArray; toString() defaults to lowercase hex. SHA-256 here is a fast general-purpose hash, never a password hash: for passwords you want bcrypt or argon2 on the server, not anything in this library.
Sign an API request with HMAC-SHA256hmac-sign-request
const signature = CryptoJS.HmacSHA256(timestamp + method + path + body, apiSecret)
.toString(CryptoJS.enc.Hex);Request signing is the most defensible surviving use of crypto-js in browsers because it is synchronous, unlike crypto.subtle. Verify signatures server-side with a constant-time comparison (Node's crypto.timingSafeEqual); crypto-js has no constant-time compare and === on hex strings leaks timing.
Derive a key from a password with PBKDF2pbkdf2-derive-key
const salt = CryptoJS.lib.WordArray.random(16);
const key = CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32,
iterations: 250000,
hasher: CryptoJS.algo.SHA256
});Pin every option explicitly. Until 4.2.0 the defaults were SHA-1 with a single iteration, roughly 1,000 times weaker than the 1993 spec (CVE-2023-46233); 4.2.0 changed them to SHA-256 with 250,000 iterations. Code that relied on defaults derives a different key after upgrading, which breaks decryption of existing data with no error message.
Convert between UTF-8, hex, and Base64encoding-conversion
const words = CryptoJS.enc.Utf8.parse("hello"); // string -> WordArray
const b64 = CryptoJS.enc.Base64.stringify(words); // "aGVsbG8="
const hex = CryptoJS.enc.Hex.stringify(words); // "68656c6c6f"
const back = CryptoJS.enc.Base64.parse(b64)
.toString(CryptoJS.enc.Utf8); // "hello"parse goes into a WordArray, stringify comes out; mixing the two directions is the classic beginner error. If encoding conversion is all you need, you do not need this library at all: Buffer, TextEncoder, atob and btoa cover it natively.
Generate a random salt or IVrandom-bytes
const salt = CryptoJS.lib.WordArray.random(16); // 16 bytes
const iv = CryptoJS.lib.WordArray.random(16);On 4.x this calls the platform's native crypto and is fine. On every 3.x release except 3.2.x it was a Math.random-seeded PRNG with an effective search space around 2^39 to 2^47 (advisory GHSA-rg76-677x-56q9); wallets that used it for recovery phrases were brute-forced, with about $5M drained per the repo's 2026 advisory. Never generate secrets on a 3.x version.
Hash data incrementally in chunksprogressive-hashing
const sha = CryptoJS.algo.SHA256.create();
sha.update("chunk one");
sha.update("chunk two");
const digest = sha.finalize().toString();update/finalize lets you feed data as it arrives, but there is no stream integration and everything happens on the main thread in pure JS. For hashing large files in Node, crypto.createHash with a stream pipe is both simpler and much faster.
Hash an ArrayBuffer or file from the browserhash-binary-data
const buf = await file.arrayBuffer();
const words = CryptoJS.lib.WordArray.create(new Uint8Array(buf));
const digest = CryptoJS.SHA256(words).toString();WordArray.create only accepts typed arrays when the lib-typedarrays module is loaded; the full require("crypto-js") bundle includes it, but if you import submodules by path you must also import crypto-js/lib-typedarrays or you get wrong results from the implicit string conversion.
Replace a crypto-js hash with WebCryptomigrate-to-native-crypto
const data = new TextEncoder().encode("Message");
const digest = await crypto.subtle.digest("SHA-256", data);
const hex = [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");This is the README's own recommendation: crypto.subtle exists in all modern browsers and in Node via globalThis.crypto. Output is byte-identical to CryptoJS.SHA256(...).toString(), so hashes and HMACs migrate cleanly; only the passphrase-mode AES format needs a compatibility shim.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @noble/hashes | npm | You need hashes, HMAC, or KDFs as a small audited zero-dependency library, including things WebCrypto lacks like blake3 and scrypt |
| @noble/ciphers | npm | You need authenticated encryption (AES-GCM, ChaCha20-Poly1305) in pure JS from the same audited noble family |
| libsodium-wrappers | npm | You want a full modern crypto toolkit with safe-by-default constructions (secretbox, sealed boxes) and do not mind the WASM payload |
| jose | npm | Your actual job is JWT, JWS, or JWE: it wraps WebCrypto correctly so you never hand-roll token crypto |