Cryptography / Failure analysis

AES-CTR provides confidentiality, not integrity

Counter (CTR) mode is one of the five confidentiality modes NIST approves in SP 800-38A. It turns a block cipher into a synchronous stream cipher by encrypting a sequence of counter blocks into a keystream and XORing that with the plaintext: C = P βŠ• S. It is not deprecated β€” NIST's 2024 review of the mode series recommended "not yet deprecating" it β€” and AES-GCM's confidentiality is, in NIST's own words, "a variation of the Counter mode of operation".

What CTR does not do is protect integrity, and it was never specified to. Used alone where an attacker can reach the ciphertext, the consequences are mechanical: flipping a ciphertext bit flips exactly the corresponding plaintext bit, and reusing a counter block under one key cancels the keystream outright (C₁ βŠ• Cβ‚‚ = P₁ βŠ• Pβ‚‚). This page demonstrates what the missing authentication costs, then shows the two correct ways to add it β€” neither of which is "stop using CTR".

Every attack below runs live in your browser against real AES via the Web Crypto API β€” locally executed in-process with zero network requests. Source & tests on GitHub β†’

Scope β€” educational / defensive use

These demonstrations exist to help engineers and security researchers detect, prevent, and remediate unauthenticated CTR mode misuse. Each demonstration executes against a self-contained, in-memory oracle. Every vulnerability is paired directly with its cryptographic mitigation. Do not test these techniques against systems you do not own or are not explicitly authorized to evaluate.

The mechanism

NIST defines Counter mode in SP 800-38A Β§6.5. A counter generation function produces a sequence of distinct 128-bit blocks T₁, Tβ‚‚, …, Tβ‚™ (typically constructed as Nonce β€– Counter). Each counter block is encrypted with the block cipher under key K to produce keystream blocks Sα΅’ = E_K(Tα΅’). The ciphertext is produced by XORing plaintext with the keystream: Cα΅’ = Pα΅’ βŠ• Sα΅’. Decryption performs the exact same operation: Pα΅’ = Cα΅’ βŠ• Sα΅’.

Unlike ECB or CBC, CTR mode requires no padding: the keystream is truncated to the exact byte length of the plaintext. While this enables high performance, random access, and parallel processing, it inherits the fundamental vulnerabilities of synchronous stream ciphers when used without an authentication tag.

Comparison of the three counter-mode options on one axis. AES-CTR alone: keystream encryption with nothing authenticating the ciphertext, so it is malleable and a reused nonce cancels the keystream β€” confidentiality only. AES-CTR plus HMAC-SHA256 over the counter block and ciphertext under a second independent key, verified before decrypting: tampering and truncation are rejected before any plaintext is returned β€” authentication you compose yourself. AES-GCM: the same counter-mode keystream plus a GHASH tag over ciphertext and associated data as one primitive, rejecting a 1-bit tamper before decryption completes, provided nonces never repeat.
All three encrypt with the same counter-mode keystream. What separates them is where the authentication tag comes from β€” nowhere, composed by you, or built into the primitive.

What CTR guarantees β€” and what it does not

The failures below are not evidence that CTR is broken or deprecated. They are what happens when a confidentiality mode is asked to do a job it was never specified for. Three facts from the standards set the boundary:

So the rule is not "never use CTR". It is: wherever an attacker can modify the ciphertext, CTR needs an authentication tag computed over that ciphertext β€” supplied by an AEAD, or by a message authentication code (MAC) you compose yourself. The rest of this page shows what the missing tag costs, and then how to add it correctly.

Three root causes, four attack vectors

Every weakness on this page traces back to three root causes in the mode itself. They are not the only way a CTR deployment can fail β€” side-channel and fault attacks target the implementation rather than the mode, and IR 8459 Β§10 surveys those separately β€” but they are what makes CTR-without-a-tag break on its own terms:

  1. Stream-Cipher Malleability: Bitwise XOR has no error diffusion. Modifying ciphertext bit i alters decrypted plaintext bit i without corrupting adjacent bytes.
  2. Keystream Determinism: Under a fixed key, a given counter block T always produces the exact same keystream block S. Reusing a nonce reproduces the keystream.
  3. Absence of Integrity / Authentication: Ciphertexts contain no authentication tag or MAC. Tampered or spliced ciphertexts decrypt cleanly without error.
Taxonomy tree showing how Malleability, Keystream Determinism, and Missing Authentication lead to Precision Bit-Flipping, Two-Time Pad Keystream Reuse, Random-Access Edit Extraction, and Counter Rollover Collisions.
Each vector inherits its primary root cause along a solid edge; a dashed edge marks a contributing one. Missing authentication is what turns malleability from a property into an attack.

Vector 1 β€” Precision bit-flipping / privilege escalation

Because encryption is C = P βŠ• S, decryption calculates P' = C' βŠ• S. If an attacker injects a differential Ξ” into the ciphertext at offset k (C' = C βŠ• Ξ”), the server decrypts P' = (C βŠ• Ξ”) βŠ• S = (P βŠ• S βŠ• Ξ”) βŠ• S = P βŠ• Ξ”. An attacker who knows or predicts a target field (such as role=user or transfer_amount=000100) can flip specific bits to forge role=root or transfer_amount=999999. There is zero avalanche effect and zero decryption error (Cryptopals Set 4 Challenge 26).

Diagram showing the step-by-step math of bit-flipping: C = P XOR S; injecting delta into C directly injects delta into decrypted plaintext P prime, turning role=user into role=root with zero errors.
The attacker never learns S or the key. The delta they inject into the ciphertext survives decryption unchanged, because XOR is its own inverse.
Live in your browser

Precision token bit-flipping playground

The service issues encrypted tokens for user accounts (email=...&uid=1000&role=user). You never see the AES key.

Vector 2 β€” Keystream reuse / two-time pad & crib-dragging

If two distinct messages are encrypted under the same key and initial counter block: C₁ = P₁ βŠ• S and Cβ‚‚ = Pβ‚‚ βŠ• S. XORing both ciphertexts completely removes the keystream: C₁ βŠ• Cβ‚‚ = (P₁ βŠ• S) βŠ• (Pβ‚‚ βŠ• S) = P₁ βŠ• Pβ‚‚. The secret key has zero influence on this result (Cryptopals Set 3 Challenges 19 & 20).

This enables two powerful attacks without ever breaking AES:

Visualizing two-time pad keystream cancellation: C1 XOR C2 yields P1 XOR P2. Dragging a candidate crib across the stream reveals candidate plaintext bytes directly.
Neither recovery path attacks AES. Once the keystream cancels, what remains is a relationship between two plaintexts, and the key is no longer involved.
Live in your browser

Two-time pad keystream cancellation & crib dragging

Recovered Message 2: [Click button above]

Vector 3 β€” Random-access read/write keystream extraction

Many systems expose seekable or editable encrypted stores (such as disk image storage, encrypted databases, or collaborative document APIs) offering an edit(ciphertext, offset, new_text) endpoint. In unauthenticated CTR mode, an attacker calls that endpoint asking for the stored content to be replaced with all-zero plaintext. The server decrypts, overwrites the plaintext with zeros, and re-encrypts under the same key and counter β€” so the ciphertext it hands back is 0x00 βŠ• S = S, the raw keystream itself. XORing this extracted keystream with the original ciphertext instantly recovers 100% of the secret plaintext in a single request (Cryptopals Set 4 Challenge 25).

Three-step diagram of the edit-oracle attack: the attacker holds only C = P XOR S and knows neither the key nor the keystream; they call edit(C, offset 0, new plaintext = all zero bytes); the server decrypts, overwrites the plaintext with zeros and re-encrypts under the same key and counter, so its reply is the raw keystream S; XORing S with the original C returns the whole plaintext in a single request.
The attacker supplies zero plaintext, not zero ciphertext β€” the server's own re-encryption is what emits the keystream.
Live in your browser

Document editor keystream extraction oracle

Original Ciphertext (hex):
Extracted Raw Keystream (hex): [Awaiting extraction]
Recovered Plaintext: [Awaiting extraction]

Vector 4 β€” Counter rollover & keystream collisions

In CTR mode, the counter block is divided into a fixed Nonce and an integer counter field of length L bits. When encrypting long data streams or high-volume network packets without rekeying, the counter reaches 2α΄Έ βˆ’ 1 and rolls over modulo 2α΄Έ. This causes the block cipher to encrypt previously used counter blocks, generating duplicate keystreams within the exact same session or connection. NIST SP 800-38A Β§6.5 requires the counter blocks to be distinct not merely within one message but "across all of the messages that are encrypted under the given key".

The simulator below wraps a deliberately tiny counter field in software so the collision is visible within a handful of blocks. What it proves is the underlying invariant β€” an identical counter block under an identical key always regenerates an identical keystream block β€” not that AES itself overflowed a counter. Deployed protocols meet this limit by sizing the field up front β€” RFC 3686 Β§4 gives the block counter 32 bits, capping one packet at 232 βˆ’ 1 blocks (68,719,476,720 octets) before the counter would repeat under the same key.

Eight sequential counter blocks under a 2-bit counter field. Blocks 1 to 4 use counter values 0 to 3 producing keystream S0 to S3; blocks 5 to 8 wrap back to the same four counter values and regenerate the identical keystream, shown highlighted, making every pair a two-time pad within one stream. Real systems reach this limit through counter field sizing: RFC 3686 section 4 gives the block counter 32 bits, capping one packet at 2^32 minus 1 blocks.
The invariant is what matters: an identical counter block under an identical key always regenerates an identical keystream block.
Live in your browser

Counter rollover & collision simulator

Three quieter failures the missing tag allows

Bit-flipping and keystream reuse are the headline attacks. An unauthenticated stream also fails in three less obvious ways, two of which a tag over the ciphertext closes and one of which it does not:

Truncation and splicing are both closed by a tag computed over the whole ciphertext, which is what Option B's third rule is for. Replay needs separate handling, and NIST says so directly for GCM: SP 800-38D Appendix D notes that GCM "does not inherently prevent an adversary from intercepting the output of an invocation of authenticated encryption and 'replaying' it," and offers two remedies β€” monitor for duplicate IVs presented for decryption, or bind "a sequential message number or a time stamp" into the associated data so a stale message fails on inspection after the tag verifies.

Detecting unauthenticated CTR

Detecting CTR vulnerabilities requires both passive/black-box testing and static code analysis:

The fix β€” authenticate the ciphertext

Two correct options. Both keep counter-mode keystream encryption, and the headline difference is whether the authentication arrives packaged or composed β€” but they are not interchangeable beyond that, and the operating limits below differ between them. Prefer the first unless something forces your hand.

Option A β€” use an AEAD

AES-GCM (SP 800-38D) and ChaCha20-Poly1305 (RFC 8439) provide confidentiality and integrity as a single primitive, and verify the tag before releasing any plaintext. Under GCM you are still running counter-mode encryption β€” the tag is what is new. Associated data (aad) is authenticated but not encrypted, which is where a message number, version tag, or recipient identifier belongs.

// AES-GCM. The 96-bit nonce MUST be unique per key β€” never reuse one.
async function seal(keyBytes, plaintext, aad) {
  const key = await crypto.subtle.importKey("raw", keyBytes, "AES-GCM", false, ["encrypt"]);
  const nonce = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = new Uint8Array(await crypto.subtle.encrypt(
    { name: "AES-GCM", iv: nonce, additionalData: aad }, key, plaintext));
  return { nonce, ciphertext };   // ciphertext already carries the 128-bit tag
}

async function unseal(keyBytes, nonce, ciphertext, aad) {
  const key = await crypto.subtle.importKey("raw", keyBytes, "AES-GCM", false, ["decrypt"]);
  // Throws if the tag fails. No plaintext is returned on failure β€” that is the point.
  return new Uint8Array(await crypto.subtle.decrypt(
    { name: "AES-GCM", iv: nonce, additionalData: aad }, key, ciphertext));
}

Option B β€” keep AES-CTR and add HMAC (Encrypt-then-MAC)

When CTR is fixed by an existing wire format, hardware, or protocol, compose it with a MAC rather than replacing it. Use this specific ordering: Bellare and Namprempre's analysis of generic composition evaluates Encrypt-and-MAC, MAC-then-Encrypt and Encrypt-then-MAC, and concludes that Encrypt-then-MAC "is secure from all points of view, making it a good choice for a standard". That conclusion is conditional: it assumes the encryption scheme is IND-CPA secure β€” indistinguishable under chosen-plaintext attack, meaning an attacker who can have plaintexts of their choosing encrypted still cannot tell which of two messages a ciphertext holds β€” and that the MAC is strongly unforgeable. AES-CTR with distinct counter blocks meets the first; HMAC-SHA256 (FIPS 198-1) meets the second.

Three details carry the security, and each is a routine way to get this wrong:

  1. Use two independent keys. Bellare and Namprempre prove the result over a composed scheme whose key is the encryption key concatenated with the MAC key β€” K_enc β€– K_mac, each produced by its own scheme's key generation β€” so the guarantee says nothing about a single key reused for both operations. Derive the two from one master secret with a key derivation function (KDF) from SP 800-108 Rev. 1, which specifies deriving additional keying material from a secret key using HMAC, CMAC or KMAC.
  2. Authenticate the counter block as well as the ciphertext. A tag over the ciphertext alone leaves the counter block attacker-controlled, which lets it be swapped to replay an old keystream against new ciphertext.
  3. Verify before decrypting. Encrypt-then-MAC is defined as verifying the tag first and decrypting only on success. Compare with a constant-time primitive β€” crypto.subtle.verify here, hmac.compare_digest in Python, hmac.Equal in Go β€” never == on the raw tag bytes.
// AES-CTR + HMAC-SHA256, Encrypt-then-MAC. encKey and macKey are INDEPENDENT.
async function sealEtM(encKey, macKey, plaintext) {
  const counter = crypto.getRandomValues(new Uint8Array(16));
  counter.fill(0, 12);                      // 96-bit random nonce β€– 32-bit counter
  // 96 bits keeps random-nonce collision below 2^-32 out to ~2^32 messages, the
  // same margin as Option A. The 32-bit counter caps one message at 2^32 blocks.
  const k = await crypto.subtle.importKey("raw", encKey, "AES-CTR", false, ["encrypt"]);
  const ciphertext = new Uint8Array(await crypto.subtle.encrypt(
    { name: "AES-CTR", counter, length: 32 }, k, plaintext));

  const mk = await crypto.subtle.importKey(
    "raw", macKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  // The tag covers the counter block AND the ciphertext.
  const tag = new Uint8Array(
    await crypto.subtle.sign("HMAC", mk, concat(counter, ciphertext)));
  return concat(counter, ciphertext, tag);
}

async function openEtM(encKey, macKey, payload) {
  if (payload.length < 16 + 32) throw new Error("payload too short");
  const counter    = payload.slice(0, 16);
  const ciphertext = payload.slice(16, payload.length - 32);
  const tag        = payload.slice(payload.length - 32);

  const mk = await crypto.subtle.importKey(
    "raw", macKey, { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
  // Verify FIRST. Never decrypt input whose tag has not been checked.
  if (!await crypto.subtle.verify("HMAC", mk, tag, concat(counter, ciphertext))) {
    throw new Error("authentication failed");
  }
  const k = await crypto.subtle.importKey("raw", encKey, "AES-CTR", false, ["decrypt"]);
  return new Uint8Array(await crypto.subtle.decrypt(
    { name: "AES-CTR", counter, length: 32 }, k, ciphertext));
}

Both snippets use the same Web Crypto calls the demonstration below runs and test/attacks.test.mjs exercises, with one deliberate difference: the samples show the counter split to use in production β€” a 96-bit nonce plus a 32-bit counter, the shape RFC 3686 uses β€” whereas the in-page demonstrations use the repository's simpler 64/64 helper, which is immaterial there because every demo run generates a fresh key. concat is a byte-array join; the repository's version is in docs/js/crypto.mjs.

Migrating a format that is already deployed

Neither option is a drop-in edit when untagged ciphertext already exists on disk or on the wire. Four things decide whether the migration actually closes the gap:

  1. A tag added later proves less than it looks like. Computing a MAC over ciphertext that has been sitting in a store since before the migration authenticates it from that moment on β€” it says nothing about whether it was already tampered with. Data that needed integrity from the start has to be decrypted and re-sealed, and until it is, treat it as unauthenticated no matter what tag now accompanies it.
  2. Put a version marker inside the authenticated region. A format version that lives outside the tag β€” or outside GCM's associated data β€” is attacker-editable, which lets a downgrade point your reader back at the legacy untagged path. Bind it: in the associated data for Option A, inside the MAC input for Option B.
  3. Read both formats, write only the new one, and set the end date first. For as long as a reader still accepts untagged input, the system's integrity is that of the weaker format, because that is the branch an attacker will aim at. The dual-read window is a migration tool, not a resting state; decide when it closes before you open it.
  4. Derive the MAC key, do not promote the existing one. The encryption key already in service must not become the MAC key β€” that is the single-key mistake above, arrived at by a different route. Derive both from a master secret with an SP 800-108 KDF and rotate on the same schedule.
Live in your browser

The same forgery against all three options

One account, one target field, one XOR delta. The attacker does the identical thing in each case β€” flipping role=user to role=root in the ciphertext. Only what authenticates the ciphertext changes.

Real-world evidence

Case / VulnerabilityWhat happenedVector
KRACK, 2017 (CVE-2017-13077) A flaw in the WPA2 four-way handshake let an attacker within radio range force a client to reinstall an in-use session key, resetting the packet number so that an already-used nonce was repeated. The resulting keystream reuse allowed replay, decryption, and frame forgery against CCMP and GCMP (Vanhoef & Piessens, ACM CCS 2017). Note this is a forced nonce reset, not counter exhaustion. Vector 2
Microsoft Word & Excel RC4, 2005 (no CVE assigned) Word and Excel encrypted documents with RC4 β€” a stream cipher rather than a counter mode, but subject to the identical keystream-reuse failure. Re-saving an edited document reused the same key and salt, so two revisions were encrypted under one keystream; XORing the ciphertexts cancelled it and leaked the plaintexts (Hongjun Wu, IACR ePrint 2005/007). Vector 2
Shadowsocks stream ciphers, 2020 (no CVE assigned) Unauthenticated stream ciphers (AES-CTR, ChaCha20) let an active attacker bit-flip the target address inside a recorded proxy header and replay it, turning the Shadowsocks server into a decryption oracle that delivered plaintext to an attacker-controlled host (Zhiniang Peng, Qihoo 360 Core Security, February 2020). AEAD ciphers were specified in SIP004 (2017, amended by SIP007), and the project now directs users to AEAD rather than stream ciphers. Vector 1
MEGA, 2022 (no CVE assigned) A boundary case worth reading closely: MEGA did authenticate its file chunks, encrypting them with a custom AES-CCM construction (AES-CTR for confidentiality plus a CBC-MAC tag). The unprotected part was the key hierarchy β€” per-file node keys were wrapped with AES-ECB under the master key with no integrity protection, so a malicious server (MEGA itself, or anyone who compromised its infrastructure) could manipulate those key blocks to mount plaintext-recovery and file-framing attacks (Backendal, Haller & Paterson, IACR ePrint 2022/959; IEEE S&P 2023). Authenticating the message is not enough if the key material is malleable. Root cause 3, applied to key material rather than the stream β€” not Vector 1

Operating limits: nonces, counters, tags, and rekeying

Authentication closes the malleability gap. It does not remove the nonce and counter discipline that CTR and GCM both depend on β€” and GCM's failure under nonce reuse is more severe than CTR's, not less.

Nonce uniqueness

Reusing a (Key, Nonce) pair in GCM destroys confidentiality exactly as it does in raw CTR, and additionally allows recovery of the GHASH authentication key, enabling arbitrary forgery (BΓΆck et al., USENIX WOOT 2016). SP 800-38D Β§8 states the requirement probabilistically: the chance that the authenticated encryption function is ever invoked with the same IV and the same key on two or more distinct input sets "shall be no greater than 2-32". For nonces produced by the random bit generator (RBG)-based construction (Β§8.2.2), Β§8.3 caps the total number of encryptions under any one key at 232 β€” that cap, together with a nonce of at least 96 bits, is what makes Β§8's bound hold.

Tag length

SP 800-38D Β§5.2.1.2 permits exactly seven tag lengths: 128, 120, 112, 104 or 96 bits generally, plus 64 and 32 bits only "for certain applications" under Appendix C's constraints. "An implementation shall not support values for t that are different from the seven choices", and "a single, fixed value for t … shall be associated with each key" β€” the tag length is a property of the key, not a per-message choice. Truncating the tag is specifically dangerous in GCM: Appendix C warns that absent its guidelines, a targeted forgery attack may be practical enough "to produce the hash subkey, H, after which the authentication assurance is completely lost." Use 128 bits unless a packet budget genuinely forces otherwise, and then read Appendix C first.

GCM's authenticity assurance also has a data bound: SP 800-38D scopes it to the confidential data "up to about 64 gigabytes per invocation".

Counter exhaustion and rekeying

A counter block is a fixed nonce field plus an integer counter field, and the counter field's width caps how much data one key can encrypt before a counter block repeats. RFC 3686 Β§4 uses a 32-bit block counter, permitting "(232)-1 blocks = 4,294,967,295 blocks = 68,719,476,720 octets" per packet. Widening the nonce shrinks the counter and vice versa; the split is a capacity decision, not a detail.

The 128-bit counter block drawn as a bar allocated between nonce and counter. Production split: 96 bits of nonce and 32 bits of counter. The 96 random nonce bits keep the chance of a repeat below 2 to the minus 32 out to about 2 to the 32 messages; the 32-bit counter allows 2 to the 32 blocks from zero, which is 64 gibibytes in one message, and RFC 3686 starts its counter at 1 so its limit is 2 to the 32 minus 1 blocks, 68,719,476,720 octets. Below it, this repository's demo helper splits 64 and 64, which is too narrow for random nonces at scale but immaterial because every demo run takes a fresh key. Neither split adds integrity β€” the budget governs only how much one key may encrypt before a counter block repeats, and a tag is still required on top.
Bits spent on the nonce are taken from the counter. One side buys headroom against nonce collision, the other buys message size β€” and neither buys integrity.

A workable rekeying discipline:

  1. Fix the split up front from the largest message and the highest message count the key must serve, and record both numbers alongside the format.
  2. Count invocations per key, not per process. SP 800-38D's 232 ceiling is explicitly "a 'global' requirement" across every instance using that key, so a fleet must divide the budget among its nodes rather than each assuming the whole.
  3. Rekey on a threshold below the bound, not on reaching it, so that in-flight work cannot cross the limit while the new key propagates.
  4. Derive the replacement with an SP 800-108 KDF from a master secret, and reset the counter only after the key has actually changed. Resetting a counter under a key still in use is the same failure as reusing a nonce.

Where the standards are heading

"Modern protocols dropped CTR" is a common reading of the last decade, and it inverts what happened. What was dropped is unauthenticated encryption. Counter mode came through that clear-out as the keystream underneath nearly everything that replaced it.

Dated timeline of counter mode's standing in the standards, 2001 to 2025 onward. December 2001, SP 800-38A approves CTR as one of five confidentiality modes. January 2004, RFC 3686 puts AES-CTR in IPsec ESP with a 32-bit block counter. November 2007, SP 800-38D defines GCM as counter mode plus a GHASH tag. August 2018, TLS 1.3 permits AEAD only β€” and all five of its cipher suites are counter mode. April 2023, NIST decides to revise SP 800-38A to add authentication guidance. September 2024, NIST IR 8459 recommends not yet deprecating the mode because no alternative is standardised. 2025 onward, the SP 800-197 accordion series is in development as the named condition for retiring it. Counter mode was never removed; what was removed is using it without a tag.
Read left to right, the mode is approved, absorbed into AEAD constructions, kept when unauthenticated modes were cut, and then explicitly retained pending a replacement. No step on this line deprecates counter mode.

TLS 1.3 removed confidentiality-only modes, not counter mode

RFC 8446 states that its list of symmetric encryption algorithms "has been pruned of all algorithms that are considered legacy. Those that remain are all Authenticated Encryption with Associated Data (AEAD) algorithms." NIST characterises the same change as a decision "to deprecate all modes of operation that only provided confidentiality protection" (IR 8459 Β§4). What that removed was RC4, and CBC composed as MAC-then-Encrypt β€” the constructions behind BEAST and Lucky Thirteen. Every suite left standing is still a counter-driven keystream with authentication attached:

TLS 1.3 cipher suite (RFC 8446 Β§B.4)KeystreamWhat authenticates it
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
AES counter modeGHASH (SP 800-38D)
TLS_AES_128_CCM_SHA256
TLS_AES_128_CCM_8_SHA256
AES counter modeCBC-MAC (SP 800-38C)
TLS_CHACHA20_POLY1305_SHA256ChaCha20 block-counter keystreamPoly1305 (RFC 8439)

CCM is not merely counter-adjacent β€” the name expands to "Counter with Cipher Block Chaining-Message Authentication Code", and SP 800-38C defines it as "combining the techniques of the Counter (CTR) mode and the Cipher Block Chaining-Message Authentication Code (CBC-MAC) algorithm." ChaCha20 has the same shape from a different primitive: RFC 8439 Β§2.4 describes it "successively call[ing] the ChaCha20 block function … with successively increasing block counter parameters," concatenating the results into a keystream and XORing that with the plaintext β€” under a 96-bit nonce and a 32-bit counter, the same split Option B's sample uses.

There was also no raw AES-CTR suite for TLS to remove in the first place. Of the 356 cipher suites in IANA's TLS registry, exactly two name CTR: a pair of GOST suites from RFC 9189, both marked not recommended β€” and both pairing CTR with an OMAC tag rather than shipping it bare. Unauthenticated CTR is a thing applications build for themselves, which is precisely why it keeps reappearing in application code rather than in protocol libraries.

NIST has not deprecated CTR, and has said what would change that

IR 8459 states the premise of this page in one sentence β€” "The OFB and CTR modes in NIST SP 800-38A are not intended to provide any notion of integrity, meaning that they are not secure against adversaries who can modify the ciphertexts" β€” and still recommends keeping the mode: "Consider not yet deprecating the other NIST SP 800-38A modes, as they are widely used in certain applications where a more secure NIST-recommended alternative is not yet available."

Read the reason, not just the verdict. CTR is retained because the replacement does not exist yet, and NIST is building it. It has decided to revise SP 800-38A β€” among the stated goals, clarifying the requirements on counter blocks and providing "guidance on the importance of incorporating authentication, where feasible" β€” and said in the same notice that if a suitable additional technique is approved, "NIST will consider deprecating the modes in SP 800-38A." That technique is in development as a family of cryptographic accordions in a new SP 800-197 series, and SP 800-38D is itself under revision (its second pre-draft comment period closed on 31 July 2026).

So the accurate status, as of this writing, is: approved, explicitly not deprecated, and on a published trajectory where the untagged use of these modes is the part expected to go. That changes nothing about what to build today β€” the fix is the one above β€” but a design premised on unauthenticated CTR staying acceptable indefinitely is betting against a direction NIST has already put in writing.

What to remember

CTR is an approved confidentiality mode that makes no integrity guarantee, and AES-GCM is counter-mode encryption plus a tag β€” so the rule is not to avoid CTR but to authenticate it. Wherever an attacker can reach the ciphertext, use an AEAD, or compose AES-CTR with HMAC as Encrypt-then-MAC under independent keys, authenticating the counter block with the ciphertext and verifying the tag before decrypting. Then keep the nonce, counter and rekeying discipline that both modes still require. Every TLS 1.3 cipher suite is built this way, and NIST's own trajectory points the same direction β€” so authenticating is where the standards are going, not a workaround for a mode on its way out.

Primary references