Fix CTR mode keystream reuse with short IV (fixes #508)#537
Open
alanturing881 wants to merge 1 commit into
Open
Fix CTR mode keystream reuse with short IV (fixes #508)#537alanturing881 wants to merge 1 commit into
alanturing881 wants to merge 1 commit into
Conversation
…323) CryptoJS.mode.CTR.processBlock() derives the counter from this._iv.slice(0) without padding it to the cipher's block size. When a caller passes a short IV (e.g. a standard 12-byte/96-bit nonce instead of a full 16-byte counter block, per NIST SP 800-38A's recommended nonce+counter construction), counter[blockSize - 1] is `undefined` after the first block. `(undefined + 1) | 0` evaluates to 0 in JS - coincidentally the same value implicitly used (via bitwise coercion of the missing array slot) as that word during the first block's keystream generation. So blocks 1 and 2 are encrypted with an *identical* keystream before the counter starts incrementing correctly from block 3 onward - a critical two-time-pad-style keystream reuse that lets an attacker recover the XOR of the first two plaintext blocks from their ciphertexts. Already publicly reported and root-caused in brix#508 with a working PoC; this commit provides the fix. CTR-Gladman mode (mode-ctr-gladman.js) was checked and is NOT affected - it increments the counter's low-order word (index 0) first, which always exists regardless of IV length. Fix: zero-pad the counter array up to blockSize immediately after copying the IV, so counter[blockSize - 1] is always a defined 0 (matching the well-known IV-padding workaround from the issue thread) rather than undefined. Verified against the official test/mode-ctr-test.js vectors (full 16-byte IV, unaffected/unchanged) and issue brix#508's own reproduction case (12-byte nonce now produces ciphertext identical to a manually zero-padded 16-byte IV). Co-Authored-By: iaohkut <thb2601@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #508 —
CryptoJS.mode.CTRreuses the same keystream for the firsttwo blocks of ciphertext whenever the supplied IV/nonce is shorter than
the cipher's block size (e.g. a 12-byte/96-bit nonce for AES's 16-byte
block, a very common choice matching typical AEAD-style nonce sizes).
Root Cause
processBlock()copies the counter directly fromiv.slice(0)with nolength check. For a 12-byte IV (a 3-word array for AES),
counter[3](
counter[blockSize - 1]) doesn't exist — reading it isundefined.Block 1's keystream generation implicitly treats that missing word as
0(via JS bitwise coercion inside
encryptBlock). The subsequent increment,counter[blockSize - 1] = (counter[blockSize - 1] + 1) | 0, evaluates(undefined + 1) | 0=NaN | 0=0— i.e. it does not actuallyincrement anything, it just makes the previously-implicit
0explicit.Block 2 then reads that same explicit
0and produces an identicalkeystream to block 1. Increments proceed correctly from block 3 onward.
This is a critical CTR-mode "two-time pad" failure: given
C1 = P1 XOR KSandC2 = P2 XOR KSwith the sameKS, an observer cancompute
C1 XOR C2 = P1 XOR P2with no knowledge of the key, and recovereither plaintext block given any partial knowledge of the other (crib
dragging).
Verified
CryptoJS.mode.CTRGladmanis not affected — it incrementsthe counter's first word (
counter[0]), which always exists regardlessof IV length, rather than the last word.
Fix
Zero-pad the counter array up to
blockSizeimmediately after copying theIV, so
counter[blockSize - 1]is always a defined0rather thanundefinedon the first block:This is exactly the "pad the nonce to 16 bytes" workaround already
independently found by users in #508's comment thread — this PR applies
it inside the library so callers don't need to remember to do it
themselves.
Verification
ciphertext byte-for-byte identical to encrypting with that same
nonce manually zero-padded to 16 bytes (previously, blocks past the
first diverged due to the keystream reuse).
test/mode-ctr-test.js's existing assertions (full 16-byte IV,unrelated to the padding path added here) still pass unchanged.
CryptoJS.mode.CTRGladmanconfirmed unaffected by the same short-IVtest, before and after this change.
Test plan
test/mode-ctr-test.jssuite passesciphertext now matches manually-zero-padded 16-byte IV ciphertext)
CTRGladmanmode is unaffected