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