Skip to content

Disk encryption

XTS-AES and Adiantum: encryption that keeps a sector exactly its size, with no room for a nonce or a tag. What that costs, and where each runs.

Status and standards: see Cipher modes and AEAD in the main table.

What it is

A disk reads and writes fixed sectors, usually 512 or 4096 bytes, by number. Encryption has to fit that: a sector encrypts to the same size, any sector decrypts on its own, and there is nowhere to keep a fresh nonce or a 16-byte tag for each write. As Adiantum's authors put it, nonce reuse is unavoidable and a MAC has no space, so AES-GCM is out.

What's left is a tweakable cipher. The sector number goes in as a tweak, so the same data in two sectors encrypts differently. Writing the same data to the same sector again gives the same ciphertext; a mode that keeps the size can't avoid that.

The older answer was CBC with an IV made from the sector number. Its problem, as Niels Ferguson wrote when designing BitLocker for Vista, is that an attacker can flip chosen bits in one block at the cost of garbling the block before. BitLocker added a diffuser on top; later systems moved to XTS.

XTS-AES

Where it came from. Liskov, Rivest and Wagner defined tweakable block ciphers (CRYPTO 2002). Rogaway's XEX (ASIACRYPT 2004) builds one from a block cipher: C = E(K, P ⊕ Δ) ⊕ Δ, where Δ comes from encrypting the tweak. The IEEE P1619 group started with LRW, then switched to XEX because of worries about key material being swapped to disk. It used two keys instead of XEX's one and added ciphertext stealing: XTS, the "XEX-based tweaked codebook mode with ciphertext stealing". IEEE 1619-2007 standardized it, NIST approved it in SP 800-38E (2010), and IEEE has since revised it as 1619-2018 and 1619-2025.

How it works.

  • Two AES keys: K1 for the data and K2 for the tweak. XTS-AES-128 takes 256 key bits and XTS-AES-256 takes 512. FIPS validation requires K1 ≠ K2.
  • For block j of sector i: T = E(K2, i) · αʲ, and C = E(K1, P ⊕ T) ⊕ T. Multiplying by α in GF(2¹²⁸) is a 1-bit shift and, on overflow, an XOR with 0x87.
  • A short last block uses ciphertext stealing. Sectors are whole blocks, so disks rarely need it.
  • SP 800-38E caps one data unit at 2²⁰ blocks (16 MiB). IEEE 1619-2018 made that a requirement too, and 1619-2025 adds a cap on how much data one key may cover. NIST's draft SP 800-38E Rev. 1 (September 2026) moves to IEEE 1619-2025.

Where it's used.

  • BitLocker: XTS-AES since Windows 10 version 1511 (2015), and XTS-AES-128 is the default. Vista and 7 defaulted to AES-CBC with Ferguson's "Elephant" diffuser.
  • FileVault: AES-XTS for whole volumes, per Apple's platform security guide.
  • LUKS / dm-crypt: cryptsetup made aes-xts-plain64 the default in 1.6 (2013). Since 2.1 (2019) the default key is 512 bits, so AES-256-XTS.
  • Android: file-based encryption uses XTS for file contents.

What it doesn't do. XTS has no integrity. A changed ciphertext block decrypts to 16 random-looking bytes rather than a chosen bit flip, but nothing reports it, and an old copy of a block can be put back unnoticed. An attacker who sees the disk at two times learns which 16-byte blocks changed. NIST still calls it better against tampering than the other confidentiality-only modes. IEEE chose a narrow (16-byte) mode over a sector-wide one mostly because of patents, according to NIST IR 8459.

Adiantum

Paul Crowley and Eric Biggers at Google designed Adiantum (2018) for cheap phones whose CPUs lack AES instructions, where AES-XTS is slow. It is a wide-block mode: the whole sector is one block, so changing any bit changes the whole sector's ciphertext. The paper calls the structure HBSH (hash, block cipher, stream cipher, hash). For a sector split into P_L and its last 16 bytes P_R, with tweak T:

  1. P_M = P_R + H(T, P_L), where H is NH plus Poly1305 and + is mod 2¹²⁸
  2. C_M = AES-256(K_E, P_M)
  3. C_L = P_L ⊕ XChaCha12(K_S, nonce from C_M)
  4. C_R = C_M − H(T, C_L)

The user key is the 256-bit XChaCha12 key K_S, and K_E and the hash keys come from its keystream. On an ARM Cortex-A7 it decrypts 4096-byte sectors at 10.6 cycles per byte, over five times faster than AES-256-XTS. Linux 5.0 (2019) added it, and Android 9 and later can use it on devices without AES instructions. Android's docs say not to use it where ARMv8 Crypto Extensions or AES-NI exist. There is no NIST or IETF standard for it.

When to use it

  • Full-disk or block-layer encryption: XTS-AES, which is what BitLocker, FileVault and LUKS already do.
  • CPUs without AES instructions: Adiantum.
  • Anywhere a nonce and tag fit (files, backups, databases, network): an AEAD, not a disk mode.

Example (Python)

XTS needs AES, which Python's standard library doesn't have, so a keyed hash stands in. That is enough to encrypt (it can't decrypt) and to see what XTS leaks. With a real AES in place of E, xts() matches the IEEE 1619 test vectors for whole blocks.

import hmac, os

def E(k, b):                        # stand-in for AES: a keyed hash
    return hmac.digest(k, b, "sha256")[:16]

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

def double(t):                      # multiply by x in GF(2^128), IEEE 1619 byte order
    n = int.from_bytes(t, "little") << 1
    if n >> 128:
        n ^= (1 << 128) | 0x87
    return n.to_bytes(16, "little")

def xts(k1, k2, sector, data):      # whole 16-byte blocks only, no ciphertext stealing
    t = E(k2, sector.to_bytes(16, "little"))
    out = b""
    for i in range(0, len(data), 16):
        out += xor(E(k1, xor(data[i:i + 16], t)), t)
        t = double(t)
    return out

def blocks(s):
    return [s[i:i + 16] for i in range(0, len(s), 16)]

k1, k2 = os.urandom(16), os.urandom(16)
old = bytes(4096)                                 # a 4 KB sector of zeros
new = old[:1000] + b"!" + old[1001:]              # change byte 1000

a = xts(k1, k2, 7, old)
print(len(set(blocks(a))))                                  # 256: equal blocks don't show
print(a == xts(k1, k2, 7, old), a == xts(k1, k2, 8, old))   # True False
b = xts(k1, k2, 7, new)
print([i for i, (x, y) in enumerate(zip(blocks(a), blocks(b))) if x != y])   # [62]

The zeros don't repeat in the ciphertext, and the same data in sector 8 looks different. But writing the same sector twice gives the same bytes, and one changed byte shows up as one changed block, number 62. Under Adiantum that last line would list all 256 blocks.

Security notes

  • XTS protects a powered-off, stolen disk. It does not stop someone who can watch and write the disk over time, such as a host holding a VM image.
  • Generate K1 and K2 independently. A 512-bit XTS key is AES-256 strength, not 512.

See also

References

Weaker sources: KernelNewbies, Linux 5.0 for the kernel version that added Adiantum.