Skip to content

Repository files navigation

AES-CTR provides confidentiality, not integrity

CI CodeQL Secret scan License

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 September 2024 review of the SP 800-38 series (IR 8459) recommended "not yet deprecating" these modes — 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. SP 800-38A Appendix D says as much: under CTR "the decryption of any ciphertext block is vulnerable to the introduction of specific bit errors into that ciphertext block if its integrity is not protected". Used alone where an attacker can reach the ciphertext, 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₂).

The rule is not "never use CTR" — it is "authenticate the ciphertext". This repository demonstrates what the missing tag costs, then shows the two correct fixes with runnable code.

If you have read that TLS 1.3 dropped counter mode, it is the other way round: TLS 1.3 dropped unauthenticated modes, and all five of its cipher suites are still a counter-driven keystream with a tag attached — AES-GCM and AES-CCM are both counter mode plus a MAC, and ChaCha20-Poly1305 is the same shape from a different primitive. The site's Where the standards are heading section sets out the evidence, including what NIST has said would have to change before it deprecates these modes.

▶ Open the interactive site → — every attack below runs live in your browser against real AES.

Run the attacks yourself, in the browser

The site turns each weakness into an interactive demonstration you can drive. The cryptography is real AES via the Web Crypto API, executed locally in-process — no servers and no network dependencies. (Verified against NIST SP 800-38A and RFC 3686 test vectors in the automated test suite.)

  • Vector 1 — Precision bit-flipping / privilege escalation — flip ciphertext bits to forge a role=root session token from a role=user account with zero decryption errors and zero corruption of surrounding bytes.
  • Vector 2 — Two-time pad & crib-dragging — encrypt two messages under the same (Key, Nonce) pair; watch the keystream cancel out (C₁ ⊕ C₂ = P₁ ⊕ P₂) and drag natural-language candidate words across the XOR stream to recover plaintexts without the key.
  • Vector 3 — Random-access read/write keystream extraction — submit all-zero plaintext to an edit(ciphertext, offset, new_text) oracle; the server re-encrypts it under the same key and counter and hands back 0x00 ⊕ S = S — the raw keystream — recovering 100% of a confidential document in a single request.
  • Vector 4 — Counter rollover & keystream collisions — wrap a deliberately tiny counter field in software and watch identical counter blocks regenerate identical keystream, the invariant behind multi-time pad vulnerabilities within a single stream. (The simulator demonstrates the invariant; it does not overflow AES's own counter.)
  • The fix — test the same token under AES-GCM (AEAD) and Encrypt-then-MAC (AES-CTR + HMAC-SHA256, under two independent keys): flip a single bit and watch the authentication tag reject the ciphertext before any plaintext or role is trusted.

Both fixes ship as copyable samples on the site:

  • Option A — use an AEAD. AES-GCM or ChaCha20-Poly1305: confidentiality and integrity in one primitive, tag verified before any plaintext is released. Still counter-mode encryption underneath.
  • Option B — keep AES-CTR and add HMAC. Encrypt-then-MAC, the ordering Bellare & Namprempre found "secure from all points of view": two independent keys, the tag covering the counter block and the ciphertext, verified in constant time before decrypting.

AES-CTR has three root causes — stream-cipher malleability (bitwise XOR has zero error spread), keystream determinism (reusing nonces reproduces identical keystreams), and missing authentication (tampered ciphertexts decrypt without error). These drive four distinct attack vectors: precision bit-flipping, two-time pad crib-dragging, random-access edit extraction, and counter rollover collisions.

Structure

  • docs/ — the GitHub Pages site and educational write-up: index.html, styles.css, and theme-aware SVG diagrams/.
  • docs/js/ — the cryptographic and attack implementation: crypto.mjs (AES-CTR, AES-GCM, Encrypt-then-MAC) and attacks.mjs (the four attack vectors), plus ui.mjs which wires them to the interactive page and html.mjs, a tagged template that escapes every interpolated value by default.
  • test/ — a Node test suite (node --test) verifying all attack vectors and defensive controls against the NIST SP 800-38A AES-128-CTR and RFC 3686 test vectors, the escape-by-default HTML construction, the two defensive code samples published on the site, and a guard against unreferenced exports.
  • reviews/ — the review audit trail. LATEST_REVIEW.md is the current review record, overwritten each review so git holds the series; CONTENT_DECISIONS.yml is the durable ledger of why content choices were made, so a later review reaffirms or supersedes them rather than re-arguing them; REVIEW_STATE.json is the pass router's machine-readable input.
  • scripts/ — tooling for auditing this content. review_passes.py decides from repository evidence which review passes are stale; capture_review_state.py freezes the reviewed commit, scope and content fingerprint; verify_content_decisions.py validates the decision register.

Develop

npm install       # install eslint devDependencies (tests require zero external dependencies)
npm test          # node --test — verifies all attack vectors against real AES & NIST vectors
npm run lint      # eslint . — static analysis and linting

# preview the interactive site locally
python3 -m http.server -d docs 8000   # then open http://localhost:8000

Diagrams are regenerated with python3 docs/diagrams/generate_diagrams.py.

Security

Found a vulnerability? Report it privately — see SECURITY.md. Do not open a public issue for security reports.

Disclaimer

For educational and defensive security research. Every demonstration runs entirely in your browser against a self-contained, in-page oracle — no network requests and no third-party systems. Use these techniques only against systems you own or are explicitly authorized to test. See DISCLAIMER.md.

License

Licensed under Apache-2.0 — see LICENSE. Covers the whole repository: code, documentation, and diagrams.

About

Why AES-CTR is unsafe without authentication — interactive demonstrations running real AES in the browser.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages