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}