
A scientific and technical article. The object of this study is the infrastructure module core.js of the CryptoJS library. The subject of this research is data representation, buffering, encodings, prototype architecture, and the source of randomness, which determine the cryptographic correctness of applied JavaScript code.
Annotation
The module core.js doesn’t implement AES, SHA-256, or HMAC as mathematical primitives, but it does define their computational environment: an object model, 32-bit word arrays, byte serialization, stream block processing, and random data generation. Therefore, its errors can compromise the security of a properly chosen primitive: by misinterpreting bytes, repeating the IV/salt, or generating an incompatible cryptographic protocol.
The key cryptanalytic fact when comparing the branches is that in 3.1.8 and 3.1.9-1, the function WordArray.random() relies on Math.random(), while in 4.0.0, it relies on the browser’s native cryptographic generator or Node.js. This replacement does not “strengthen AES”; it removes the predictability of the input secrets/nonces, which is the basis for practical attacks against schemes with a correct but poorly initialized cipher.
Material and methodology
The repository tags 3.1.8, 3.1.9-1, and 4.0.0 are being reviewed brix/crypto-js. The 3.1.x branch represents the original CryptoJS, wrapped in CommonJS modules; 3.3.0 was subsequently reverted to the contents of 3.1.9-1. Version 4.0.0 introduced a change that was intentionally incompatible with some older environments: deprecating the Math.random() CryptoJS API in favor of a native cryptographic API.
The analysis is performed at three levels: semantic (invariants WordArray), protocol (the role of IV, salt, and nonce), and cryptanalytic (what changes with partially predictable randomness). The examples below are intended for auditing and secure migration, not for extracting other people’s keys or bypassing security.
Component Map
Category core.js | Purpose | Critical invariant |
|---|---|---|
UMD-wrapper | Export to CommonJS, AMD, and the browser’s global scope. | The same assembly should receive the expected entropy source in the target environment. |
Base | Pseudo-class model: extend, create, mixIn, clone. | Hasher and buffer states must not be implicitly shared between instances. |
WordArray | Storing bytes as signed 32-bit JavaScript words. | sigBytes Specifies the exact length of the message; extra bits should be removed. |
C.enc | Hex, Latin1 and UTF-8: Bytes ↔ String transitions. | Cryptography is defined over bytes, not over visual text. |
BufferedBlockAlgorithm | Accumulation and issuance of blocks to algorithms. | You cannot prematurely process the tail if a specific mode requires you to hold the block. |
Hasher | A framework for incremental hashing and HMAC helpers. | finalize() destroys the working condition; reuse requires a new copy. |
WordArray.random | Getting bytes for IV, salt and other random values. | For secrets and nonces, a CSPRNG is required, not a statistically “random-like” PRNG. |
Version differentiation
| Tag | Randomness model in core.js | Practical assessment |
|---|---|---|
| 3.1.8 | WordArray.random() builds words from Math.random(). | Do not use to generate cryptographic keys, salts, IVs, or nonces. |
| 3.1.9-1 | Saves the 3.1.x model with Math.random(). | Legacy compatibility does not compensate for the lack of cryptographic entropy. |
| 4.0.0 | Tries to use window.crypto.getRandomValues, global.crypto or require('crypto').randomBytes; throws an exception on failure. | Correct migration direction; old environments without a native API may stop working. |
Important. The lack of fallback in Math.random() 4.0.0 is a security feature. In cryptography, an error-induced failure is preferable to a “silent” transition to insecure randomness: the latter produces ciphertext that is externally indistinguishable from the correct one, but potentially vulnerable.
Cryptanalytic fact
The strength of a symmetric scheme is a property of the combination of “algorithm + mode + key + random parameters + implementation.” If WordArray.random(16) it produces a predictable value, then AES-256 remains AES-256 as a transformation, but the design of “AES-CBC with a predictable IV” or “password encryption with a predictable salt” loses the stated protocol guarantee.
C = E_K(P \oplus IV) \quad \text{(first CBC block)}
If the IV is repeated and the first block of plaintext is the same, the equality holds:
P_1 = P’_1 \land IV = IV’ \Rightarrow C_1 = C’_1.
This doesn’t automatically reveal the entire text, but it does create an observable equality feature and facilitates message matching. A historical analog is WEP: RC4’s weaknesses, combined with IV reuse and predictable first bytes of frames, turned a large collection of intercepts into material for statistically reconstructing the keystream. The lesson for CryptoJS: the quality of the “wrapping” around a primitive is as important as the choice of the primitive itself.
K = \operatorname{PBKDF2}(\mathrm{password},\mathrm{salt},c,dkLen).
The salt in PBKDF2 doesn’t have to be secret, but it must be unique and unpredictable when a record is created. Repeating the salt allows for reuse of precomputations against identical passwords; predictability makes it easier for an attacker to prepare in advance. Modern systems require a unique salt, a sufficiently high cost c , and preferably a memory-intensive KDF when available.
WordArrays
WordArray Stores the message as well as words the number of significant bytes sigBytes. The byte with the index i is extracted from the word words[i >>> 2] with a shift 24 - 8(i mod 4), that is, the big-endian representation of bytes within a 32-bit word is used.
b_i = (w_{\lfloor i/4 \rfloor} \mathbin{\mathtt{>>>}} (24 – 8(i \bmod 4))) \mathbin{\&} 255.
var x = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6);
console.log(x.toString(CryptoJS.enc.Hex)); // 000102030405
A cryptanalytic example. The length error here is not cosmetic. If the buffer contains six significant bytes, but the developer mistakenly transmits eight, the hash, MAC, or encryption will contain two trailing bytes. In digital forensics and the verification of signed artifacts, such serialization ambiguity leads to two parties hashing different byte sequences, even though the interface displays the “same” object.
Clamp operation
The method clamp() zeros out the insignificant bits of the last word and truncates the array. Its invariant is that after normalization, the representation contains no bytes beyond the boundary sigBytes.
w_{\lfloor s/4 \rfloor} \leftarrow w_{\lfloor s/4 \rfloor} \mathbin{\&} (2^{32} – 1) \ll (32 – 8(s \bmod 4)), \quad s=\mathrm{sigBytes}.
var a = CryptoJS.enc.Hex.parse('a1b2c3');
a.clamp();
console.log(a.sigBytes); // 3
A historical parallel. The class of “length-extension” attacks on Merkle–Damgård hashes demonstrates that the length and boundaries of blocks are part of the cryptographic message. While clamp() it doesn’t eliminate the length-extension attack itself, properly accounting for the length prevents another class of implementation errors: the addition of residual data to the stream being authenticated or hashed.
Encodings and boundaries
Hex.stringify/parse Provides transparent byte mapping, Latin1 uses 0–255 values as character codes, and Utf8 converts the JavaScript string using encodeURIComponent/ decodeURIComponent. The input string is not “already bytes”: the same visible string can have different Unicode normalizations and, therefore, different UTF-8 sequences.
var m = CryptoJS.enc.Utf8.parse('Привет');
var h = CryptoJS.SHA256(m).toString(CryptoJS.enc.Hex);
// Для межсистемного протокола фиксируйте UTF-8 и правила нормализации явно.
A cryptanalytic example. Protocol attacks often exploit canonicalization mismatches: the client signs one set of bytes, and the server interprets another. Similar flaws have been observed in systems where URLs, JSON, Unicode, or line breaks are normalized differently before and after MAC/signature verification. Therefore, for binary keys, IVs, salts, and ciphertext, use WordArray, Uint8Array, Base64, or Hex, rather than implicit text strings.
Block buffering
BufferedBlockAlgorithm Implements a streaming scheme: _append() it converts strings to UTF-8 and adds them to a buffer, then _process(doFlush) passes the completed blocks to a specific algorithm via _doProcessBlock. The block size is specified in 32-bit words; the basic one Hasher is 16 words, or 64 bytes.
n_{ready}=\begin{cases}\lceil |D|/B \rceil,&\text{with flush};\\\max(\lfloor |D|/B \rfloor-m,0),&\text{normal mode},\end{cases}
where |D| is the number of bytes in the buffer, B is the block size in bytes, m and is _minBufferSize. The last parameter is important for block modes and finalization, when the final block cannot be irreversibly processed without adding padding or without authenticity checking.
var hasher = CryptoJS.algo.SHA256.create();
hasher.update('audit record: ');
hasher.update('chunk-2');
var digest = hasher.finalize().toString();
A practical example: In log archives and network dumps, data arrives in fragments of arbitrary size. An incremental hash must yield the same digest as hashing the entire concatenation; otherwise, a “stream split dependency” error occurs, which is dangerous for backup integrity monitoring and evidentiary storage of digital artifacts.
Hasher and HMAC
Hasher defines the life cycle of init → reset → update* → finalize; specific SHA modules implement _doReset, _doProcessBlock and _doFinalize. The source code comment correctly emphasizes: finalize() is a destructive “read-once” operation, so the object after it should not be treated as a continuation of the same computation.
\operatorname{HMAC}_H(K,M)=H((K’\oplus opad)\parallel H((K’\oplus ipad)\parallel M)).
var mac = CryptoJS.HmacSHA256(
CryptoJS.enc.Utf8.parse('amount=1000¤cy=RUB'),
CryptoJS.enc.Hex.parse('00112233445566778899aabbccddeeff')
).toString();
A cryptanalytic example. HMAC cannot be replaced with the construction SHA256(key + message): for Merkle–Damgård hashes, this construction can be vulnerable to length-extension with a known digest and a guessable key length. HMAC is designed precisely to avoid this error and should be used for message authentication unless the protocol uses modern AEAD mode.
Randomness in versions
In 4.0.0, the function cryptoSecureRandomInt() selects the first available native tool: crypto.getRandomValues(new Uint32Array(1))[0] in the browser or crypto.randomBytes(4).readInt32LE() in Node.js. It then WordArray.random(nBytes) assembles 32-bit words until the required length is reached and stores the exact length in sigBytes.
// CryptoJS 4.0.0: API библиотеки, но энтропия должна прийти из native crypto.
var iv = CryptoJS.lib.WordArray.random(16);
var salt = CryptoJS.lib.WordArray.random(16);
Historical context. Predictable randomness generators have already led to catastrophic consequences: a well-known case in Debian OpenSSL (2006–2008) reduced the key generation space to a practically brute-forceable limit; a separate category are the Bitcoin Android signature vulnerabilities, where insufficient nonce randomness in ECDSA allowed private keys to be recovered. These incidents relate to different codebases, but illustrate a general principle: a cryptographic algorithm does not compensate for a lack of input entropy.
\text{For ECDSA:}\quad s \equiv k^{-1}(z+rd) \pmod n.\quad \text{Repetition or predictability of }k\text{ compromises }d.
Audit conclusion. Any call WordArray.random() in a CryptoJS 3.1.8/3.1.9-1 application should be considered a risk indicator if the result is used as a key, salt, IV, nonce, password reset token, or session identifier. Even if the value is not secret (e.g., CBC IV), predictability or repeatability may compromise the semantic security of the mode.
Analysis of fragment 4.0.0
var crypto;
if (typeof window !== 'undefined' && window.crypto) {
crypto = window.crypto;
}
if (!crypto && typeof window !== 'undefined' && window.msCrypto) {
crypto = window.msCrypto;
}
if (!crypto && typeof global !== 'undefined' && global.crypto) {
crypto = global.crypto;
}
if (!crypto && typeof require === 'function') {
try { crypto = require('crypto'); } catch (err) {}
}
This fragment is an environment adaptation layer, not a native DRBG. Its security is inherited from the browser’s Web Crypto API or the Node.js crypto module. A modern audit should separately verify that the builder hasn’t replaced the Node module with a browser polyfill and that the target environment actually provides a system CSPRNG.
if (typeof crypto.getRandomValues === 'function') {
return crypto.getRandomValues(new Uint32Array(1))[0];
}
if (typeof crypto.randomBytes === 'function') {
return crypto.randomBytes(4).readInt32LE();
}
throw new Error('Native crypto module could not be used ...');
An example of a migration incident: An old web application targeting IE 10 or a non-standard WebView may fail to generate an IV with an exception after migrating to 4.0.0. This is operationally inconvenient, but safer than generating an IV via Math.random(). The correct solution is to upgrade the platform or explicitly provide a verified CSPRNG; don’t “fix” compatibility by returning an unsafe fallback.
Limitations of CryptoJS
The repository is marked as deprecated: further active development of CryptoJS has ceased, and the authors recommend the native Crypto module for modern browsers and Node.js. Therefore, for new systems, the Web Crypto API ( crypto.subtle, crypto.getRandomValues) in the browser and the module node:crypto in Node.js are preferred; CryptoJS is primarily justified for legacy compatibility and strictly limited auditable code.
In addition to the randomness generator, the audit should identify: password encryption instead of explicit KDF, CBC without authentication, ECB, nonce repetition in CTR/GCM, outdated MD5/SHA-1 for security purposes, string keys without a clear encoding specification, and a lack of error checking during decryption. Unauthenticated encryption does not ensure integrity: an attacker can modify the ciphertext, and the application can process the corrupted result.
Recommendations to the auditor
- Inventory all CryptoJS dependencies and capture the exact tags, lock files, and delivery hashes.
- Immediately remove
WordArray.random()from versions 3.1.8 and 3.1.9-1 security-critical data; migrate at least to an environment with a native CSPRNG. - Pass keys, IV, salt, and ciphertext as binary values or explicitly Hex/Base64 encoded, committing to UTF-8 and canonicalization of the text.
- Use AEAD (AES-GCM or ChaCha20-Poly1305 via modern native APIs), not “AES-CBC alone plus a homemade MAC”.
- For passwords, use a unique salt and a modern KDF with parameters stored alongside the result; conduct cost load testing on the target infrastructure.
- Test stream processing: the digest for a single buffer should match the digest for all splits of the same bytes.
- Treat
finalize()as a terminal operation of the instance and create a new object for the new message.
Final Provision
The evolution core.js from 3.1.8/3.1.9-1 to 4.0.0 is a prime example of how cryptographic security is determined not by the algorithm’s name, but by the entire implementation chain. The transition from Math.random() a system-wide CSPRNG to a system-wide CSPRNG eliminates the fundamental flaw in the entropy source, but does not eliminate the responsibility for properly selecting the mode, managing nonces/IVs, serializing bytes, and authenticating ciphertext.
Sources
CryptoJS core.js source code: tags 3.1.8, 3.1.9-1 and 4.0.0: https://cryptrade.ru/cryptojs-core-js-source-code-tags-3-1-8-3-1-9-1-and-4-0-0/
- CryptoJS repository and release notes: https://github.com/brix/crypto-js .
- CryptoJS
core.js, tag 4.0.0: https://github.com/brix/crypto-js/blob/4.0.0/core.js . - NIST SP 800-38A, Recommendation for Block Cipher Modes of Operation.
- NIST SP 800-132, Recommendation for Password-Based Key Derivation.
- Bellare, Canetti, Krawczyk. Keying Hash Functions for Message Authentication (HMAC).
