Glossary

AES-CTR

AES-CTR is a mode of operation that turns AES into a stream cipher by encrypting a running counter and XORing the result with the data. CTR stands for counter mode and is defined in NIST SP 800-38A. It needs no padding, so ciphertext is exactly as long as the plaintext, and it provides no integrity check on its own. AES-GCM is built on top of it.

How it works

The encryptor builds a sequence of 16-byte counter blocks T1, T2, T3 and so on. Each one is encrypted with AES to give a keystream block, and the keystream is XORed with the plaintext. Decryption is the same operation. The last block can be partial, since unused keystream bytes are simply dropped. Only the AES encrypt direction is ever used, and blocks do not depend on one another, so work can run in parallel.

NIST SP 800-38A has one hard rule: across all messages encrypted under a key, every counter block must be distinct. Layout is up to you. Common choices:

  • Full 128-bit counter: the initial value is a random or unique 16-byte block and it is incremented by one for each block. This is how most libraries treat the 16-byte value you pass in.
  • Nonce plus counter: RFC 3686 uses a 4-byte nonce, an 8-byte per-packet IV and a 4-byte block counter that starts at 1 and counts up.
  • Key: 128, 192 or 256 bits.

The example uses Python's cryptography package. It checks that the keystream equals AES encryption of consecutive counter values, and that OpenSSL agrees on the output.

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
key = bytes(range(16))
nonce = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff")
e = Cipher(algorithms.AES(key), modes.CTR(nonce)).encryptor()
print(e.update(b"hello").hex())
e = Cipher(algorithms.AES(key), modes.CTR(nonce)).encryptor()
ks = e.update(bytes(32))
ecb = Cipher(algorithms.AES(key), modes.ECB()).encryptor()
print(ks[:16].hex(), ecb.update(nonce).hex())
print(ks[16:].hex(), ecb.update((int.from_bytes(nonce, "big") + 1).to_bytes(16, "big")).hex())
0ec2ab845b
66a7c7e8345231489751de073316adad 66a7c7e8345231489751de073316adad
b281d700b79e3cada4ad73bb6e9c1fea b281d700b79e3cada4ad73bb6e9c1fea

The five-byte message "hello" gives five ciphertext bytes, 0ec2ab845b, and the command openssl enc -aes-128-ctr printed the same value.

Does AES-CTR need padding?

No. AES-CTR needs no padding because it XORs a keystream and works on any length, including a single byte. The ciphertext length equals the plaintext length, which is also a small leak, since observers learn the exact message size. CBC, in comparison, pads to a multiple of 16 bytes.

What happens if a counter value repeats?

The same keystream is applied twice, and an attacker who XORs the two ciphertexts gets the XOR of the two plaintexts. With one key and one counter start, attack at dawn and retreat at six produced 13110013060a5441155444121e16, identical to the XOR of the plaintexts. RFC 3686 calls any reuse of the IV with the same nonce and key catastrophic, and says fresh keys are needed.

Common pitfalls

  • No authentication: flipping a ciphertext bit flips the same plaintext bit. Changing one byte of a ciphertext turned "pay 100 to bob" into "pay 900 to bob" with no error. Add HMAC or use AES-GCM.
  • Reusing a counter range: two messages with the same key and starting counter share a keystream. Give every message a unique nonce.
  • Overlapping counters: a message long enough to run into another message's counter range reuses keystream. Reserve enough counter space per message.
  • Treating the nonce as a secret or as a password: it is public and must only be unique. Secrecy comes from the key.
  • Using a static key across power cycles: RFC 3686 says static keys are inappropriate for AES-CTR, since IV reuse is hard to rule out. Use fresh session keys.

Related terms

  • AES — the block cipher that produces the keystream.
  • AES-GCM — CTR plus GHASH authentication.
  • AES-CBC — the chained mode that needs padding and an IV.
  • HMAC — supplies the integrity check CTR lacks.
  • Salsa20 — a stream cipher that is also built around a counter and a nonce.

See also