Skip to main content

storage_xts/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use cipher::inout::InOut;
6use cipher::typenum::consts::U16;
7use cipher::{
8    Array, BlockCipherDecBackend, BlockCipherDecClosure, BlockCipherEncBackend,
9    BlockCipherEncClosure, BlockSizeUser,
10};
11use static_assertions::assert_cfg;
12use storage_ptr_slice::{MutPtrByteSlice, PtrByteSlice};
13use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
14
15// This assumes little-endianness which is likely to always be the case.
16assert_cfg!(target_endian = "little");
17
18#[derive(IntoBytes, KnownLayout, FromBytes, Immutable, Debug, Clone, Copy, PartialEq, Eq)]
19#[repr(C)]
20pub struct Tweak(pub u128);
21
22impl Tweak {
23    pub fn new(val: u128) -> Self {
24        Self(val)
25    }
26
27    fn update(&mut self) {
28        self.0 = (self.0 << 1) ^ ((self.0 as i128 >> 127) as u128 & 0x87);
29    }
30}
31
32/// To be used with encrypt|decrypt_with_backend.
33pub struct XtsProcessor<'a, 'b> {
34    tweak: Tweak,
35    src: PtrByteSlice<'a>,
36    dst: MutPtrByteSlice<'b>,
37}
38
39fn xts_encrypt_chunk<B: BlockCipherEncBackend<BlockSize = U16>>(
40    backend: &B,
41    mut val: u128,
42    tweak: &Tweak,
43) -> u128 {
44    // XOR plaintext with tweak.
45    val ^= tweak.0;
46
47    let arr: &mut Array<u8, U16> = val.as_mut_bytes().try_into().unwrap();
48    backend.encrypt_block(InOut::from(arr));
49
50    // XOR ciphertext with tweak.
51    val ^ tweak.0
52}
53
54/// Internal only. Assumes all fields have been validated.
55fn xts_encrypt_buffer<B: BlockCipherEncBackend<BlockSize = U16>>(
56    backend: &B,
57    src: PtrByteSlice<'_>,
58    mut dst: MutPtrByteSlice<'_>,
59    tweak: &mut Tweak,
60) {
61    debug_assert!(src.as_ptr().cast::<u128>().is_aligned());
62    debug_assert!(dst.as_ptr().cast::<u128>().is_aligned());
63    let src_chunks = src.iter_as::<u128>();
64    let dst_chunks = dst.iter_as_mut::<u128>();
65
66    for (src_chunk, dst_chunk) in src_chunks.zip(dst_chunks) {
67        let val = xts_encrypt_chunk(backend, src_chunk.read(), &tweak);
68        dst_chunk.write(val);
69        tweak.update();
70    }
71}
72
73fn xts_decrypt_chunk<B: BlockCipherDecBackend<BlockSize = U16>>(
74    backend: &B,
75    mut val: u128,
76    tweak: &Tweak,
77) -> u128 {
78    // XOR ciphertext with tweak.
79    val ^= tweak.0;
80
81    let arr: &mut Array<u8, U16> = val.as_mut_bytes().try_into().unwrap();
82    backend.decrypt_block(InOut::from(arr));
83
84    // XOR plaintext with tweak.
85    val ^ tweak.0
86}
87
88/// Internal only. Assumes all fields have been validated.
89fn xts_decrypt_buffer<B: BlockCipherDecBackend<BlockSize = U16>>(
90    backend: &B,
91    src: PtrByteSlice<'_>,
92    mut dst: MutPtrByteSlice<'_>,
93    tweak: &mut Tweak,
94) {
95    debug_assert!(src.as_ptr().cast::<u128>().is_aligned());
96    debug_assert!(dst.as_ptr().cast::<u128>().is_aligned());
97    let src_chunks = src.iter_as::<u128>();
98    let dst_chunks = dst.iter_as_mut::<u128>();
99
100    for (src_chunk, dst_chunk) in src_chunks.zip(dst_chunks) {
101        let val = xts_decrypt_chunk(backend, src_chunk.read(), &tweak);
102        dst_chunk.write(val);
103        tweak.update();
104    }
105}
106
107impl<'a, 'b> XtsProcessor<'a, 'b> {
108    // `tweak` should be encrypted. `src` and `dst` must have the same length and be 16 byte
109    // aligned.
110    pub fn new(tweak: Tweak, src: PtrByteSlice<'a>, mut dst: MutPtrByteSlice<'b>) -> Self {
111        assert_eq!(src.len(), dst.len(), "Source and destination lengths must match");
112        assert!(src.as_ptr().cast::<u128>().is_aligned(), "src must be 16 byte aligned");
113        assert!(dst.as_ptr().cast::<u128>().is_aligned(), "dst must be 16 byte aligned");
114        dst.zero_no_rfo();
115        Self { tweak, src, dst }
116    }
117
118    /// Creates an XtsProcessor for in-place operation on a single buffer.
119    pub fn new_in_place(tweak: Tweak, buf: MutPtrByteSlice<'a>) -> XtsProcessor<'a, 'a> {
120        assert!(buf.as_ptr().cast::<u128>().is_aligned(), "buf must be 16 byte aligned");
121        let len = buf.len();
122        let ptr = buf.as_ptr_slice().as_ptr();
123        // SAFETY: We are creating a PtrByteSlice that aliases with the MutPtrByteSlice.
124        // This is safe because PtrByteSlice only allows read access, and we control the
125        // execution in `call` to ensure we don't violate safety (we read a block, then write it,
126        // so we don't have concurrent read/write on the same sub-block).
127        let src = unsafe { PtrByteSlice::new(std::ptr::slice_from_raw_parts(ptr, len)) };
128        XtsProcessor { tweak, src, dst: buf }
129    }
130}
131
132impl BlockSizeUser for XtsProcessor<'_, '_> {
133    type BlockSize = U16;
134}
135
136impl BlockCipherEncClosure for XtsProcessor<'_, '_> {
137    fn call<B: BlockCipherEncBackend<BlockSize = Self::BlockSize>>(self, backend: &B) {
138        let Self { mut tweak, src, dst } = self;
139        xts_encrypt_buffer(backend, src, dst, &mut tweak);
140    }
141}
142
143impl BlockCipherDecClosure for XtsProcessor<'_, '_> {
144    fn call<B: BlockCipherDecBackend<BlockSize = Self::BlockSize>>(self, backend: &B) {
145        let Self { mut tweak, src, dst } = self;
146        xts_decrypt_buffer(backend, src, dst, &mut tweak);
147    }
148}
149
150/// Handles ciphertext stealing in order to allow non-BlockSize lengths to be used as long as they
151/// are >= BlockSize. To be used with encrypt|decrypt_with_backend. Conforms to IEEE 1619-2007.
152pub struct XtsCtsProcessor<'a, 'b> {
153    tweak: Tweak,
154    src: PtrByteSlice<'a>,
155    dst: MutPtrByteSlice<'b>,
156}
157
158impl BlockSizeUser for XtsCtsProcessor<'_, '_> {
159    type BlockSize = U16;
160}
161
162impl<'a, 'b> XtsCtsProcessor<'a, 'b> {
163    /// `tweak` should be encrypted. `src` and `dst` must have the same length and be 16 byte
164    /// aligned.
165    pub fn new(tweak: Tweak, src: PtrByteSlice<'a>, mut dst: MutPtrByteSlice<'b>) -> Self {
166        assert_eq!(src.len(), dst.len(), "Source and destination lengths must match");
167        assert!(src.len() >= size_of::<u128>());
168        assert!(src.as_ptr().cast::<u128>().is_aligned(), "src must be 16 byte aligned");
169        assert!(dst.as_ptr().cast::<u128>().is_aligned(), "dst must be 16 byte aligned");
170        dst.zero_no_rfo();
171        Self { tweak, src, dst }
172    }
173
174    /// Creates an XtsCtsProcessor for in-place operation on a single buffer.
175    pub fn new_in_place(tweak: Tweak, buf: MutPtrByteSlice<'a>) -> XtsCtsProcessor<'a, 'a> {
176        assert!(buf.len() >= size_of::<u128>());
177        assert!(buf.as_ptr().cast::<u128>().is_aligned(), "buf must be 16 byte aligned");
178        let len = buf.len();
179        let ptr = buf.as_ptr_slice().as_ptr();
180        // SAFETY: We are creating a PtrByteSlice that aliases with the MutPtrByteSlice.
181        // This is safe because PtrByteSlice only allows read access, and we control the
182        // execution in `call` to ensure we don't violate safety.
183        let src = unsafe { PtrByteSlice::new(std::ptr::slice_from_raw_parts(ptr, len)) };
184        XtsCtsProcessor { tweak, src, dst: buf }
185    }
186}
187
188impl BlockCipherEncClosure for XtsCtsProcessor<'_, '_> {
189    fn call<B: BlockCipherEncBackend<BlockSize = Self::BlockSize>>(self, backend: &B) {
190        let Self { mut tweak, src, mut dst } = self;
191        let len = src.len();
192        let base_size = len & (!0x0F);
193
194        // Fast path for non-CTS (len is an exact multiple of 16):
195        if len == base_size {
196            xts_encrypt_buffer(backend, src, dst, &mut tweak);
197            return;
198        }
199
200        // All but the last two blocks are normal.
201        if base_size > 16 {
202            let src_base = src.subslice(0..(base_size - 16));
203            let dst_base = dst.subslice_mut(0..(base_size - 16));
204            xts_encrypt_buffer(backend, src_base, dst_base, &mut tweak);
205        }
206
207        let last_full_block_range = (base_size - 16)..base_size;
208        let last_full_block =
209            src.subslice(last_full_block_range.clone()).read().expect("Size validated above");
210        let intermediate_ciphertext = xts_encrypt_chunk(backend, last_full_block, &tweak);
211
212        let extra = len & 0x0F;
213        let intermediate_cipher_bytes = intermediate_ciphertext.to_le_bytes();
214
215        // Construct combined block: start with intermediate_cipher_bytes at the end and put the
216        // remaining plaintext at the beginning. Must read from second-to-last before writing to
217        // last block to support in-place operation.
218        let mut combined_block_bytes = intermediate_cipher_bytes;
219        src.subslice(base_size..len).copy_to_slice(&mut combined_block_bytes[0..extra]);
220
221        // Write the first ciphertext bytes into the partial block slot.
222        dst.subslice_mut(base_size..len).copy_from_slice(&intermediate_cipher_bytes[0..extra]);
223
224        tweak.update();
225
226        let combined_block = u128::from_le_bytes(combined_block_bytes);
227        let full_ciphertext = xts_encrypt_chunk(backend, combined_block, &tweak);
228
229        // Write the encrypted full ciphertext block into the second-to-last block slot.
230        dst.subslice_mut(last_full_block_range)
231            .write(full_ciphertext)
232            .expect("Size validated above");
233    }
234}
235
236impl BlockCipherDecClosure for XtsCtsProcessor<'_, '_> {
237    fn call<B: BlockCipherDecBackend<BlockSize = Self::BlockSize>>(self, backend: &B) {
238        let Self { mut tweak, src, mut dst } = self;
239        let len = src.len();
240        let base_size = len & (!0x0F);
241
242        // Fast path for non-CTS (len is an exact multiple of 16):
243        if len == base_size {
244            xts_decrypt_buffer(backend, src, dst, &mut tweak);
245            return;
246        }
247
248        // All but the last two blocks are normal.
249        if base_size > 16 {
250            let src_base = src.subslice(0..(base_size - 16));
251            let dst_base = dst.subslice_mut(0..(base_size - 16));
252            xts_decrypt_buffer(backend, src_base, dst_base, &mut tweak);
253        }
254
255        let last_full_block_tweak = tweak;
256        let last_full_block_range = (base_size - 16)..base_size;
257
258        tweak.update();
259        let extra = len & 0x0F;
260
261        // Decrypt full ciphertext block at last_full_block_range using updated tweak:
262        let full_ciphertext =
263            src.subslice(last_full_block_range.clone()).read().expect("Size validated above");
264        let decrypted_combined_block = xts_decrypt_chunk(backend, full_ciphertext, &tweak);
265        let combined_bytes = decrypted_combined_block.to_le_bytes();
266
267        // Reconstruct intermediate ciphertext for second-to-last block. Start with combined_bytes
268        // at the end and put the input ciphertext at the beginning. Must read from second-to-last
269        // before writing to last block to support in-place operation.
270        let mut intermediate_bytes = combined_bytes;
271        src.subslice(base_size..len).copy_to_slice(&mut intermediate_bytes[0..extra]);
272
273        // Write recovered partial plaintext.
274        dst.subslice_mut(base_size..len).copy_from_slice(&combined_bytes[0..extra]);
275
276        let last_block_ciphertext = u128::from_le_bytes(intermediate_bytes);
277        let val = xts_decrypt_chunk(backend, last_block_ciphertext, &last_full_block_tweak);
278        dst.subslice_mut(last_full_block_range).write(val).expect("Size validated above");
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use cipher::inout::InOut;
286    use cipher::typenum::consts::U1;
287    use cipher::{Block, ParBlocksSizeUser};
288    use std::cell::RefCell;
289    use test_case::test_case;
290
291    struct MockCipher {
292        recorded_blocks: RefCell<Vec<u128>>,
293        key: u128,
294    }
295
296    impl MockCipher {
297        fn new(key: u128) -> Self {
298            Self { recorded_blocks: RefCell::new(Vec::new()), key }
299        }
300    }
301
302    impl BlockSizeUser for MockCipher {
303        type BlockSize = U16;
304    }
305
306    impl ParBlocksSizeUser for MockCipher {
307        type ParBlocksSize = U1;
308    }
309
310    impl BlockCipherEncBackend for MockCipher {
311        fn encrypt_block(&self, mut block: InOut<'_, '_, Block<Self>>) {
312            // SAFETY: Block<Self> is Array<u8, U16>, which is 16 bytes.
313            let mut val =
314                unsafe { std::ptr::read_unaligned(block.get_in().as_ptr() as *const u128) };
315            self.recorded_blocks.borrow_mut().push(val);
316            val ^= self.key;
317            // SAFETY: Block<Self> is Array<u8, U16>, which is 16 bytes.
318            unsafe {
319                std::ptr::write_unaligned(block.get_out().as_mut_ptr() as *mut u128, val);
320            }
321        }
322    }
323
324    impl BlockCipherDecBackend for MockCipher {
325        fn decrypt_block(&self, mut block: InOut<'_, '_, Block<Self>>) {
326            // SAFETY: Block<Self> is Array<u8, U16>, which is 16 bytes.
327            let mut val =
328                unsafe { std::ptr::read_unaligned(block.get_in().as_ptr() as *const u128) };
329            self.recorded_blocks.borrow_mut().push(val);
330            // Lazy scramble. See above for the encryption method.
331            val = val.rotate_right(3);
332            val ^= self.key;
333            // SAFETY: Block<Self> is Array<u8, U16>, which is 16 bytes.
334            unsafe {
335                std::ptr::write_unaligned(block.get_out().as_mut_ptr() as *mut u128, val);
336            }
337        }
338    }
339
340    /// A simple fake scrambler that includes a rotate left to keep the XOR from canceling itself
341    /// out.
342    struct MockNonLinearCipher {
343        key: u128,
344    }
345
346    impl MockNonLinearCipher {
347        fn new(key: u128) -> Self {
348            Self { key }
349        }
350    }
351
352    impl BlockSizeUser for MockNonLinearCipher {
353        type BlockSize = U16;
354    }
355
356    impl ParBlocksSizeUser for MockNonLinearCipher {
357        type ParBlocksSize = U1;
358    }
359
360    impl BlockCipherEncBackend for MockNonLinearCipher {
361        fn encrypt_block(&self, mut block: InOut<'_, '_, Block<Self>>) {
362            let mut val =
363                unsafe { std::ptr::read_unaligned(block.get_in().as_ptr() as *const u128) };
364            val = val.rotate_left(11) ^ self.key;
365            unsafe {
366                std::ptr::write_unaligned(block.get_out().as_mut_ptr() as *mut u128, val);
367            }
368        }
369    }
370
371    impl BlockCipherDecBackend for MockNonLinearCipher {
372        fn decrypt_block(&self, mut block: InOut<'_, '_, Block<Self>>) {
373            let mut val =
374                unsafe { std::ptr::read_unaligned(block.get_in().as_ptr() as *const u128) };
375            val = (val ^ self.key).rotate_right(11);
376            unsafe {
377                std::ptr::write_unaligned(block.get_out().as_mut_ptr() as *mut u128, val);
378            }
379        }
380    }
381
382    #[repr(C)]
383    #[derive(FromBytes, IntoBytes, Immutable)]
384    struct Blocks<const N: usize>([u128; N]);
385
386    static_assertions::const_assert!(std::mem::align_of::<Blocks<1>>() == 16);
387    static_assertions::const_assert!(std::mem::align_of::<Blocks<2>>() == 16);
388
389    impl<const N: usize> Default for Blocks<N> {
390        fn default() -> Self {
391            Self([0u128; N])
392        }
393    }
394
395    #[test]
396    fn test_xts_out_of_place() {
397        let mut plaintext: Blocks<2> = Default::default();
398        for (i, x) in plaintext.as_mut_bytes().iter_mut().enumerate() {
399            *x = i as u8;
400        }
401        let mut ciphertext: Blocks<2> = Default::default();
402
403        let src = PtrByteSlice::from(plaintext.as_bytes());
404        let dst = MutPtrByteSlice::from(ciphertext.as_mut_bytes());
405
406        let tweak_val = 0x123456789abcdef0123456789abcdef0u128;
407        let tweak = Tweak::new(tweak_val);
408        let key = 0xffeeddccbbaa99887766554433221100u128;
409
410        let processor = XtsProcessor::new(tweak, src, dst);
411        let cipher = MockCipher::new(key);
412
413        BlockCipherEncClosure::call(processor, &cipher);
414
415        // Verify ciphertext.
416        // Since our mock cipher is just XOR with key, the tweak should cancel out.
417        // C = P ^ K.
418        let expected_c0 =
419            u128::from_le_bytes(plaintext.as_bytes()[0..16].try_into().unwrap()) ^ key;
420        let expected_c1 =
421            u128::from_le_bytes(plaintext.as_bytes()[16..32].try_into().unwrap()) ^ key;
422
423        let actual_c0 = u128::from_le_bytes(ciphertext.as_bytes()[0..16].try_into().unwrap());
424        let actual_c1 = u128::from_le_bytes(ciphertext.as_bytes()[16..32].try_into().unwrap());
425
426        assert_eq!(actual_c0, expected_c0);
427        assert_eq!(actual_c1, expected_c1);
428
429        // Verify recorded blocks (should be P ^ T).
430        assert_eq!(cipher.recorded_blocks.borrow().len(), 2);
431
432        let p0 = u128::from_le_bytes(plaintext.as_bytes()[0..16].try_into().unwrap());
433        let p1 = u128::from_le_bytes(plaintext.as_bytes()[16..32].try_into().unwrap());
434
435        let mut t0 = tweak;
436        assert_eq!(cipher.recorded_blocks.borrow()[0], p0 ^ t0.0);
437        t0.update();
438        assert_eq!(cipher.recorded_blocks.borrow()[1], p1 ^ t0.0);
439    }
440
441    #[test]
442    fn test_xts_in_place() {
443        let mut buf: Blocks<2> = Default::default();
444        for (i, x) in buf.as_mut_bytes().iter_mut().enumerate() {
445            *x = i as u8;
446        }
447
448        let tweak_val = 0x123456789abcdef0123456789abcdef0u128;
449        let tweak = Tweak::new(tweak_val);
450        let key = 0xffeeddccbbaa99887766554433221100u128;
451
452        // Save original plaintext for verification.
453        let p0 = u128::from_le_bytes(buf.as_bytes()[0..16].try_into().unwrap());
454        let p1 = u128::from_le_bytes(buf.as_bytes()[16..32].try_into().unwrap());
455
456        let slice = MutPtrByteSlice::from(buf.as_mut_bytes());
457        let processor = XtsProcessor::new_in_place(tweak, slice);
458        let cipher = MockCipher::new(key);
459
460        BlockCipherEncClosure::call(processor, &cipher);
461
462        // Verify in-place ciphertext.
463        let expected_c0 = p0 ^ key;
464        let expected_c1 = p1 ^ key;
465
466        let actual_c0 = u128::from_le_bytes(buf.as_bytes()[0..16].try_into().unwrap());
467        let actual_c1 = u128::from_le_bytes(buf.as_bytes()[16..32].try_into().unwrap());
468
469        assert_eq!(actual_c0, expected_c0);
470        assert_eq!(actual_c1, expected_c1);
471
472        // Verify recorded blocks.
473        assert_eq!(cipher.recorded_blocks.borrow().len(), 2);
474        let mut t0 = tweak;
475        assert_eq!(cipher.recorded_blocks.borrow()[0], p0 ^ t0.0);
476        t0.update();
477        assert_eq!(cipher.recorded_blocks.borrow()[1], p1 ^ t0.0);
478    }
479
480    #[test_case(16; "exact_block")]
481    #[test_case(17; "one_byte_more_than_block")]
482    #[test_case(31; "one_byte_less_than_two_blocks")]
483    #[test_case(32; "exact_two_blocks")]
484    #[test_case(80; "bunch_of_blocks_exact")]
485    #[test_case(87; "bunch_of_blocks_cts")]
486    fn test_cts_encrypt_decrypt(len: usize) {
487        let tweak = Tweak::new(0x123456789abcdef0123456789abcdef0);
488        let key = 0xffeeddccbbaa99887766554433221100;
489
490        let mut plaintext_vec = vec![0u128; (len + 15) / 16];
491        let plaintext_bytes = plaintext_vec.as_mut_bytes();
492        for (i, b) in plaintext_bytes[..len].iter_mut().enumerate() {
493            *b = ((i % 255) + 1) as u8;
494        }
495        let plaintext = &plaintext_bytes[..len];
496
497        let mut ciphertext_vec = vec![0u128; (len + 15) / 16];
498        let ciphertext_bytes = ciphertext_vec.as_mut_bytes();
499
500        let mut decrypted_vec = vec![0u128; (len + 15) / 16];
501        let decrypted_bytes = decrypted_vec.as_mut_bytes();
502
503        let cipher = MockNonLinearCipher::new(key);
504
505        // Encrypt out-of-place
506        {
507            let src = PtrByteSlice::from(plaintext);
508            let dst = MutPtrByteSlice::from(&mut ciphertext_bytes[..len]);
509            let processor = XtsCtsProcessor::new(tweak, src, dst);
510            BlockCipherEncClosure::call(processor, &cipher);
511        }
512
513        let ciphertext = &ciphertext_bytes[..len];
514
515        // Verify none of the original blocks/content are intact
516        assert_ne!(ciphertext, plaintext, "Ciphertext should not match plaintext");
517        for chunk_start in (0..len).step_by(16) {
518            let chunk_end = (chunk_start + 16).min(len);
519            assert_ne!(
520                &ciphertext[chunk_start..chunk_end],
521                &plaintext[chunk_start..chunk_end],
522                "Chunk at {chunk_start}..{chunk_end} should not match plaintext"
523            );
524        }
525
526        // Decrypt out-of-place
527        {
528            let src = PtrByteSlice::from(ciphertext);
529            let dst = MutPtrByteSlice::from(&mut decrypted_bytes[..len]);
530            let processor = XtsCtsProcessor::new(tweak, src, dst);
531            BlockCipherDecClosure::call(processor, &cipher);
532        }
533
534        let decrypted = &decrypted_bytes[..len];
535
536        // Verify decrypted matches original plaintext
537        assert_eq!(decrypted, plaintext, "Decrypted text should match original plaintext");
538    }
539
540    #[test]
541    fn test_cts_encrypt_decrypt_in_place() {
542        const LEN: usize = 87;
543
544        let tweak = Tweak::new(0x123456789abcdef0123456789abcdef0);
545        let key = 0xffeeddccbbaa99887766554433221100;
546
547        let mut buf_vec = vec![0u128; (LEN + 15) / 16];
548        let buf_bytes = buf_vec.as_mut_bytes();
549        for (i, b) in buf_bytes[..LEN].iter_mut().enumerate() {
550            *b = ((i % 255) + 1) as u8;
551        }
552        let original_plaintext = buf_bytes[..LEN].to_vec();
553
554        let cipher = MockNonLinearCipher::new(key);
555
556        // Encrypt in-place
557        {
558            let slice = MutPtrByteSlice::from(&mut buf_bytes[..LEN]);
559            let processor = XtsCtsProcessor::new_in_place(tweak, slice);
560            BlockCipherEncClosure::call(processor, &cipher);
561        }
562
563        // Verify in-place ciphertext differs from original plaintext
564        assert_ne!(
565            &buf_bytes[..LEN],
566            &original_plaintext[..],
567            "In-place ciphertext should not match plaintext"
568        );
569        for chunk_start in (0..LEN).step_by(16) {
570            let chunk_end = (chunk_start + 16).min(LEN);
571            assert_ne!(
572                &buf_bytes[chunk_start..chunk_end],
573                &original_plaintext[chunk_start..chunk_end],
574                "Chunk at {chunk_start}..{chunk_end} should not match plaintext"
575            );
576        }
577
578        // Decrypt in-place
579        {
580            let slice = MutPtrByteSlice::from(&mut buf_bytes[..LEN]);
581            let processor = XtsCtsProcessor::new_in_place(tweak, slice);
582            BlockCipherDecClosure::call(processor, &cipher);
583        }
584
585        // Verify decrypted matches original plaintext
586        assert_eq!(
587            &buf_bytes[..LEN],
588            &original_plaintext[..],
589            "Decrypted in-place text should match original plaintext"
590        );
591    }
592
593    #[test_case(1; "one_block")]
594    #[test_case(2; "two_blocks")]
595    #[test_case(5; "five_blocks")]
596    fn test_cts_matches_normal_xts_on_exact_blocks(num_blocks: usize) {
597        let tweak = Tweak::new(0x9876543210abcdef9876543210abcdef);
598        let key = 0x0123456789abcdef0123456789abcdef;
599
600        let len = num_blocks * 16;
601        let mut plaintext_vec = vec![0u128; num_blocks];
602        let plaintext_bytes = plaintext_vec.as_mut_bytes();
603        for (i, b) in plaintext_bytes[..len].iter_mut().enumerate() {
604            *b = (i as u8).wrapping_mul(17).wrapping_add(3);
605        }
606        let plaintext = &plaintext_bytes[..len];
607
608        let mut normal_cts_vec = vec![0u128; num_blocks];
609        let mut cts_vec = vec![0u128; num_blocks];
610
611        let cipher = MockNonLinearCipher::new(key);
612
613        // Normal XTS encryption
614        {
615            let src = PtrByteSlice::from(plaintext);
616            let dst = MutPtrByteSlice::from(&mut normal_cts_vec.as_mut_bytes()[..len]);
617            let processor = XtsProcessor::new(tweak, src, dst);
618            BlockCipherEncClosure::call(processor, &cipher);
619        }
620
621        // CTS XTS encryption
622        {
623            let src = PtrByteSlice::from(plaintext);
624            let dst = MutPtrByteSlice::from(&mut cts_vec.as_mut_bytes()[..len]);
625            let processor = XtsCtsProcessor::new(tweak, src, dst);
626            BlockCipherEncClosure::call(processor, &cipher);
627        }
628
629        assert_eq!(
630            normal_cts_vec, cts_vec,
631            "CTS XTS should match normal XTS for {num_blocks} blocks"
632        );
633    }
634
635    #[test]
636    fn test_cts_different_tweaks_different_ciphertext() {
637        const LEN: usize = 87;
638
639        let tweak1 = Tweak::new(0x123456789abcdef0123456789abcdef0);
640        let tweak2 = Tweak::new(0xfeef876543210fedcba9876543210fed);
641        let key = 0xffeeddccbbaa99887766554433221100;
642
643        let mut plaintext_vec = vec![0u128; (LEN + 15) / 16];
644        let plaintext_bytes = plaintext_vec.as_mut_bytes();
645        for (i, b) in plaintext_bytes[..LEN].iter_mut().enumerate() {
646            *b = ((i % 255) + 1) as u8;
647        }
648        let plaintext = &plaintext_bytes[..LEN];
649
650        let mut ciphertext1_vec = vec![0u128; (LEN + 15) / 16];
651        let ciphertext1_bytes = ciphertext1_vec.as_mut_bytes();
652
653        let mut ciphertext2_vec = vec![0u128; (LEN + 15) / 16];
654        let ciphertext2_bytes = ciphertext2_vec.as_mut_bytes();
655
656        let cipher = MockNonLinearCipher::new(key);
657
658        // Encrypt with tweak1
659        {
660            let src = PtrByteSlice::from(plaintext);
661            let dst = MutPtrByteSlice::from(&mut ciphertext1_bytes[..LEN]);
662            let processor = XtsCtsProcessor::new(tweak1, src, dst);
663            BlockCipherEncClosure::call(processor, &cipher);
664        }
665
666        // Encrypt with tweak2
667        {
668            let src = PtrByteSlice::from(plaintext);
669            let dst = MutPtrByteSlice::from(&mut ciphertext2_bytes[..LEN]);
670            let processor = XtsCtsProcessor::new(tweak2, src, dst);
671            BlockCipherEncClosure::call(processor, &cipher);
672        }
673
674        assert_ne!(
675            &ciphertext1_bytes[..LEN],
676            &ciphertext2_bytes[..LEN],
677            "Different tweaks should produce different ciphertexts"
678        );
679    }
680
681    #[test]
682    #[should_panic(expected = "Source and destination lengths must match")]
683    fn test_cts_panic_length_mismatch() {
684        let tweak = Tweak::new(0);
685        let src_buf = vec![0u128; 2];
686        let mut dst_buf = vec![0u128; 3];
687        let src = PtrByteSlice::from(src_buf.as_bytes());
688        let dst = MutPtrByteSlice::from(dst_buf.as_mut_bytes());
689        let _ = XtsCtsProcessor::new(tweak, src, dst);
690    }
691
692    #[test]
693    #[should_panic]
694    fn test_cts_panic_too_short() {
695        let tweak = Tweak::new(0);
696        let src_buf = vec![0u128; 2];
697        let mut dst_buf = vec![0u128; 2];
698        let src = PtrByteSlice::from(&src_buf.as_bytes()[..15]);
699        let dst = MutPtrByteSlice::from(&mut dst_buf.as_mut_bytes()[..15]);
700        let _ = XtsCtsProcessor::new(tweak, src, dst);
701    }
702
703    #[test]
704    #[should_panic(expected = "src must be 16 byte aligned")]
705    fn test_cts_panic_unaligned_src() {
706        let tweak = Tweak::new(0);
707        let src_buf = vec![0u128; 3];
708        let mut dst_buf = vec![0u128; 2];
709        let src = PtrByteSlice::from(&src_buf.as_bytes()[1..17]);
710        let dst = MutPtrByteSlice::from(&mut dst_buf.as_mut_bytes()[..16]);
711        let _ = XtsCtsProcessor::new(tweak, src, dst);
712    }
713
714    #[test]
715    #[should_panic(expected = "dst must be 16 byte aligned")]
716    fn test_cts_panic_unaligned_dst() {
717        let tweak = Tweak::new(0);
718        let src_buf = vec![0u128; 2];
719        let mut dst_buf = vec![0u128; 3];
720        let src = PtrByteSlice::from(&src_buf.as_bytes()[..16]);
721        let dst = MutPtrByteSlice::from(&mut dst_buf.as_mut_bytes()[1..17]);
722        let _ = XtsCtsProcessor::new(tweak, src, dst);
723    }
724}