mrkeyoor.com_
Thu 06 Aug 23:54 UTC
npmSecurityupdated 06 Aug 2026

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.

Verdict

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.

API stability4/5Nothing has changed since 4.2.0 in October 2023 and nothing ever will, which is stability of a sort. The asterisk is that 4.2.0 itself changed PBKDF2 defaults from SHA-1 with 1 iteration to SHA-256 with 250,000, so default-relying code derives different keys across the 4.1/4.2 line, and 4.0.0 broke environments without native crypto.
Docs3/5The README covers install, the full module list, AES basics, and inline release notes, and a gitbook docs site exists. But nothing warns that string-passphrase AES means single-iteration MD5 key derivation, and the docs predate the discontinuation, so they read as if the project were alive.
Maintenance1/5Officially discontinued in the README, deprecated on npm, last push August 2024, and 278 open issues and PRs with no triage. The repo published a new security advisory in 2026 (weak 3.x PRNG behind real wallet drains) with no patch coming, because none ever will.
Ecosystem4/519.3M weekly downloads, 16.4k stars, @types/crypto-js coverage, and a decade of Stack Overflow answers and cross-language tutorials built on its OpenSSL-compatible formats. That gravity is real and is exactly how a dead library keeps getting into new projects.

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
Skip it if

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__ format

A 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

PackageRegistryPick it when
@noble/hashesnpmYou need hashes, HMAC, or KDFs as a small audited zero-dependency library, including things WebCrypto lacks like blake3 and scrypt
@noble/ciphersnpmYou need authenticated encryption (AES-GCM, ChaCha20-Poly1305) in pure JS from the same audited noble family
libsodium-wrappersnpmYou want a full modern crypto toolkit with safe-by-default constructions (secretbox, sealed boxes) and do not mind the WASM payload
josenpmYour actual job is JWT, JWS, or JWE: it wraps WebCrypto correctly so you never hand-roll token crypto