Skip to main content

fxfs_crypto/cipher/
fscrypt_ino_lblk32.rs

1// Copyright 2025 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 super::{Cipher, Tweak, UnwrappedKey, XtsInPlaceProcessor};
6use aes::Aes256;
7use aes::cipher::inout::InOutBuf;
8use aes::cipher::{
9    Block, BlockCipherDecrypt, BlockCipherEncrypt, BlockModeDecrypt, BlockModeEncrypt, KeyInit,
10    KeyIvInit,
11};
12use anyhow::{Context, Error, ensure};
13use siphasher::sip::SipHasher;
14use std::hash::Hasher;
15use storage_ptr_slice::MutPtrByteSlice;
16use zerocopy::IntoBytes;
17
18const BLOCK_SIZE: usize = 4096;
19const MAX_FILENAME_LEN: usize = 255;
20const MAX_SYMLINK_LEN: usize = 4093;
21const NAME_PADDING: usize = 16;
22
23#[derive(Debug)]
24pub(crate) struct FscryptInoLblk32DirCipher {
25    cts_key: [u8; 32],
26    ino_hash_key: [u8; 16],
27    dir_hash_key: [u8; 16],
28}
29impl FscryptInoLblk32DirCipher {
30    pub fn new(key: &UnwrappedKey) -> Self {
31        Self {
32            cts_key: key[..32].try_into().unwrap(),
33            ino_hash_key: key[32..48].try_into().unwrap(),
34            dir_hash_key: key[48..64].try_into().unwrap(),
35        }
36    }
37}
38impl Cipher for FscryptInoLblk32DirCipher {
39    fn encrypt(
40        &self,
41        _ino: u64,
42        _attribute_id: u64,
43        _device_offset: u64,
44        _file_offset: u64,
45        _buffer: MutPtrByteSlice<'_>,
46    ) -> Result<(), Error> {
47        Err(zx_status::Status::NOT_SUPPORTED).context("encrypt not supported for InoLblk32Dir")
48    }
49
50    fn decrypt(
51        &self,
52        _ino: u64,
53        _attribute_id: u64,
54        _device_offset: u64,
55        _file_offset: u64,
56        _buffer: MutPtrByteSlice<'_>,
57    ) -> Result<(), Error> {
58        Err(zx_status::Status::NOT_SUPPORTED).context("decrypt not supported for InoLblk32Dir")
59    }
60
61    fn encrypt_filename(&self, object_id: u64, buffer: &mut Vec<u8>) -> Result<(), Error> {
62        self.encrypt_filename_with_max_len(object_id, buffer, MAX_FILENAME_LEN)
63    }
64
65    fn decrypt_filename(&self, object_id: u64, buffer: &mut Vec<u8>) -> Result<(), Error> {
66        self.decrypt_filename_with_max_len(object_id, buffer, MAX_FILENAME_LEN)
67    }
68
69    fn encrypt_symlink(&self, object_id: u64, buffer: &mut Vec<u8>) -> Result<(), Error> {
70        self.encrypt_filename_with_max_len(object_id, buffer, MAX_SYMLINK_LEN)
71    }
72
73    fn decrypt_symlink(&self, object_id: u64, buffer: &mut Vec<u8>) -> Result<(), Error> {
74        self.decrypt_filename_with_max_len(object_id, buffer, MAX_SYMLINK_LEN)
75    }
76
77    fn hash_code(&self, _raw_filename: &[u8], _filename: &str) -> Option<u32> {
78        None
79    }
80
81    fn hash_code_casefold(&self, filename: &str) -> u32 {
82        fscrypt::direntry::casefold_encrypt_hash_filename(filename.into(), &self.dir_hash_key)
83    }
84
85    fn supports_inline_encryption(&self) -> bool {
86        false
87    }
88
89    fn crypt_ctx(&self, _ino: u64, _attribute_id: u64, _file_offset: u64) -> Option<(u64, u8)> {
90        None
91    }
92}
93
94impl FscryptInoLblk32DirCipher {
95    fn encrypt_filename_with_max_len(
96        &self,
97        object_id: u64,
98        buffer: &mut Vec<u8>,
99        max_len: usize,
100    ) -> Result<(), Error> {
101        ensure!(buffer.len() <= max_len, "Filename too long");
102
103        let mut hasher = SipHasher::new_with_key(&self.ino_hash_key);
104        hasher.write(object_id.as_bytes());
105        let iv = [hasher.finish() as u32, 0, 0, 0];
106
107        buffer.resize(buffer.len().next_multiple_of(NAME_PADDING), 0);
108
109        let mut cbc = cbc::Encryptor::<aes::Aes256>::new(
110            (&self.cts_key).try_into().unwrap(),
111            iv.as_bytes().try_into().unwrap(),
112        );
113        let inout = InOutBuf::<'_, '_, u8>::from(&mut buffer[..]);
114        let (mut blocks, _): (InOutBuf<'_, '_, Block<aes::Aes256>>, _) = inout.into_chunks();
115        let mut chunks = blocks.get_out();
116        cbc.encrypt_blocks(&mut chunks);
117        if chunks.len() >= 2 {
118            // We are encrypting with CTS.  In most cases, the padding will mean it's a multiple of
119            // NAME_PADDING bytes, so all we need to do is swap the last two chunks.  There is one
120            // exception: when the filename ends up being longer than max_len after padding.  In
121            // that case, all we have to do is trim the end after swapping the last two chunks.
122            chunks.swap(chunks.len() - 1, chunks.len() - 2);
123            buffer.truncate(max_len);
124        }
125        Ok(())
126    }
127
128    fn decrypt_filename_with_max_len(
129        &self,
130        object_id: u64,
131        buffer: &mut Vec<u8>,
132        max_len: usize,
133    ) -> Result<(), Error> {
134        let alignment = buffer.len() % NAME_PADDING;
135        if alignment != 0 {
136            // For CTS, the only case we need to care about is when the encrypted filename is
137            // max_len bytes. In all other cases, the filename should be a multiple of NAME_PADDING
138            // bytes.
139            ensure!(buffer.len() == max_len, "Unexpected filename length");
140
141            // Decrypt the second to last block.
142            let cipher = aes::Aes256::new((&self.cts_key).try_into().unwrap());
143            let mut out: Block<aes::Aes256> =
144                buffer[max_len - alignment - NAME_PADDING..max_len - alignment].try_into().unwrap();
145            cipher.decrypt_block(&mut out);
146
147            // Copy the extra bytes we need.
148            buffer.extend_from_slice(&out[alignment..]);
149        }
150
151        let mut hasher = SipHasher::new_with_key(&self.ino_hash_key);
152        hasher.write(object_id.as_bytes());
153        let iv = [hasher.finish() as u32, 0, 0, 0];
154
155        let mut cbc = cbc::Decryptor::<aes::Aes256>::new(
156            (&self.cts_key).try_into().unwrap(),
157            iv.as_bytes().try_into().unwrap(),
158        );
159        let inout = InOutBuf::<'_, '_, u8>::from(&mut buffer[..]);
160        let (mut blocks, _): (InOutBuf<'_, '_, Block<aes::Aes256>>, _) = inout.into_chunks();
161        let mut chunks = blocks.get_out();
162        if chunks.len() >= 2 {
163            chunks.swap(chunks.len() - 1, chunks.len() - 2);
164        }
165        cbc.decrypt_blocks(&mut chunks);
166
167        // Strip padding
168        while let Some(0) = buffer.last() {
169            buffer.pop();
170        }
171        Ok(())
172    }
173}
174
175#[derive(Debug)]
176pub(super) struct FscryptInoLblk32FileCipher {
177    slot: u8,
178    ino_hash_key: [u8; 16],
179}
180
181impl FscryptInoLblk32FileCipher {
182    pub fn new(key: &UnwrappedKey) -> Self {
183        Self { slot: key[0], ino_hash_key: key[1..17].try_into().unwrap() }
184    }
185
186    #[inline(always)]
187    fn tweak(&self, ino: u64, block_num: u64) -> u32 {
188        let mut hasher = SipHasher::new_with_key(&self.ino_hash_key);
189        hasher.write(ino.as_bytes());
190        (hasher.finish().wrapping_add(block_num)) as u32
191    }
192}
193
194// TODO(https://fxbug.dev/436902004): Remove encrypt/decrypt support once this cipher supports
195// inline encryption.
196impl Cipher for FscryptInoLblk32FileCipher {
197    fn encrypt(
198        &self,
199        _ino: u64,
200        _attribute_id: u64,
201        _device_offset: u64,
202        _file_offset: u64,
203        _buffer: MutPtrByteSlice<'_>,
204    ) -> Result<(), Error> {
205        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
206        Err(e.context("encrypt not supported for InoLblk32File"))
207    }
208
209    fn decrypt(
210        &self,
211        _ino: u64,
212        _attribute_id: u64,
213        _device_offset: u64,
214        _file_offset: u64,
215        _buffer: MutPtrByteSlice<'_>,
216    ) -> Result<(), Error> {
217        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
218        Err(e.context("decrypt not supported for InoLblk32File"))
219    }
220
221    fn encrypt_filename(&self, _object_id: u64, _buffer: &mut Vec<u8>) -> Result<(), Error> {
222        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
223        Err(e.context("encrypt_filename not supported for InoLblk32File"))
224    }
225
226    fn decrypt_filename(&self, _object_id: u64, _buffer: &mut Vec<u8>) -> Result<(), Error> {
227        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
228        Err(e.context("decrypt_filename not supported for InoLblk32File"))
229    }
230
231    fn encrypt_symlink(&self, _object_id: u64, _buffer: &mut Vec<u8>) -> Result<(), Error> {
232        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
233        Err(e.context("encrypt_symlink not supported for InoLblk32File"))
234    }
235
236    fn decrypt_symlink(&self, _object_id: u64, _buffer: &mut Vec<u8>) -> Result<(), Error> {
237        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
238        Err(e.context("decrypt_symlink not supported for InoLblk32File"))
239    }
240
241    fn hash_code(&self, _raw_filename: &[u8], _filename: &str) -> Option<u32> {
242        debug_assert!(false, "hash_code called on file cipher");
243        None
244    }
245
246    fn hash_code_casefold(&self, _filename: &str) -> u32 {
247        debug_assert!(false, "hash_code_casefold called on file cipher");
248        0
249    }
250
251    fn supports_inline_encryption(&self) -> bool {
252        true
253    }
254
255    fn crypt_ctx(&self, ino: u64, _attribute_id: u64, file_offset: u64) -> Option<(u64, u8)> {
256        assert_eq!(file_offset % BLOCK_SIZE as u64, 0);
257        let block_num = file_offset / BLOCK_SIZE as u64;
258        let tweak = self.tweak(ino, block_num);
259        Some((tweak as u64, self.slot))
260    }
261}
262
263// Software-fallback for the lblk32 file cipher.
264#[derive(Debug)]
265pub struct FscryptSoftwareInoLblk32FileCipher {
266    xts_key1: Aes256,
267    xts_key2: Aes256,
268}
269
270impl FscryptSoftwareInoLblk32FileCipher {
271    pub fn new(key: &UnwrappedKey) -> Self {
272        Self {
273            xts_key1: Aes256::new((&key[..32]).try_into().unwrap()),
274            xts_key2: Aes256::new((&key[32..64]).try_into().unwrap()),
275        }
276    }
277
278    pub fn encrypt(&self, buffer: &mut [u8], mut tweak: u128) -> Result<(), Error> {
279        fxfs_trace::duration!("encrypt", "len" => buffer.len());
280        assert_eq!(buffer.len() % BLOCK_SIZE, 0);
281
282        for block in buffer.chunks_exact_mut(BLOCK_SIZE) {
283            let mut block_tweak = tweak;
284            self.xts_key2.encrypt_block(block_tweak.as_mut_bytes().try_into().unwrap());
285            self.xts_key1.encrypt_with_backend(XtsInPlaceProcessor::new(
286                Tweak(block_tweak),
287                MutPtrByteSlice::from(&mut block[..]),
288            ));
289            tweak += 1;
290        }
291        Ok(())
292    }
293
294    pub fn decrypt(&self, buffer: &mut [u8], mut tweak: u128) -> Result<(), Error> {
295        fxfs_trace::duration!("decrypt", "len" => buffer.len());
296        assert_eq!(buffer.len() % BLOCK_SIZE, 0);
297        for block in buffer.chunks_exact_mut(BLOCK_SIZE) {
298            let mut block_tweak = tweak;
299            self.xts_key2.encrypt_block(block_tweak.as_mut_bytes().try_into().unwrap());
300            self.xts_key1.decrypt_with_backend(XtsInPlaceProcessor::new(
301                Tweak(block_tweak),
302                MutPtrByteSlice::from(&mut block[..]),
303            ));
304            tweak += 1;
305        }
306        Ok(())
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::{
313        BLOCK_SIZE, FscryptInoLblk32DirCipher, FscryptSoftwareInoLblk32FileCipher, UnwrappedKey,
314    };
315    use crate::Cipher;
316    use crate::cipher::fscrypt_test_data;
317    use fscrypt::proxy_filename::ProxyFilename;
318    use std::sync::Arc;
319
320    #[test]
321    fn test_encrypt_filename() {
322        let mut unwrapped_key = UnwrappedKey::new([0; 64].to_vec());
323        unwrapped_key.0[0] = 0x10;
324        let cipher: Arc<dyn Cipher> = Arc::new(FscryptInoLblk32DirCipher::new(&unwrapped_key));
325        let object_id = 2;
326
327        // One block case.
328        // ```shell
329        // echo -n filename > in.txt ; truncate -s 16 in.txt
330        // openssl aes-256-cbc -e -iv 014ae2cc000000000000000000000000 -nosalt -K 1000000000000000000000000000000000000000000000000000000000000000  -in in.txt -out out.txt -nopad
331        // hexdump out.txt -e "16/1 \"%02x\" \"\n\"" -v
332        // ```
333        let mut text = b"filename".to_vec();
334        cipher.encrypt_filename(object_id, &mut text).expect("encrypt filename failed");
335        assert_eq!(text, hex::decode("2b7c885165f090393fcbb15f5018f18a").expect("decode failed"));
336
337        // Two block case.
338        // ```shell
339        // echo -n "0123456789abcdef_filename" > in.txt ; truncate -s 16 in.txt
340        // openssl aes-256-cbc -e -iv 014ae2cc000000000000000000000000 -nosalt -K 1000000000000000000000000000000000000000000000000000000000000000  -in in.txt -out out.txt -nopad
341        // hexdump out.txt -e "16/1 \"%02x\" \"\n\"" -v
342        // 3da06c8fc2e54065f391531affeae1fb
343        // d6bad68cc11eb87719735fc50b7efbb3
344        // <Swap the last two blocks and concatenate>
345        // ``````
346        let mut text = b"0123456789abcdef_filename".to_vec();
347        cipher.encrypt_filename(object_id, &mut text).expect("encrypt filename failed");
348        assert_eq!(
349            text,
350            hex::decode("d6bad68cc11eb87719735fc50b7efbb33da06c8fc2e54065f391531affeae1fb")
351                .expect("decode failed")
352        );
353
354        // Test a 192 byte filename -- same as in test image (known to decrypt successfully).
355        // ```shell
356        // export LONG_NAME_16=xxxxxxxxyyyyyyyy
357        // export LONG_NAME_32=${LONG_NAME_16}${LONG_NAME_16}
358        // export LONG_NAME_64=${LONG_NAME_32}${LONG_NAME_32}
359        // export LONG_NAME_128=${LONG_NAME_64}${LONG_NAME_64}
360        // export LONG_NAME_192=${LONG_NAME_128}${LONG_NAME_64}
361        // echo -n "${LONG_NAME_192}" > in.txt
362        // openssl aes-256-cbc -e -iv 014ae2cc000000000000000000000000 -nosalt -K 1000000000000000000000000000000000000000000000000000000000000000  -in in.txt -out out.txt -nopad
363        // hexdump out.txt -e "16/1 \"%02x\" \"\n\"" -v
364        // f59d083c16915d5d3479b9dbf7b7f053
365        // 1905bde71624f4ba1ab416b15831ca87
366        // c2d99e43f97bd2fc18f2ad03da252715
367        // abf9d0cd9bde4215bfeeec7d07dbcf89
368        // 0bcc4a230faaaf73cabdfc3ca8b20a06
369        // 84847f7f3991d55b6b30859dfc662c1a
370        // ef03c7d16830ef7df367a3392a82e588
371        // 629b89feffe49036e420686598545b20
372        // 119c346af4f80fdbd225a625aa0f45ce
373        // 393cfff0bd9971b6782d8768dbd13587
374        // 38e3a65f8ef14612881e6cbd38cf4bcf
375        // 08a75c38d9fb681304fdaa1e85a091ce
376        // <Swap the last two blocks and concatenate>
377        // ``````
378        let long_name_64 = b"xxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyy";
379        let mut text = vec![];
380        for _ in 0..3 {
381            text.extend_from_slice(long_name_64);
382        }
383
384        let raw = hex::decode(concat!(
385            "f59d083c16915d5d3479b9dbf7b7f0531905bde71624f4ba1ab416b15831ca87",
386            "c2d99e43f97bd2fc18f2ad03da252715abf9d0cd9bde4215bfeeec7d07dbcf89",
387            "0bcc4a230faaaf73cabdfc3ca8b20a0684847f7f3991d55b6b30859dfc662c1a",
388            "ef03c7d16830ef7df367a3392a82e588629b89feffe49036e420686598545b20",
389            "119c346af4f80fdbd225a625aa0f45ce393cfff0bd9971b6782d8768dbd13587",
390            "08a75c38d9fb681304fdaa1e85a091ce38e3a65f8ef14612881e6cbd38cf4bcf"
391        ))
392        .expect("decode failed");
393        cipher.encrypt_filename(object_id, &mut text).expect("encrypt filename failed");
394        assert_eq!(text, raw);
395    }
396
397    #[test]
398    fn test_decrypt_filename() {
399        // Should be equivalent to:
400        // ```shell
401        // openssl aes-256-cbc -d -iv 014ae2cc000000000000000000000000 -nosalt -K 1000000000000000000000000000000000000000000000000000000000000000  -in in.txt -out out.txt -nopad
402        // cat in.txt
403        // ```
404        let mut unwrapped_key = UnwrappedKey::new([0; 64].to_vec());
405        unwrapped_key.0[0] = 0x10;
406        let cipher: Arc<dyn Cipher> = Arc::new(FscryptInoLblk32DirCipher::new(&unwrapped_key));
407        let object_id = 2;
408
409        // One block case.
410        let mut text = hex::decode("2b7c885165f090393fcbb15f5018f18a").expect("decode failed");
411        cipher.decrypt_filename(object_id, &mut text).expect("encrypt filename failed");
412        assert_eq!(text, b"filename".to_vec());
413
414        // Two block case.
415        let mut text =
416            hex::decode("d6bad68cc11eb87719735fc50b7efbb33da06c8fc2e54065f391531affeae1fb")
417                .expect("decode failed");
418        cipher.decrypt_filename(object_id, &mut text).expect("encrypt filename failed");
419        assert_eq!(text, b"0123456789abcdef_filename".to_vec());
420    }
421
422    #[test]
423    fn test_generated_filenames() {
424        let cipher: Arc<dyn Cipher> = Arc::new(FscryptInoLblk32DirCipher::new(&UnwrappedKey(
425            fscrypt::to_directory_keys(
426                fscrypt_test_data::KEY,
427                fscrypt_test_data::UUID,
428                fscrypt_test_data::DIR_NONCE,
429            )
430            .to_unwrapped_key(),
431        )));
432
433        for file in fscrypt_test_data::FILES {
434            let mut buffer = file.unencrypted_name.as_bytes().to_vec();
435            cipher.encrypt_filename(fscrypt_test_data::DIR_INODE, &mut buffer).unwrap();
436            let proxy_name = ProxyFilename::new(&buffer);
437            let proxy_name_str: String = proxy_name.into();
438            assert_eq!(
439                proxy_name_str,
440                file.proxy_name,
441                "Proxy name mismatch for (len {}) {}",
442                file.unencrypted_name.len(),
443                file.unencrypted_name
444            );
445            cipher.decrypt_filename(fscrypt_test_data::DIR_INODE, &mut buffer).unwrap();
446            assert_eq!(String::from_utf8(buffer).unwrap(), file.unencrypted_name);
447        }
448    }
449
450    #[test]
451    fn test_generated_casefold_filenames() {
452        let unwrapped = UnwrappedKey(
453            fscrypt::to_directory_keys(
454                fscrypt_test_data::KEY,
455                fscrypt_test_data::UUID,
456                fscrypt_test_data::CASEFOLD_DIR_NONCE,
457            )
458            .to_unwrapped_key(),
459        );
460        let cipher_struct = FscryptInoLblk32DirCipher::new(&unwrapped);
461        let cipher: Arc<dyn Cipher> = Arc::new(cipher_struct);
462
463        for file in fscrypt_test_data::CASEFOLD_FILES {
464            let mut buffer = file.unencrypted_name.as_bytes().to_vec();
465            cipher.encrypt_filename(fscrypt_test_data::CASEFOLD_DIR_INODE, &mut buffer).unwrap();
466
467            let expected_proxy: ProxyFilename = file.proxy_name.try_into().unwrap();
468            let mut hash_code = cipher.hash_code_casefold(file.unencrypted_name);
469            if file.unencrypted_name.len() == 255 {
470                // There's an f2fs bug for filenames that are 255 bytes long.  The bug means that
471                // the name isn't case folded before the hash is computed.  For now, we just copy
472                // f2fs's hash code computation.
473                hash_code = expected_proxy.hash_code as u32;
474            }
475            let actual_proxy = ProxyFilename::new_with_hash_code(hash_code as u64, &buffer);
476
477            assert_eq!(
478                actual_proxy,
479                expected_proxy,
480                "Proxy name mismatch for (len {}) {}",
481                file.unencrypted_name.len(),
482                file.unencrypted_name
483            );
484            cipher.decrypt_filename(fscrypt_test_data::CASEFOLD_DIR_INODE, &mut buffer).unwrap();
485            assert_eq!(String::from_utf8(buffer).unwrap(), file.unencrypted_name);
486        }
487    }
488
489    #[test]
490    fn test_generated_casefold_symlinks() {
491        let unwrapped = UnwrappedKey(
492            fscrypt::to_directory_keys(
493                fscrypt_test_data::KEY,
494                fscrypt_test_data::UUID,
495                fscrypt_test_data::CASEFOLD_DIR_NONCE,
496            )
497            .to_unwrapped_key(),
498        );
499        let cipher_struct = FscryptInoLblk32DirCipher::new(&unwrapped);
500        let cipher: Arc<dyn Cipher> = Arc::new(cipher_struct);
501
502        for file in fscrypt_test_data::SYMLINKS {
503            // Verify symlink target encryption/decryption
504            // Symlink targets are encrypted using the same mechanism as filenames,
505            // using the symlink's own inode as the IV.
506            let mut target_buffer = file.target.as_bytes().to_vec();
507            cipher.encrypt_symlink(file.inode, &mut target_buffer).unwrap();
508
509            let expected_proxy: ProxyFilename =
510                file.encrypted_target_proxy_name.try_into().unwrap();
511            // Symlinks don't have a hash code, so we use 0.
512            let actual_proxy = ProxyFilename::new_with_hash_code(0, &target_buffer);
513
514            assert_eq!(
515                actual_proxy,
516                expected_proxy,
517                "Proxy name mismatch for symlink length {}",
518                file.target.len()
519            );
520
521            cipher.decrypt_symlink(file.inode, &mut target_buffer).unwrap();
522            assert_eq!(
523                String::from_utf8(target_buffer).unwrap(),
524                file.target,
525                "Decrypted target mismatch for symlink {}",
526                file.target
527            );
528        }
529    }
530
531    #[test]
532    fn test_software_file_cipher_multi_block() {
533        let key = UnwrappedKey::new((0..64).collect());
534        let cipher = FscryptSoftwareInoLblk32FileCipher::new(&key);
535        let base_tweak: u128 = 0x1234_5678;
536
537        // Create a 3-block buffer with distinct data per block.
538        let mut multi_block_buf = Vec::with_capacity(3 * BLOCK_SIZE);
539        for i in 0..3u8 {
540            multi_block_buf.extend(std::iter::repeat_n(i + 1, BLOCK_SIZE));
541        }
542        let original_plaintext = multi_block_buf.clone();
543
544        // Encrypt all 3 blocks in a single call.
545        cipher.encrypt(&mut multi_block_buf, base_tweak).expect("multi-block encrypt failed");
546
547        // Encrypting each block individually with its respective tweak (base_tweak + i) must
548        // produce identical ciphertext for every block. Note that testing encrypt followed by
549        // decrypt on the same multi-block buffer would NOT catch a bug where both encrypt and
550        // decrypt compute the wrong tweak sequence across loop iterations.
551        for i in 0..3 {
552            let mut single_block =
553                original_plaintext[i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE].to_vec();
554            cipher
555                .encrypt(&mut single_block, base_tweak + i as u128)
556                .expect("single-block encrypt failed");
557            assert_eq!(
558                &multi_block_buf[i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE],
559                &single_block[..],
560                "Ciphertext mismatch at block {i}"
561            );
562        }
563
564        // Verify that decrypting each block individually from the multi-block ciphertext restores
565        // the original plaintext.
566        for i in 0..3 {
567            let mut single_block = multi_block_buf[i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE].to_vec();
568            cipher
569                .decrypt(&mut single_block, base_tweak + i as u128)
570                .expect("single-block decrypt failed");
571            assert_eq!(
572                &single_block[..],
573                &original_plaintext[i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE],
574                "Single-block decrypt mismatch at block {i}"
575            );
576        }
577
578        // Verify multi-block decrypt restores all blocks at once.
579        cipher.decrypt(&mut multi_block_buf, base_tweak).expect("multi-block decrypt failed");
580        assert_eq!(multi_block_buf, original_plaintext);
581    }
582}