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
39impl<'a, 'b> XtsProcessor<'a, 'b> {
40    // `tweak` should be encrypted. `src` and `dst` must have the same length and be 16 byte
41    // aligned.
42    pub fn new(tweak: Tweak, src: PtrByteSlice<'a>, dst: MutPtrByteSlice<'b>) -> Self {
43        assert_eq!(src.len(), dst.len(), "Source and destination lengths must match");
44        assert!(src.as_ptr().cast::<u128>().is_aligned(), "src must be 16 byte aligned");
45        assert!(dst.as_ptr().cast::<u128>().is_aligned(), "dst must be 16 byte aligned");
46        Self { tweak, src, dst }
47    }
48
49    // Creates an XtsProcessor for in-place operation on a single buffer.
50    pub fn new_in_place(tweak: Tweak, buf: MutPtrByteSlice<'a>) -> XtsProcessor<'a, 'a> {
51        assert!(buf.as_ptr().cast::<u128>().is_aligned(), "buf must be 16 byte aligned");
52        let len = buf.len();
53        let ptr = buf.as_ptr_slice().as_ptr();
54        // SAFETY: We are creating a PtrByteSlice that aliases with the MutPtrByteSlice.
55        // This is safe because PtrByteSlice only allows read access, and we control the
56        // execution in `call` to ensure we don't violate safety (we read a block, then write it,
57        // so we don't have concurrent read/write on the same sub-block).
58        let src = unsafe { PtrByteSlice::new(std::ptr::slice_from_raw_parts(ptr, len)) };
59        XtsProcessor { tweak, src, dst: buf }
60    }
61}
62
63impl BlockSizeUser for XtsProcessor<'_, '_> {
64    type BlockSize = U16;
65}
66
67impl BlockCipherEncClosure for XtsProcessor<'_, '_> {
68    fn call<B: BlockCipherEncBackend<BlockSize = Self::BlockSize>>(self, backend: &B) {
69        let Self { mut tweak, src, mut dst } = self;
70        let src_chunks = src.chunks::<u128>();
71        let dst_chunks = dst.chunks_mut::<u128>();
72
73        for (src_chunk, dst_chunk) in src_chunks.zip(dst_chunks) {
74            let mut val = src_chunk.read();
75
76            // XOR plaintext with tweak.
77            val ^= tweak.0;
78
79            let arr: &mut Array<u8, U16> = val.as_mut_bytes().try_into().unwrap();
80            backend.encrypt_block(InOut::from(arr));
81
82            // XOR ciphertext with tweak.
83            val ^= tweak.0;
84
85            dst_chunk.write(val);
86
87            tweak.update();
88        }
89    }
90}
91
92impl BlockCipherDecClosure for XtsProcessor<'_, '_> {
93    fn call<B: BlockCipherDecBackend<BlockSize = Self::BlockSize>>(self, backend: &B) {
94        let Self { mut tweak, src, mut dst } = self;
95        let src_chunks = src.chunks::<u128>();
96        let dst_chunks = dst.chunks_mut::<u128>();
97
98        for (src_chunk, dst_chunk) in src_chunks.zip(dst_chunks) {
99            let mut val = src_chunk.read();
100
101            // XOR ciphertext with tweak.
102            val ^= tweak.0;
103
104            let arr: &mut Array<u8, U16> = val.as_mut_bytes().try_into().unwrap();
105            backend.decrypt_block(InOut::from(arr));
106
107            // XOR plaintext with tweak.
108            val ^= tweak.0;
109
110            dst_chunk.write(val);
111
112            tweak.update();
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use cipher::inout::InOut;
121    use cipher::typenum::consts::U1;
122    use cipher::{Block, ParBlocksSizeUser};
123    use std::cell::RefCell;
124
125    struct MockCipher {
126        recorded_blocks: RefCell<Vec<u128>>,
127        key: u128,
128    }
129
130    impl MockCipher {
131        fn new(key: u128) -> Self {
132            Self { recorded_blocks: RefCell::new(Vec::new()), key }
133        }
134    }
135
136    impl BlockSizeUser for MockCipher {
137        type BlockSize = U16;
138    }
139
140    impl ParBlocksSizeUser for MockCipher {
141        type ParBlocksSize = U1;
142    }
143
144    impl BlockCipherEncBackend for MockCipher {
145        fn encrypt_block(&self, mut block: InOut<'_, '_, Block<Self>>) {
146            // SAFETY: Block<Self> is Array<u8, U16>, which is 16 bytes.
147            let mut val =
148                unsafe { std::ptr::read_unaligned(block.get_in().as_ptr() as *const u128) };
149            self.recorded_blocks.borrow_mut().push(val);
150            val ^= self.key;
151            // SAFETY: Block<Self> is Array<u8, U16>, which is 16 bytes.
152            unsafe {
153                std::ptr::write_unaligned(block.get_out().as_mut_ptr() as *mut u128, val);
154            }
155        }
156    }
157
158    #[repr(C)]
159    #[derive(FromBytes, IntoBytes, Immutable)]
160    struct Blocks<const N: usize>([u128; N]);
161
162    static_assertions::const_assert!(std::mem::align_of::<Blocks<1>>() == 16);
163    static_assertions::const_assert!(std::mem::align_of::<Blocks<2>>() == 16);
164
165    impl<const N: usize> Default for Blocks<N> {
166        fn default() -> Self {
167            Self([0u128; N])
168        }
169    }
170
171    #[test]
172    fn test_xts_out_of_place() {
173        let mut plaintext: Blocks<2> = Default::default();
174        for (i, x) in plaintext.as_mut_bytes().iter_mut().enumerate() {
175            *x = i as u8;
176        }
177        let mut ciphertext: Blocks<2> = Default::default();
178
179        let src = PtrByteSlice::from(plaintext.as_bytes());
180        let dst = MutPtrByteSlice::from(ciphertext.as_mut_bytes());
181
182        let tweak_val = 0x123456789abcdef0123456789abcdef0u128;
183        let tweak = Tweak::new(tweak_val);
184        let key = 0xffeeddccbbaa99887766554433221100u128;
185
186        let processor = XtsProcessor::new(tweak, src, dst);
187        let cipher = MockCipher::new(key);
188
189        BlockCipherEncClosure::call(processor, &cipher);
190
191        // Verify ciphertext.
192        // Since our mock cipher is just XOR with key, the tweak should cancel out.
193        // C = P ^ K.
194        let expected_c0 =
195            u128::from_le_bytes(plaintext.as_bytes()[0..16].try_into().unwrap()) ^ key;
196        let expected_c1 =
197            u128::from_le_bytes(plaintext.as_bytes()[16..32].try_into().unwrap()) ^ key;
198
199        let actual_c0 = u128::from_le_bytes(ciphertext.as_bytes()[0..16].try_into().unwrap());
200        let actual_c1 = u128::from_le_bytes(ciphertext.as_bytes()[16..32].try_into().unwrap());
201
202        assert_eq!(actual_c0, expected_c0);
203        assert_eq!(actual_c1, expected_c1);
204
205        // Verify recorded blocks (should be P ^ T).
206        assert_eq!(cipher.recorded_blocks.borrow().len(), 2);
207
208        let p0 = u128::from_le_bytes(plaintext.as_bytes()[0..16].try_into().unwrap());
209        let p1 = u128::from_le_bytes(plaintext.as_bytes()[16..32].try_into().unwrap());
210
211        let mut t0 = tweak;
212        assert_eq!(cipher.recorded_blocks.borrow()[0], p0 ^ t0.0);
213        t0.update();
214        assert_eq!(cipher.recorded_blocks.borrow()[1], p1 ^ t0.0);
215    }
216
217    #[test]
218    fn test_xts_in_place() {
219        let mut buf: Blocks<2> = Default::default();
220        for (i, x) in buf.as_mut_bytes().iter_mut().enumerate() {
221            *x = i as u8;
222        }
223
224        let tweak_val = 0x123456789abcdef0123456789abcdef0u128;
225        let tweak = Tweak::new(tweak_val);
226        let key = 0xffeeddccbbaa99887766554433221100u128;
227
228        // Save original plaintext for verification.
229        let p0 = u128::from_le_bytes(buf.as_bytes()[0..16].try_into().unwrap());
230        let p1 = u128::from_le_bytes(buf.as_bytes()[16..32].try_into().unwrap());
231
232        let slice = MutPtrByteSlice::from(buf.as_mut_bytes());
233        let processor = XtsProcessor::new_in_place(tweak, slice);
234        let cipher = MockCipher::new(key);
235
236        BlockCipherEncClosure::call(processor, &cipher);
237
238        // Verify in-place ciphertext.
239        let expected_c0 = p0 ^ key;
240        let expected_c1 = p1 ^ key;
241
242        let actual_c0 = u128::from_le_bytes(buf.as_bytes()[0..16].try_into().unwrap());
243        let actual_c1 = u128::from_le_bytes(buf.as_bytes()[16..32].try_into().unwrap());
244
245        assert_eq!(actual_c0, expected_c0);
246        assert_eq!(actual_c1, expected_c1);
247
248        // Verify recorded blocks.
249        assert_eq!(cipher.recorded_blocks.borrow().len(), 2);
250        let mut t0 = tweak;
251        assert_eq!(cipher.recorded_blocks.borrow()[0], p0 ^ t0.0);
252        t0.update();
253        assert_eq!(cipher.recorded_blocks.borrow()[1], p1 ^ t0.0);
254    }
255}