Grover bites AES: why 128 is dead and 256 is your new baseline
Grover's algorithm gives a quadratic speed-up on brute-force key search. AES-128 drops to ~64-bit effective security. AES-256 stays at ~128-bit. The fix is one config line — but every place stores cipher state, you need to find.
TL;DR ¶
A quantum adversary running Grover's algorithm against an n-bit symmetric
cipher needs about 2^(n/2) quantum operations to recover the key. AES-128
falls to ~2⁶⁴ effective work — within reach of a serious adversary in
the 2030s. AES-256 falls to ~2¹²⁸ effective — beyond every projected
budget. Move every AES-128 key to AES-256 now. This is the cheapest PQ
upgrade you will ever do.
1. The math, compressed ¶
Grover gives a square-root speedup on unstructured search. For a key
space of size 2^n:
- Classical brute-force:
2^ntrials,~2^nquantum gates. - Quantum (Grover):
~2^(n/2)iterations, each costing~O(n)reversible gates plus an oracle evaluation.
Concretely, for AES:
| Cipher | Classical effort | Grover effort | Verdict |
|---|---|---|---|
| AES-128 | 2¹²⁸ | 2⁶⁴ | Falls in 2030s |
| AES-192 | 2¹⁹² | 2⁹⁶ | Borderline |
| AES-256 | 2²⁵⁶ | 2¹²⁸ | Safe |
The 2⁶⁴ number is roughly the cost of mining the Bitcoin chain twice. Not trivial — but predictable, parallelizable, and within reach of a state-level adversary by the late 2020s.
NSA's CNSA 2.0 advisory (2022) explicitly drops AES-128 from approved suites for "long-term confidentiality of national security information". The migration target is AES-256-GCM.
2. Why hashes are mostly OK ¶
The same speedup applies to hash preimages. SHA-256 preimage cost drops from 2²⁵⁶ to ~2¹²⁸ under Grover — still safe. Collision attacks (birthday) are harder under quantum: Brassard-Høyer-Tapp gives a cubic-root, so SHA-256 collisions drop from 2¹²⁸ to ~2⁸⁵.
That collision number matters for long-lived commitments — Merkle roots in append-only logs, certificate-transparency entries, deduplicated storage. For those use cases, migrate to SHA-384 or SHA3-384. SHA-256 is fine for short-lived integrity (TLS record MAC, HMAC of session tokens).
3. Migration: every place you wrote AES-128, sed it ¶
Most teams have AES-128 sprinkled across three places:
- TLS cipher suites (server config, load balancer config).
- Application-level envelope encryption (KMS calls, database column encryption, file-at-rest).
- Internal RPC encryption (Wireguard, Tailscale, custom HMAC + AES).
3a. Python (cryptography) ¶
1 # cryptography ≥ 41 2 from cryptography.hazmat.primitives.ciphers.aead import AESGCM 3 import os 4 5 # BEFORE: AES-128 — Grover-vulnerable 6 key128 = AESGCM.generate_key(bit_length=128) 7 aead128 = AESGCM(key128) 8 9 # AFTER: AES-256 — Grover-safe 10 key256 = AESGCM.generate_key(bit_length=256) 11 aead256 = AESGCM(key256) 12 13 nonce = os.urandom(12) 14 ct = aead256.encrypt(nonce, b'plaintext', b'associated-data') 15 pt = aead256.decrypt(nonce, ct, b'associated-data')
3b. Node.js ¶
1 const crypto = require('node:crypto'); 2 3 // BEFORE: aes-128-gcm 4 function encryptOld(key, plaintext) { 5 const iv = crypto.randomBytes(12); 6 const cipher = crypto.createCipheriv('aes-128-gcm', key, iv); 7 const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]); 8 return { iv, tag: cipher.getAuthTag(), ct }; 9 } 10 11 // AFTER: aes-256-gcm (only the algorithm string and key length change) 12 function encryptNew(key, plaintext) { 13 // key must be 32 bytes 14 const iv = crypto.randomBytes(12); 15 const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); 16 const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]); 17 return { iv, tag: cipher.getAuthTag(), ct }; 18 } 19 20 // Generate a fresh 256-bit key 21 const key256 = crypto.randomBytes(32);
3c. PHP (libsodium) ¶
1 <?php 2 // libsodium ships with PHP 7.2+ — built-in, no extension to install. 3 // XChaCha20-Poly1305 is a near-drop-in for AES-256-GCM and is the 4 // quantum-safe symmetric primitive recommended in CNSA 2.0. 5 6 // BEFORE: openssl AES-128-GCM 7 $key128 = random_bytes(16); 8 $iv = random_bytes(12); 9 $tag = ''; 10 $ct128 = openssl_encrypt('hello', 'aes-128-gcm', $key128, OPENSSL_RAW_DATA, $iv, $tag); 11 12 // AFTER: libsodium XChaCha20-Poly1305 (effectively 256-bit, quantum-safe symmetric) 13 $key256 = sodium_crypto_aead_xchacha20poly1305_ietf_keygen(); // 32 bytes 14 $nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES); 15 $ct256 = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt('hello', '', $nonce, $key256);
3d. TLS — Apache / nginx ¶
1 # nginx: drop AES-128, keep AES-256 AEADs only 2 ssl_protocols TLSv1.2 TLSv1.3; 3 ssl_ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"; 4 ssl_prefer_server_ciphers off;
1 # Apache mod_ssl 2 SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 3 SSLCipherSuite TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384 4 SSLHonorCipherOrder off
4. One-shot sweep across a repo ¶
1 # aes256-migrate.sh — flag every AES-128 reference in a tree. 2 # Conservative: never auto-edits, only lists. Manual review required. 3 set -eu 4 5 ROOT="${1:-.}" 6 7 echo "=== AES-128 references in $ROOT ===" 8 grep -rnE 'AES[-_]?128[-_]?(GCM|CBC|CTR|ECB)?|aes-128-(gcm|cbc|ctr|ecb)|aes_128' 9 --include='*.{py,js,ts,php,go,rb,java,kt,rs,c,cpp,sh,yaml,yml,conf,nginx,htaccess}' 10 "$ROOT" 2>/dev/null 11 12 echo 13 echo "=== bit_length=128 generators ===" 14 grep -rnE 'bit_lengths*=s*128|key.*=.*16s*[);]|randomBytes(s*16s*)' 15 --include='*.{py,js,ts,php,go,rb,rs}' "$ROOT" 2>/dev/null 16 17 echo 18 echo "=== TLS configs referencing CBC suites ===" 19 grep -rnE 'AES.*CBC|CBC-SHA' 20 --include='*.{conf,nginx,htaccess,yaml,yml}' "$ROOT" 2>/dev/null
Make it executable and run against your repo: chmod +x aes256-migrate.sh && ./aes256-migrate.sh /srv/app. Triage by file.
5. Cost of waiting ¶
A document encrypted with AES-128 in 2026 will be openable by a state-level adversary with quantum capacity around 2032-2034. If your retention policy says "encrypted for 7 years", the math says you are already late.
The migration to AES-256 has no protocol breakage. No new dependencies. No performance regression worth measuring (AES-NI on Intel/AMD makes 256 nearly indistinguishable from 128 at the throughput you care about). This is the single cheapest post-quantum upgrade in the entire stack.
6. References ¶
- Bernstein, Cost analysis of hash collisions: will quantum computers make SHARCS obsolete? (2009)
- Grassl, Langenberg, Roetteler & Steinwandt, Applying Grover's algorithm to AES: quantum resource estimates (2016)
- NSA CNSA 2.0 (2022, updated 2024) — mandates AES-256-GCM for new builds
- NIST SP 800-131A Rev. 2 — retires AES-128 for "transition" use cases
Migrate symmetric first. It costs nothing. The expensive PQ work (asymmetric, signatures, KEMs) deserves your attention after.
RELATED OPS
Inside the NIST PQC suite: ML-KEM (FIPS 203), ML-DSA (FIPS 204), SLH-DSA (FIPS 205)
Four years after the round-3 finalists were named, the standards are signed. Here is what each one does, what parameter set to pick at each security level, and the smallest-possible call into each one from Python and TypeScript.
Q-day: how analysts are pinning the window between 2029 and 2034
The intelligence forecasts converge on a five-year window. We line up six published estimates side by side, show what each one assumes, and ship a Mosca-theorem calculator so you can derive your own shelf-life-aware migration deadline.
RSA-2048 falls: the 20M noisy-qubit estimate, redrawn
Gidney & Ekerå said ~20 million noisy qubits and ~8 hours. Three years of error-correction progress later, the number is moving — but not the conclusion. Where the 2026 resource estimates land.