Skip to main content

arcana/cipher/
xts.rs

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