Skip to main content
Cipher Visualizers home
Modern Block Cipher Intermediate

AES Modes of Operation: CTR, CFB, OFB, and XTS

AES only encrypts one 16-byte block. A mode of operation is the wrapper that turns that single block into something that protects a whole message or an entire disk. This guide walks through four modes, CTR, CFB, OFB, and XTS, with verified NIST numbers you can check yourself.

Pashalis Laoutaris 14 min read

Try it

Interactive AES Modes of Operation

🔐 AES Modes of Operation

Enter text and click a button to start!
key
IV
tweak key
sector #
Pick a mode and click Encrypt to watch AES turn one block into a whole message.

AES has one job. It takes a 16-byte block and a key, and it scrambles that single block into another 16 bytes. That is the whole cipher. Real data is almost never exactly 16 bytes. A message is longer, a file is much longer, and a disk is enormous. A mode of operation is the wrapper that turns the one-block cipher into something that protects data of any length. This guide covers four of them: CTR, CFB, OFB, and XTS. Every number below is a real NIST or library-checked value you can reproduce.

Table of Contents

  1. Why a Block Cipher Needs a Mode
  2. CTR: Counter Mode
  3. OFB: Output Feedback
  4. CFB: Cipher Feedback
  5. XTS: Encrypting a Disk
  6. A Verified Example
  7. Python Implementation
  8. For Fun: The Same Thing in ~20 Lines
  9. Choosing a Mode
  10. Security Considerations
  11. Real-World Applications
  12. Frequently Asked Questions
  13. References and Further Reading

Why a Block Cipher Needs a Mode

The naive answer is to chop the data into 16-byte blocks and encrypt each one on its own. That approach has a name, Electronic Codebook, or ECB, and it is broken. The problem is that ECB is deterministic per block. Two identical plaintext blocks always produce two identical ciphertext blocks. Patterns in the data survive encryption, which is why the famous “ECB penguin” image is still clearly a penguin after encryption.

Every good mode fixes this by making each block’s encryption depend on something that changes. That something is a counter, a feedback value, or a position-dependent tweak. The other job a mode does is handle length. A message of 1000 bytes is not a multiple of 16, so a mode either pads it or borrows a stream-cipher trick to avoid padding entirely.

Three of the four modes here, CTR, OFB, and CFB, take the stream-cipher route. They use AES to generate a pseudorandom keystream and then XOR that keystream with the plaintext. This has a striking consequence. None of them ever calls AES decryption, not even to decrypt. XOR is its own inverse, so running the exact same keystream against the ciphertext gives the plaintext back. The fourth mode, XTS, is different. It is built for disks, and we cover it on its own terms below.

CTR: Counter Mode

Counter mode is the cleanest way to turn a block cipher into a stream cipher. You pick a starting value, usually a nonce joined to a counter, and you encrypt it with AES. That encrypted counter is your first 16 bytes of keystream. Then you add one to the counter, encrypt again, and get the next 16 bytes. You keep going until you have as much keystream as you have plaintext.

The keystream for block i is E(key, counter + i). Notice what the plaintext is doing here: nothing at all. The keystream depends only on the key and the counter, never on the data. That gives CTR two famous properties. It is fully parallelizable, because block 5’s keystream does not wait for block 4. It also supports random access, so you can decrypt the middle of a huge file by computing just the counters you need.

CTR is the engine inside the most widely used authenticated mode, AES-GCM. It is fast, simple, and friendly to hardware. It carries one strict rule, which the security section revisits: never encrypt two different messages with the same key and the same counter value.

OFB: Output Feedback

Output Feedback is CTR’s older cousin. Instead of counting, it feeds the cipher’s own output back into its input. You start with an IV and encrypt it. That output is the first keystream block. Then you encrypt that output to get the second keystream block, and encrypt the result again for the third, and so on.

The keystream is a chain: O1 = E(IV), then O2 = E(O1), then O3 = E(O2). Each block is the AES encryption of the one before it. Like CTR, the keystream never touches the plaintext, so OFB is also a pure stream cipher that only uses AES encryption in both directions.

OFB lost the popularity contest to CTR for one reason. Because each keystream block depends on the previous one, you cannot jump ahead or parallelize the keystream generation. You can precompute the whole keystream before you have the data, which is occasionally useful, but CTR gives you the same benefit plus random access. OFB is mostly of historical and educational interest today.

CFB: Cipher Feedback

Cipher Feedback feeds the ciphertext back, not the cipher’s raw output. To encrypt a block, you encrypt the previous ciphertext block with AES and XOR the result into the current plaintext. The very first block uses the IV in place of a previous ciphertext. From then on, each ciphertext block becomes the input that generates the next block’s keystream.

This feedback of real ciphertext makes CFB self-synchronizing. If a ciphertext block is damaged in transit, the error affects that block and the next one, and then the stream recovers on its own. That property mattered for noisy communication lines, which is where CFB earned its keep.

There is a neat detail worth seeing. For the very first block, CFB and OFB are identical, because both XOR the plaintext with E(IV). They only diverge at block two, when CFB switches to feeding back ciphertext and OFB keeps feeding back its own output. The verified example below shows this exact split: the first ciphertext block matches between the two modes, and the second block differs.

XTS: Encrypting a Disk

Disk encryption breaks the assumptions the other modes rely on. A disk has no natural order of writes, so you cannot chain blocks the way CFB does. Sectors are rewritten in place constantly, so you cannot store a fresh IV next to every sector. You also need the same plaintext written to two different sectors to look different, otherwise an attacker learns which sectors share content. XTS-AES, standardized in IEEE 1619 and NIST SP 800-38E, is the answer the storage industry settled on.

XTS uses two AES keys. The first key encrypts data. The second key encrypts a tweak, which is just the sector number turned into 16 bytes. The encrypted tweak is then multiplied in the finite field GF(2^128) by a value called alpha, once per block, to give each block within the sector its own distinct tweak. The core operation is a sandwich: XOR the tweak into the plaintext, run AES, then XOR the tweak back out. Cryptographers call this an XEX construction, for XOR-Encrypt-XOR.

Because the tweak is derived purely from position, XTS needs no IV and no chaining. Any sector decrypts independently, which is exactly what a disk needs. When a sector length is not a multiple of 16, XTS uses ciphertext stealing to avoid padding, borrowing bits from the second-to-last block so the ciphertext stays the same length as the plaintext. One honest warning: XTS provides confidentiality only. It does not authenticate, so it cannot detect tampering, only make the tampered result unpredictable.

A Verified Example

Every value here comes from NIST Special Publication 800-38A for CTR, CFB, and OFB. The XTS values were checked against the cryptography library’s XTS implementation. All of them are reproduced by the from-scratch code in the next section and by the live visualizer above.

The shared setup for the three stream modes:

  • Key: 2b7e151628aed2a6abf7158809cf4f3c
  • First plaintext block: 6bc1bee22e409f96e93d7e117393172a
  • CTR initial counter: f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff
  • CFB and OFB IV: 000102030405060708090a0b0c0d0e0f

The first ciphertext block from each mode:

Mode First keystream block First ciphertext block
CTR ec8cdf7398607cb0f2d21675ea9ea1e4 874d6191b620e3261bef6864990db6ce
CFB 3b3fd92eb72dad20... applied to data 3b3fd92eb72dad20333449f8e83cfb4a
OFB 3b3fd92eb72dad20333449f8e83cfb4a 3b3fd92eb72dad20333449f8e83cfb4a

Look at CFB and OFB. Their first ciphertext blocks are identical, 3b3fd92eb72dad20333449f8e83cfb4a, because both equal the plaintext XORed with E(IV). At the second block they part ways, exactly as described above.

For XTS, with data key 2b7e151628aed2a6abf7158809cf4f3c, tweak key 000102030405060708090a0b0c0d0e0f, and sector number 0x21436587:

  • Encrypted tweak for the first block: fb60cf191501967e80be5516a06ac51b
  • Plaintext (two blocks): 6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51
  • Ciphertext: 0cf456e5787c64179f1a18b98b4ab5d299d289cd6327b4c44545579e866a21a4

Python Implementation

This builds every mode on top of a single AES block function. The AES core is omitted here for length and is the standard one from the AES guide; assume aes_encrypt_block, aes_decrypt_block, and key_expansion exist. The point of this section is the modes, which are short.

def xor(a, b):
    return bytes(x ^ y for x, y in zip(a, b))

def ctr_crypt(data, key, counter):
    """CTR encrypts and decrypts with the same code."""
    rks = key_expansion(key)
    out, ctr = bytearray(), int.from_bytes(counter, 'big')
    for i in range(0, len(data), 16):
        ks = aes_encrypt_block((ctr % (1 << 128)).to_bytes(16, 'big'), rks)
        block = data[i:i + 16]
        out += xor(block, ks[:len(block)])
        ctr += 1
    return bytes(out)

def ofb_crypt(data, key, iv):
    """OFB is symmetric too: the keystream ignores the data."""
    rks, out, fb = key_expansion(key), bytearray(), iv
    for i in range(0, len(data), 16):
        fb = aes_encrypt_block(fb, rks)          # feed the AES output back
        block = data[i:i + 16]
        out += xor(block, fb[:len(block)])
    return bytes(out)

def cfb_encrypt(data, key, iv):
    rks, out, fb = key_expansion(key), bytearray(), iv
    for i in range(0, len(data), 16):
        ks = aes_encrypt_block(fb, rks)          # encrypt the previous ciphertext
        block = data[i:i + 16]
        c = xor(block, ks[:len(block)])
        out += c
        fb = c if len(c) == 16 else c + ks[len(c):]
    return bytes(out)

def cfb_decrypt(data, key, iv):
    rks, out, fb = key_expansion(key), bytearray(), iv
    for i in range(0, len(data), 16):
        ks = aes_encrypt_block(fb, rks)          # still forward AES, never decrypt
        block = data[i:i + 16]
        out += xor(block, ks[:len(block)])
        fb = block if len(block) == 16 else block + ks[len(block):]
    return bytes(out)

XTS needs one extra helper, the finite-field multiply by alpha that advances the tweak from one block to the next.

def gf_mul_alpha(t):
    """Multiply the 16-byte tweak by x in GF(2^128), little-endian."""
    t, carry = bytearray(t), 0
    for i in range(16):
        b = t[i]
        t[i] = ((b << 1) | carry) & 0xFF
        carry = b >> 7
    if carry:
        t[0] ^= 0x87                 # the XTS reduction polynomial
    return bytes(t)

def xts_encrypt_aligned(data, key1, key2, data_unit):
    """Block-aligned XTS. Partial blocks need ciphertext stealing."""
    rks1, rks2 = key_expansion(key1), key_expansion(key2)
    t = aes_encrypt_block(data_unit, rks2)       # encrypt the sector number
    out = bytearray()
    for i in range(0, len(data), 16):
        p = data[i:i + 16]
        c = xor(aes_encrypt_block(xor(p, t), rks1), t)   # XOR-Encrypt-XOR
        out += c
        t = gf_mul_alpha(t)
    return bytes(out)

Run these against the NIST vectors in the previous section and every byte matches. The CTR and OFB functions double as their own inverse. CFB and XTS have explicit decrypt counterparts, and XTS decryption is the only place any of these modes calls AES decryption.

For Fun: The Same Thing in ~20 Lines

CTR is so simple that the whole mode fits in a few lines on top of a block function. This generator yields keystream forever, and the caller XORs as much as it needs.

from itertools import count

def ctr_stream(key, nonce):                      # nonce is 8 bytes here
    rks = key_expansion(key)
    for i in count():
        block = nonce + i.to_bytes(8, 'big')
        yield from aes_encrypt_block(block, rks)

def ctr(data, key, nonce):
    return bytes(d ^ k for d, k in zip(data, ctr_stream(key, nonce)))

Encrypt and decrypt are the same call, ctr(data, key, nonce), because XOR undoes itself. That three-line core is the real reason CTR won: there is almost nothing to get wrong, as long as you never repeat a nonce.

Choosing a Mode

Use CTR when you need a plain stream cipher, though in practice you should reach for an authenticated mode like AES-GCM that wraps CTR with integrity. Use XTS for full-disk or sector-level encryption, which is the one job it was designed for. Treat OFB and CFB as legacy. They are worth understanding because you will meet them in older protocols and exam questions, but new designs do not pick them.

None of these four modes authenticates. That is the single most important thing to remember. If you need to detect tampering, and you almost always do, you want an AEAD mode such as AES-GCM or ChaCha20-Poly1305, or you pair the mode with a separate MAC.

Security Considerations

  • Nonce and IV reuse is catastrophic for CTR and OFB. Reusing a counter or IV with the same key produces the same keystream twice. XOR the two ciphertexts and the keystream cancels, leaving the XOR of two plaintexts, which crib-dragging can often peel apart. This is the exact weakness the many-time-pad attack exploits.
  • No integrity by default. All four modes are malleable. An attacker who flips a ciphertext bit flips the matching plaintext bit in CTR, OFB, and CFB, with predictable effect. The CBC padding oracle and the BEAST attack live in this same family of problems.
  • XTS leaks equality at block granularity. Within one sector and key, identical plaintext blocks at the same position encrypt identically across writes. XTS hides content but not every pattern, and it still provides no authentication.
  • XTS data-unit size matters. The standard caps a single data unit at 2^20 AES blocks. Using a fresh tweak per sector, as real disk software does, keeps you well inside that bound.

Real-World Applications

CTR mode underpins AES-GCM, which secures the overwhelming majority of TLS 1.3 traffic, plus IPsec, SSH, and WPA3. XTS is the default for full-disk encryption across the industry: BitLocker, FileVault, the Linux kernel’s dm-crypt and LUKS, and the self-encrypting drives in the TCG Opal standard all use XTS-AES. CFB and OFB show up mainly in older systems, including some OpenPGP messages and legacy financial and industrial protocols that have not been modernized.

Frequently Asked Questions

Why do CTR, OFB, and CFB never use AES decryption? They are stream ciphers. AES generates a keystream, and the data is only ever XORed with it. Since XOR is its own inverse, decryption runs the identical keystream against the ciphertext. The AES decryption routine is dead weight for these three modes.

Is CTR the same as a one-time pad? It is the same idea with a crucial difference. A one-time pad uses a truly random key as long as the message, and it is unbreakable. CTR uses a short key to generate a pseudorandom keystream, which is only as strong as AES. Reuse a CTR counter and you recreate the many-time-pad disaster.

Why does XTS need two keys? One key encrypts the data, the other encrypts the tweak. Keeping them separate prevents interactions between the data path and the tweak path that would otherwise weaken the construction. XTS-AES-128 means two 128-bit keys, so 256 bits of key material total.

What is ciphertext stealing? It is a trick to encrypt data that is not a whole number of blocks without adding padding. XTS borrows the needed bytes from the second-to-last ciphertext block to fill out the final partial block. The result is exactly as long as the plaintext, which matters when a sector is a fixed size.

Can I use XTS to encrypt network messages? You should not. XTS has no nonce and no authentication, and it is tuned for random-access storage. For messages, use an AEAD mode that detects tampering and takes a per-message nonce.

References and Further Reading

  • NIST SP 800-38A: Recommendation for Block Cipher Modes of Operation. The source of the CTR, CFB, and OFB definitions and the test vectors used above.
  • NIST SP 800-38E: Recommendation for the XTS-AES Mode. The storage-device mode.
  • IEEE 1619-2018: Standard for Cryptographic Protection of Data on Block-Oriented Storage Devices. The origin of XTS.
  • FIPS-197: The AES specification. The block cipher every mode here is built on.
  • Rogaway, “Evaluation of Some Blockcipher Modes of Operation” (2011). A readable survey of the tradeoffs.
A visual representation of the AES Algorithm
Modern Block Cipher Intermediate

AES

AES secures nearly everything: HTTPS traffic, encrypted disks, and government secrets alike. Here's how the Rijndael cipher transforms data, round by round.

17 min read

A visual representation of the Kerberos authentication protocol
Hybrid Functions Advanced

Kerberos

Kerberos is a ticket-based authentication protocol from the 1980s that still runs most corporate networks. Learn the AS, TGS, and AP exchanges, the DER message format, and the AES encryption types of RFC 3962 and RFC 8009, with Python code that reproduces the MIT reference encodings and runs a full login.

40 min read

A visual representation of the GMAC Algorithm
Message Authentication Codes Advanced

The GMAC Algorithm

GMAC is the authentication-only mode of AES-GCM. It authenticates data with a polynomial hash over the field GF(2^128), masked by an AES-encrypted counter. Learn how GHASH works, why an IV must never repeat, and how a repeated IV reveals the hash key.

20 min read