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}