Skip to main content

arcana/cipher/
xts.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4//! AES-XTS tweakable block cipher (IEEE 1619, NIST SP 800-38E).
5//!
6//! XTS = XEX-based Tweaked codebook with ciphertext Stealing.
7//!
8//! XTS is the **disk encryption** mode of AES. Unlike GCM and CCM,
9//! it is **not** an AEAD: there is no authentication tag, no
10//! associated data, and no nonce-uniqueness assumption. Instead it
11//! is a *length-preserving, deterministic, tweakable* cipher
12//! designed for storage scenarios where:
13//!
14//! * Each storage unit (typically a 512-byte or 4096-byte sector)
15//!   has a stable identifier — its **sector number** — which is
16//!   used as the tweak.
17//! * Encrypted sectors are written in place at the same byte
18//!   position they had in plaintext (length-preserving).
19//! * The ciphertext for sector `n` is independent of every other
20//!   sector, so single-sector reads / writes are possible.
21//!
22//! Used by **LUKS2** (Linux full-disk encryption), **BitLocker**
23//! (Windows since Vista), **FileVault** (macOS), **VeraCrypt**, and
24//! the embedded SSD self-encryption layers (Opal SED, eDrive).
25//!
26//! # Construction (IEEE 1619 §5.3)
27//!
28//! Two AES keys: `K = K1 || K2` (so 32 bytes for XTS-AES-128 and
29//! 64 bytes for XTS-AES-256). For each 16-byte block `j` of a
30//! sector with sequence number `i`:
31//!
32//! ```text
33//!     T = AES_K2(i)             ; encrypt the tweak (once per sector)
34//!     for j in 0..n_blocks:
35//!         C_j = AES_K1(P_j XOR T) XOR T
36//!         T   = T * α  in GF(2^128)    ; α = 0x02
37//! ```
38//!
39//! `i` is the data unit sequence number, **encoded as 16
40//! little-endian bytes**. The multiplication by `α = 0x02` in
41//! `GF(2^128)` reduces by `x^128 + x^7 + x^2 + x + 1` (the
42//! standard XEX polynomial; `0x87` byte for the high-bit reduction).
43//!
44//! When the sector length is **not** a multiple of 16, the last
45//! block uses **ciphertext stealing**: the last full block and the
46//! short tail block are processed jointly so that the output is
47//! the same length as the input. See [`AesXts::encrypt_sector`]
48//! for the details.
49//!
50//! # API
51//!
52//! ```rust,ignore
53//! use arcana::cipher::xts::AesXts;
54//!
55//! let key:    [u8; 32]   = /* K1 || K2, 32 bytes for XTS-AES-128 */;
56//! let tweak:  [u8; 16]   = sector_number_le_padded;
57//! let mut buf            = sector_plaintext.to_vec();
58//!
59//! let xts = AesXts::new(&key).unwrap();
60//! xts.encrypt_sector(&tweak, &mut buf);
61//! // ... write `buf` to disk ...
62//! xts.decrypt_sector(&tweak, &mut buf);
63//! ```
64//!
65//! # Key reuse warning
66//!
67//! XTS is **not** an AEAD. It does not detect tampering — flipping
68//! one ciphertext bit just flips the same bit in plaintext (within
69//! a single 16-byte block). Disk encryption tools deal with this
70//! at a higher layer (file checksums, file system journals,
71//! application-level MACs).
72//!
73//! XTS *does* offer per-sector independence: rewriting sector 100
74//! does not affect sector 101. But within a sector, an attacker
75//! who can corrupt ciphertext bytes can corrupt plaintext bytes
76//! one-for-one. Don't use XTS for anything that needs end-to-end
77//! integrity.
78//!
79//! # Tests
80//!
81//! Pinned against the standard IEEE 1619 test vectors (the same
82//! ones used by OpenSSL, Crypto++, and BoringSSL).
83
84use super::aes::Aes;
85use crate::BlockCipher;
86
87// ============================================================================
88// GF(2^128) tweak multiplication by α (= 0x02), little-endian byte layout
89// ============================================================================
90
91/// Multiply a 16-byte tweak by `α = 0x02` in `GF(2^128)`, where the
92/// reduction polynomial is `x^128 + x^7 + x^2 + x + 1` (the IEEE
93/// 1619 XEX polynomial — note: this is the same poly as GCM but
94/// with a *little-endian* byte ordering, which inverts the
95/// semantics relative to GHASH).
96///
97/// In bit terms: shift the 128-bit integer left by 1 (so each byte
98/// is shifted left by 1 with the carry coming from the previous
99/// byte), and if the high bit overflowed, XOR `0x87` into byte 0.
100fn gf128_mul_alpha(t: &mut [u8; 16]) {
101    let mut carry: u8 = 0;
102    for byte in t.iter_mut() {
103        let new_carry = *byte >> 7;
104        *byte = (*byte << 1) | carry;
105        carry = new_carry;
106    }
107    if carry != 0 {
108        t[0] ^= 0x87;
109    }
110}
111
112// ============================================================================
113// Public API
114// ============================================================================
115
116/// AES-XTS state. Holds the two AES key schedules `K1` (for the
117/// data block encryption) and `K2` (for the tweak encryption).
118///
119/// Construction is the bottleneck — both key schedules are
120/// expanded once at `new` and reused for every sector.
121pub struct AesXts {
122    /// AES instance with key `K1`, used for the data-path
123    /// `AES_K1(P_j XOR T)` step.
124    k1: Aes,
125    /// AES instance with key `K2`, used for the tweak-path
126    /// `T = AES_K2(i)` step (called exactly once per sector).
127    k2: Aes,
128}
129
130impl AesXts {
131    /// Initialise XTS with a concatenated key `K = K1 || K2`.
132    ///
133    /// Accepts:
134    /// * 32 bytes -- XTS-AES-128 (each half is a 16-byte AES-128 key)
135    /// * 64 bytes -- XTS-AES-256 (each half is a 32-byte AES-256 key)
136    ///
137    /// Returns `None` if the key length is invalid or if `K1 == K2`
138    /// (the IEEE 1619 spec mandates the two halves be distinct, since
139    /// `K1 == K2` collapses XTS to a degenerate variant of XEX with
140    /// a tweak that's just `AES_K(i)` and exposes a known-plaintext
141    /// distinguishing attack).
142    pub fn new(key: &[u8]) -> Option<Self> {
143        let half = match key.len() {
144            32 => 16,
145            64 => 32,
146            _ => return None,
147        };
148        let (k1_bytes, k2_bytes) = key.split_at(half);
149        // IEEE 1619 §5.1: K1 != K2.
150        if k1_bytes == k2_bytes {
151            return None;
152        }
153        Some(Self {
154            k1: <Aes as BlockCipher>::new(k1_bytes),
155            k2: <Aes as BlockCipher>::new(k2_bytes),
156        })
157    }
158
159    /// Encrypt one sector in place.
160    ///
161    /// `tweak` is 16 bytes (little-endian encoding of the sector
162    /// sequence number; pad with zeros for sequence numbers smaller
163    /// than 128 bits, which is the common case).
164    ///
165    /// `data` may be any length **>= 16 bytes** (XTS is not defined
166    /// for < 16 bytes — fewer than one block has nothing to "steal"
167    /// from). Returns silently with the data unchanged if the
168    /// length is below 16. For lengths that are multiples of 16, it
169    /// is plain XEX. Otherwise the last full block and the partial
170    /// tail are joined via ciphertext stealing per IEEE 1619 §5.3.2.
171    pub fn encrypt_sector(&self, tweak: &[u8; 16], data: &mut [u8]) {
172        if data.len() < 16 {
173            return;
174        }
175
176        // Step 1: encrypt the tweak with K2.
177        let mut t = *tweak;
178        self.k2.encrypt_block(&mut t);
179
180        // Step 2: process all but possibly the last two blocks
181        // straightforwardly. If the data length is a multiple of
182        // 16, we'll process all blocks in this loop and skip the
183        // ciphertext-stealing step. Otherwise we'll stop one block
184        // early and feed the trailing two blocks (one full + one
185        // short) into the stealing path.
186        let n = data.len();
187        let full_blocks = n / 16;
188        let tail = n % 16;
189        let blocks_in_main = if tail == 0 { full_blocks } else { full_blocks - 1 };
190
191        for j in 0..blocks_in_main {
192            let off = j * 16;
193            let mut block = [0u8; 16];
194            block.copy_from_slice(&data[off..off + 16]);
195            // P XOR T
196            for i in 0..16 {
197                block[i] ^= t[i];
198            }
199            // AES_K1(...)
200            self.k1.encrypt_block(&mut block);
201            // ... XOR T
202            for i in 0..16 {
203                block[i] ^= t[i];
204            }
205            data[off..off + 16].copy_from_slice(&block);
206
207            // Advance the tweak.
208            gf128_mul_alpha(&mut t);
209        }
210
211        // Step 3: ciphertext stealing for the last 1.x blocks (if any).
212        if tail > 0 {
213            // We are at the second-to-last full block (offset
214            // `(full_blocks - 1) * 16`). Encrypt it with the
215            // current tweak `t`.
216            let last_full_off = (full_blocks - 1) * 16;
217            let mut block = [0u8; 16];
218            block.copy_from_slice(&data[last_full_off..last_full_off + 16]);
219            for i in 0..16 {
220                block[i] ^= t[i];
221            }
222            self.k1.encrypt_block(&mut block);
223            for i in 0..16 {
224                block[i] ^= t[i];
225            }
226            // `block` is now the encrypted "next-to-last" block;
227            // it will become the *last* output block after stealing.
228
229            // The tail block (length `tail`) takes the first `tail`
230            // bytes of `block` to round itself out to 16 bytes;
231            // the original tail bytes XOR-replace the high `tail`
232            // bytes of `block`. (See IEEE 1619 §5.3.2 figure for
233            // a graphical view.)
234            let tail_off = full_blocks * 16;
235            let mut cc = [0u8; 16];
236            // High `16 - tail` bytes of `cc` come from the encrypted
237            // last full block (the "stolen" bytes).
238            cc[tail..].copy_from_slice(&block[tail..]);
239            // Low `tail` bytes of `cc` come from the original tail
240            // plaintext.
241            cc[..tail].copy_from_slice(&data[tail_off..tail_off + tail]);
242
243            // Advance the tweak (one more α-multiply for the
244            // "stolen" final block).
245            gf128_mul_alpha(&mut t);
246
247            // Encrypt `cc` with the advanced tweak.
248            for i in 0..16 {
249                cc[i] ^= t[i];
250            }
251            self.k1.encrypt_block(&mut cc);
252            for i in 0..16 {
253                cc[i] ^= t[i];
254            }
255
256            // Write outputs:
257            // - The "encrypted-tweaked-stolen" block `cc` is the
258            //   new last full block at offset `last_full_off`.
259            // - The first `tail` bytes of `block` (the original
260            //   penultimate-block ciphertext) become the tail of
261            //   the output, at offset `tail_off`.
262            data[last_full_off..last_full_off + 16].copy_from_slice(&cc);
263            data[tail_off..tail_off + tail].copy_from_slice(&block[..tail]);
264        }
265    }
266
267    /// Decrypt one sector in place.
268    ///
269    /// Inverse of [`Self::encrypt_sector`]. Same length / tweak conventions.
270    pub fn decrypt_sector(&self, tweak: &[u8; 16], data: &mut [u8]) {
271        if data.len() < 16 {
272            return;
273        }
274
275        // Encrypt the tweak with K2 (same as on encrypt -- the
276        // tweak path is symmetric).
277        let mut t = *tweak;
278        self.k2.encrypt_block(&mut t);
279
280        let n = data.len();
281        let full_blocks = n / 16;
282        let tail = n % 16;
283        let blocks_in_main = if tail == 0 { full_blocks } else { full_blocks - 1 };
284
285        for j in 0..blocks_in_main {
286            let off = j * 16;
287            let mut block = [0u8; 16];
288            block.copy_from_slice(&data[off..off + 16]);
289            for i in 0..16 {
290                block[i] ^= t[i];
291            }
292            self.k1.decrypt_block(&mut block);
293            for i in 0..16 {
294                block[i] ^= t[i];
295            }
296            data[off..off + 16].copy_from_slice(&block);
297
298            gf128_mul_alpha(&mut t);
299        }
300
301        // Ciphertext stealing on decrypt: same shape, but the
302        // tweak for the second-to-last full block is **the
303        // ADVANCED tweak**, not the current one. We compute the
304        // advanced tweak first, decrypt the last full block with
305        // it, recover the stolen bytes, then decrypt the
306        // (penultimate) block with the un-advanced tweak.
307        if tail > 0 {
308            let last_full_off = (full_blocks - 1) * 16;
309            let tail_off = full_blocks * 16;
310
311            // Save the current tweak; we'll need it after the
312            // advance for the second-to-last block.
313            let mut t_advanced = t;
314            gf128_mul_alpha(&mut t_advanced);
315
316            // Decrypt the last full block (the one at last_full_off,
317            // which currently holds the "encrypted-stolen" block)
318            // with the *advanced* tweak.
319            let mut block = [0u8; 16];
320            block.copy_from_slice(&data[last_full_off..last_full_off + 16]);
321            for i in 0..16 {
322                block[i] ^= t_advanced[i];
323            }
324            self.k1.decrypt_block(&mut block);
325            for i in 0..16 {
326                block[i] ^= t_advanced[i];
327            }
328            // `block` now contains: low `tail` bytes = original
329            // penultimate plaintext, high `16 - tail` bytes = the
330            // bytes that were "stolen" from the encrypted second-
331            // to-last block.
332
333            // Reconstruct the second-to-last ciphertext block by
334            // taking the low `tail` bytes from the on-disk tail
335            // followed by the high `16 - tail` bytes of `block`.
336            let mut cc = [0u8; 16];
337            cc[..tail].copy_from_slice(&data[tail_off..tail_off + tail]);
338            cc[tail..].copy_from_slice(&block[tail..]);
339
340            // Decrypt `cc` with the un-advanced tweak.
341            for i in 0..16 {
342                cc[i] ^= t[i];
343            }
344            self.k1.decrypt_block(&mut cc);
345            for i in 0..16 {
346                cc[i] ^= t[i];
347            }
348
349            // Write outputs.
350            data[last_full_off..last_full_off + 16].copy_from_slice(&cc);
351            data[tail_off..tail_off + tail].copy_from_slice(&block[..tail]);
352        }
353    }
354}
355
356// ============================================================================
357// Tests (IEEE 1619 / Crypto++ pinned vectors)
358// ============================================================================
359
360#[cfg(test)]
361// Index-based loops in the tests below are test scaffolding, not
362// shipped constant-time code.
363#[allow(clippy::needless_range_loop)]
364mod tests {
365    use super::*;
366
367    fn hex(s: &str) -> Vec<u8> {
368        let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
369        assert!(s.len() % 2 == 0);
370        (0..s.len())
371            .step_by(2)
372            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
373            .collect()
374    }
375
376    fn hex_arr<const N: usize>(s: &str) -> [u8; N] {
377        let v = hex(s);
378        assert_eq!(v.len(), N);
379        let mut out = [0u8; N];
380        out.copy_from_slice(&v);
381        out
382    }
383
384    /// **IEEE 1619 / Crypto++ test vector 1** -- the all-zero canary.
385    ///
386    /// Key:        00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
387    ///             00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
388    /// Tweak (i):  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
389    /// Plain:      00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
390    ///             00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
391    /// Cipher:     91 7c f6 9e bd 68 b2 ec 9b 9f e9 a3 ea dd a6 92
392    ///             cd 43 d2 f5 95 98 ed 85 8c 02 c2 65 2f bf 92 2e
393    ///
394    /// This vector is a sharp test for two classes of bugs:
395    /// (1) any state contamination from a non-zero K1, K2, or
396    /// initial tweak would change the output; (2) the K1 == K2
397    /// rejection: this vector intentionally uses K1 == K2 == 0,
398    /// which our `new` rightly rejects -- so we have to construct
399    /// the AES instances directly to exercise it. We do that via
400    /// the test-only path below.
401    ///
402    /// Note: for the public API, the K1 == K2 == 0 case is
403    /// (rightly) refused. We bypass that here only to validate the
404    /// raw construction against the canonical reference.
405    #[test]
406    fn ieee1619_vector_1_all_zero() {
407        let zero = [0u8; 16];
408        let xts = AesXts {
409            k1: <Aes as BlockCipher>::new(&zero),
410            k2: <Aes as BlockCipher>::new(&zero),
411        };
412        let tweak: [u8; 16] = [0u8; 16];
413        let mut data = [0u8; 32];
414        xts.encrypt_sector(&tweak, &mut data);
415
416        let expected = hex("917cf69ebd68b2ec9b9fe9a3eadda692\
417             cd43d2f59598ed858c02c2652fbf922e");
418        assert_eq!(data.to_vec(), expected);
419
420        // Round-trip
421        xts.decrypt_sector(&tweak, &mut data);
422        assert_eq!(data, [0u8; 32]);
423    }
424
425    /// **IEEE 1619 / Crypto++ test vector 2.**
426    ///
427    /// Key:    11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11
428    ///         22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22
429    /// Tweak:  33 33 33 33 33 00 00 00 00 00 00 00 00 00 00 00
430    /// Plain:  44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44
431    ///         44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44
432    /// Cipher: c4 54 18 5e 6a 16 93 6e 39 33 40 38 ac ef 83 8b
433    ///         fb 18 6f ff 74 80 ad c4 28 93 82 ec d6 d3 94 f0
434    #[test]
435    fn ieee1619_vector_2() {
436        let key = hex("11111111111111111111111111111111\
437             22222222222222222222222222222222");
438        let tweak: [u8; 16] = hex_arr("33333333330000000000000000000000");
439        let mut data = hex("44444444444444444444444444444444\
440             44444444444444444444444444444444");
441
442        let xts = AesXts::new(&key).unwrap();
443        xts.encrypt_sector(&tweak, &mut data);
444
445        let expected = hex("c454185e6a16936e39334038acef838b\
446             fb186fff7480adc4289382ecd6d394f0");
447        assert_eq!(data, expected);
448
449        // Round-trip
450        xts.decrypt_sector(&tweak, &mut data);
451        let original = hex("44444444444444444444444444444444\
452             44444444444444444444444444444444");
453        assert_eq!(data, original);
454    }
455
456    /// **IEEE 1619 / Crypto++ test vector 3** -- different K1, same
457    /// K2 / tweak / plaintext as vector 2 to cross-check that K1
458    /// is actually consumed in the data path.
459    ///
460    /// Key:    ff fe fd fc fb fa f9 f8 f7 f6 f5 f4 f3 f2 f1 f0
461    ///         22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22
462    /// Tweak:  33 33 33 33 33 00 00 00 00 00 00 00 00 00 00 00
463    /// Plain:  44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44
464    ///         44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44
465    /// Cipher: af 85 33 6b 59 7a fc 1a 90 0b 2e b2 1e c9 49 d2
466    ///         92 df 4c 04 7e 0b 21 53 21 86 a5 97 1a 22 7a 89
467    #[test]
468    fn ieee1619_vector_3() {
469        let key = hex("fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0\
470             22222222222222222222222222222222");
471        let tweak: [u8; 16] = hex_arr("33333333330000000000000000000000");
472        let mut data = hex("44444444444444444444444444444444\
473             44444444444444444444444444444444");
474
475        let xts = AesXts::new(&key).unwrap();
476        xts.encrypt_sector(&tweak, &mut data);
477
478        let expected = hex("af85336b597afc1a900b2eb21ec949d2\
479             92df4c047e0b21532186a5971a227a89");
480        assert_eq!(data, expected);
481    }
482
483    /// Build a 32-byte XTS key with K1 != K2 (the IEEE 1619 spec
484    /// requires the two halves to be distinct, and our `new` enforces
485    /// it). Used by every property test below.
486    fn distinct_key_32() -> [u8; 32] {
487        let mut k = [0u8; 32];
488        for i in 0..16 {
489            k[i] = i as u8 ^ 0x42;
490        }
491        for i in 16..32 {
492            k[i] = i as u8 ^ 0xa5;
493        }
494        k
495    }
496
497    /// Round-trip on a multi-block sector (4 full blocks = 64 bytes).
498    /// Tests the tweak advance / `gf128_mul_alpha` path across more
499    /// than two blocks.
500    #[test]
501    fn xts_multi_block_roundtrip() {
502        let key = distinct_key_32();
503        let tweak = [0xa5u8; 16];
504        let original: Vec<u8> = (0..64).map(|i| i as u8).collect();
505        let mut data = original.clone();
506
507        let xts = AesXts::new(&key).unwrap();
508        xts.encrypt_sector(&tweak, &mut data);
509        assert_ne!(data, original);
510
511        xts.decrypt_sector(&tweak, &mut data);
512        assert_eq!(data, original);
513    }
514
515    /// Round-trip with ciphertext stealing: 17 bytes (1 full + 1 byte).
516    #[test]
517    fn xts_ciphertext_stealing_17_bytes() {
518        let key = distinct_key_32();
519        let tweak = [0xa5u8; 16];
520        let original: Vec<u8> = (0..17).map(|i| (i * 11) as u8).collect();
521        let mut data = original.clone();
522
523        let xts = AesXts::new(&key).unwrap();
524        xts.encrypt_sector(&tweak, &mut data);
525        assert_eq!(data.len(), 17, "XTS must be length-preserving");
526        assert_ne!(data, original);
527
528        xts.decrypt_sector(&tweak, &mut data);
529        assert_eq!(data, original);
530    }
531
532    /// Round-trip with ciphertext stealing: 31 bytes (1 full + 15 bytes).
533    /// Exercises the maximum tail length.
534    #[test]
535    fn xts_ciphertext_stealing_31_bytes() {
536        let key = distinct_key_32();
537        let tweak = [0xa5u8; 16];
538        let original: Vec<u8> = (0..31).map(|i| (i * 7) as u8).collect();
539        let mut data = original.clone();
540
541        let xts = AesXts::new(&key).unwrap();
542        xts.encrypt_sector(&tweak, &mut data);
543        assert_eq!(data.len(), 31);
544
545        xts.decrypt_sector(&tweak, &mut data);
546        assert_eq!(data, original);
547    }
548
549    /// Round-trip with ciphertext stealing: 100 bytes (6 full + 4 bytes).
550    /// Tests stealing combined with multiple full blocks.
551    #[test]
552    fn xts_ciphertext_stealing_100_bytes() {
553        let key = distinct_key_32();
554        let tweak = [0xa5u8; 16];
555        let original: Vec<u8> = (0..100).map(|i| (i ^ 0x5a) as u8).collect();
556        let mut data = original.clone();
557
558        let xts = AesXts::new(&key).unwrap();
559        xts.encrypt_sector(&tweak, &mut data);
560        assert_eq!(data.len(), 100);
561
562        xts.decrypt_sector(&tweak, &mut data);
563        assert_eq!(data, original);
564    }
565
566    /// Different tweaks must produce different ciphertexts for the
567    /// same plaintext + key. This is the *whole point* of XTS:
568    /// per-sector independence.
569    #[test]
570    fn xts_different_tweaks_differ() {
571        let key = distinct_key_32();
572        let tweak1 = [0u8; 16];
573        let mut tweak2 = [0u8; 16];
574        tweak2[0] = 1;
575        let original: Vec<u8> = (0..32).map(|i| i as u8).collect();
576
577        let xts = AesXts::new(&key).unwrap();
578
579        let mut data1 = original.clone();
580        xts.encrypt_sector(&tweak1, &mut data1);
581
582        let mut data2 = original.clone();
583        xts.encrypt_sector(&tweak2, &mut data2);
584
585        assert_ne!(data1, data2);
586    }
587
588    /// XTS-AES-256 round-trip (64-byte concatenated key).
589    #[test]
590    fn xts_aes256_roundtrip() {
591        let mut key = [0u8; 64];
592        for i in 0..32 {
593            key[i] = i as u8;
594        }
595        for i in 32..64 {
596            key[i] = (i + 0x80) as u8;
597        }
598        let tweak = [0x11u8; 16];
599        let original: Vec<u8> = (0..48).map(|i| i as u8).collect();
600        let mut data = original.clone();
601
602        let xts = AesXts::new(&key).unwrap();
603        xts.encrypt_sector(&tweak, &mut data);
604        xts.decrypt_sector(&tweak, &mut data);
605        assert_eq!(data, original);
606    }
607
608    /// Parameter validation: invalid key lengths are rejected.
609    #[test]
610    fn xts_rejects_invalid_key_lengths() {
611        for bad_len in [0, 1, 15, 16, 17, 24, 31, 33, 48, 63, 65, 128] {
612            let key = vec![0u8; bad_len];
613            assert!(AesXts::new(&key).is_none(), "key length {} should be rejected", bad_len);
614        }
615        // Valid lengths.
616        // We need K1 != K2 to actually construct, so use distinct halves.
617        let mut k32 = [0u8; 32];
618        k32[16] = 1;
619        assert!(AesXts::new(&k32).is_some());
620        let mut k64 = [0u8; 64];
621        k64[32] = 1;
622        assert!(AesXts::new(&k64).is_some());
623    }
624
625    /// Parameter validation: K1 == K2 is rejected per IEEE 1619 §5.1.
626    #[test]
627    fn xts_rejects_k1_eq_k2() {
628        // 32-byte key with K1 == K2 == all 1's
629        let k32 = [0x11u8; 32];
630        assert!(AesXts::new(&k32).is_none());
631        // 64-byte key with K1 == K2 == all 1's
632        let k64 = [0x11u8; 64];
633        assert!(AesXts::new(&k64).is_none());
634    }
635
636    /// gf128_mul_alpha sanity test: shifting `0x01 || 0...0` should
637    /// give `0x02 || 0...0`. Shifting `0x80 || 0...0` (highest byte
638    /// of byte 0) should give `0x00 || 0...0` with no carry, so we
639    /// also test `0...0 || 0x80` which sets the high bit overall.
640    #[test]
641    fn gf128_mul_alpha_basic() {
642        // Test 1: shift 0x01 -> 0x02
643        let mut t = [0u8; 16];
644        t[0] = 0x01;
645        gf128_mul_alpha(&mut t);
646        let mut expected = [0u8; 16];
647        expected[0] = 0x02;
648        assert_eq!(t, expected);
649
650        // Test 2: shift the highest bit of the last byte -- this
651        // is the bit that overflows out of the 128-bit register and
652        // triggers the `0x87` reduction XOR on byte 0.
653        let mut t = [0u8; 16];
654        t[15] = 0x80;
655        gf128_mul_alpha(&mut t);
656        let mut expected = [0u8; 16];
657        expected[0] = 0x87;
658        assert_eq!(t, expected);
659    }
660}