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, XtsProcessor};
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<(u32, 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<(u32, 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, 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], tweak: u128) -> Result<(), Error> {
279        fxfs_trace::duration!("encrypt", "len" => buffer.len());
280        assert_eq!(buffer.len() % BLOCK_SIZE, 0);
281        let mut tweak = tweak;
282
283        for block in buffer.chunks_exact_mut(BLOCK_SIZE) {
284            self.xts_key2.encrypt_block(tweak.as_mut_bytes().try_into().unwrap());
285            self.xts_key1.encrypt_with_backend(XtsProcessor::new_in_place(
286                Tweak(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            self.xts_key2.encrypt_block(tweak.as_mut_bytes().try_into().unwrap());
299            self.xts_key1.decrypt_with_backend(XtsProcessor::new_in_place(
300                Tweak(tweak),
301                MutPtrByteSlice::from(&mut block[..]),
302            ));
303            tweak += 1;
304        }
305        Ok(())
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::{FscryptInoLblk32DirCipher, UnwrappedKey};
312    use crate::Cipher;
313    use crate::cipher::fscrypt_test_data;
314    use fscrypt::proxy_filename::ProxyFilename;
315    use std::sync::Arc;
316
317    #[test]
318    fn test_encrypt_filename() {
319        let mut unwrapped_key = UnwrappedKey::new([0; 64].to_vec());
320        unwrapped_key.0[0] = 0x10;
321        let cipher: Arc<dyn Cipher> = Arc::new(FscryptInoLblk32DirCipher::new(&unwrapped_key));
322        let object_id = 2;
323
324        // One block case.
325        // ```shell
326        // echo -n filename > in.txt ; truncate -s 16 in.txt
327        // openssl aes-256-cbc -e -iv 014ae2cc000000000000000000000000 -nosalt -K 1000000000000000000000000000000000000000000000000000000000000000  -in in.txt -out out.txt -nopad
328        // hexdump out.txt -e "16/1 \"%02x\" \"\n\"" -v
329        // ```
330        let mut text = b"filename".to_vec();
331        cipher.encrypt_filename(object_id, &mut text).expect("encrypt filename failed");
332        assert_eq!(text, hex::decode("2b7c885165f090393fcbb15f5018f18a").expect("decode failed"));
333
334        // Two block case.
335        // ```shell
336        // echo -n "0123456789abcdef_filename" > in.txt ; truncate -s 16 in.txt
337        // openssl aes-256-cbc -e -iv 014ae2cc000000000000000000000000 -nosalt -K 1000000000000000000000000000000000000000000000000000000000000000  -in in.txt -out out.txt -nopad
338        // hexdump out.txt -e "16/1 \"%02x\" \"\n\"" -v
339        // 3da06c8fc2e54065f391531affeae1fb
340        // d6bad68cc11eb87719735fc50b7efbb3
341        // <Swap the last two blocks and concatenate>
342        // ``````
343        let mut text = b"0123456789abcdef_filename".to_vec();
344        cipher.encrypt_filename(object_id, &mut text).expect("encrypt filename failed");
345        assert_eq!(
346            text,
347            hex::decode("d6bad68cc11eb87719735fc50b7efbb33da06c8fc2e54065f391531affeae1fb")
348                .expect("decode failed")
349        );
350
351        // Test a 192 byte filename -- same as in test image (known to decrypt successfully).
352        // ```shell
353        // export LONG_NAME_16=xxxxxxxxyyyyyyyy
354        // export LONG_NAME_32=${LONG_NAME_16}${LONG_NAME_16}
355        // export LONG_NAME_64=${LONG_NAME_32}${LONG_NAME_32}
356        // export LONG_NAME_128=${LONG_NAME_64}${LONG_NAME_64}
357        // export LONG_NAME_192=${LONG_NAME_128}${LONG_NAME_64}
358        // echo -n "${LONG_NAME_192}" > in.txt
359        // openssl aes-256-cbc -e -iv 014ae2cc000000000000000000000000 -nosalt -K 1000000000000000000000000000000000000000000000000000000000000000  -in in.txt -out out.txt -nopad
360        // hexdump out.txt -e "16/1 \"%02x\" \"\n\"" -v
361        // f59d083c16915d5d3479b9dbf7b7f053
362        // 1905bde71624f4ba1ab416b15831ca87
363        // c2d99e43f97bd2fc18f2ad03da252715
364        // abf9d0cd9bde4215bfeeec7d07dbcf89
365        // 0bcc4a230faaaf73cabdfc3ca8b20a06
366        // 84847f7f3991d55b6b30859dfc662c1a
367        // ef03c7d16830ef7df367a3392a82e588
368        // 629b89feffe49036e420686598545b20
369        // 119c346af4f80fdbd225a625aa0f45ce
370        // 393cfff0bd9971b6782d8768dbd13587
371        // 38e3a65f8ef14612881e6cbd38cf4bcf
372        // 08a75c38d9fb681304fdaa1e85a091ce
373        // <Swap the last two blocks and concatenate>
374        // ``````
375        let long_name_64 = b"xxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyy";
376        let mut text = vec![];
377        for _ in 0..3 {
378            text.extend_from_slice(long_name_64);
379        }
380
381        let raw = hex::decode(concat!(
382            "f59d083c16915d5d3479b9dbf7b7f0531905bde71624f4ba1ab416b15831ca87",
383            "c2d99e43f97bd2fc18f2ad03da252715abf9d0cd9bde4215bfeeec7d07dbcf89",
384            "0bcc4a230faaaf73cabdfc3ca8b20a0684847f7f3991d55b6b30859dfc662c1a",
385            "ef03c7d16830ef7df367a3392a82e588629b89feffe49036e420686598545b20",
386            "119c346af4f80fdbd225a625aa0f45ce393cfff0bd9971b6782d8768dbd13587",
387            "08a75c38d9fb681304fdaa1e85a091ce38e3a65f8ef14612881e6cbd38cf4bcf"
388        ))
389        .expect("decode failed");
390        cipher.encrypt_filename(object_id, &mut text).expect("encrypt filename failed");
391        assert_eq!(text, raw);
392    }
393
394    #[test]
395    fn test_decrypt_filename() {
396        // Should be equivalent to:
397        // ```shell
398        // openssl aes-256-cbc -d -iv 014ae2cc000000000000000000000000 -nosalt -K 1000000000000000000000000000000000000000000000000000000000000000  -in in.txt -out out.txt -nopad
399        // cat in.txt
400        // ```
401        let mut unwrapped_key = UnwrappedKey::new([0; 64].to_vec());
402        unwrapped_key.0[0] = 0x10;
403        let cipher: Arc<dyn Cipher> = Arc::new(FscryptInoLblk32DirCipher::new(&unwrapped_key));
404        let object_id = 2;
405
406        // One block case.
407        let mut text = hex::decode("2b7c885165f090393fcbb15f5018f18a").expect("decode failed");
408        cipher.decrypt_filename(object_id, &mut text).expect("encrypt filename failed");
409        assert_eq!(text, b"filename".to_vec());
410
411        // Two block case.
412        let mut text =
413            hex::decode("d6bad68cc11eb87719735fc50b7efbb33da06c8fc2e54065f391531affeae1fb")
414                .expect("decode failed");
415        cipher.decrypt_filename(object_id, &mut text).expect("encrypt filename failed");
416        assert_eq!(text, b"0123456789abcdef_filename".to_vec());
417    }
418
419    #[test]
420    fn test_generated_filenames() {
421        let cipher: Arc<dyn Cipher> = Arc::new(FscryptInoLblk32DirCipher::new(&UnwrappedKey(
422            fscrypt::to_directory_keys(
423                fscrypt_test_data::KEY,
424                fscrypt_test_data::UUID,
425                fscrypt_test_data::DIR_NONCE,
426            )
427            .to_unwrapped_key(),
428        )));
429
430        for file in fscrypt_test_data::FILES {
431            let mut buffer = file.unencrypted_name.as_bytes().to_vec();
432            cipher.encrypt_filename(fscrypt_test_data::DIR_INODE, &mut buffer).unwrap();
433            let proxy_name = ProxyFilename::new(&buffer);
434            let proxy_name_str: String = proxy_name.into();
435            assert_eq!(
436                proxy_name_str,
437                file.proxy_name,
438                "Proxy name mismatch for (len {}) {}",
439                file.unencrypted_name.len(),
440                file.unencrypted_name
441            );
442            cipher.decrypt_filename(fscrypt_test_data::DIR_INODE, &mut buffer).unwrap();
443            assert_eq!(String::from_utf8(buffer).unwrap(), file.unencrypted_name);
444        }
445    }
446
447    #[test]
448    fn test_generated_casefold_filenames() {
449        let unwrapped = UnwrappedKey(
450            fscrypt::to_directory_keys(
451                fscrypt_test_data::KEY,
452                fscrypt_test_data::UUID,
453                fscrypt_test_data::CASEFOLD_DIR_NONCE,
454            )
455            .to_unwrapped_key(),
456        );
457        let cipher_struct = FscryptInoLblk32DirCipher::new(&unwrapped);
458        let cipher: Arc<dyn Cipher> = Arc::new(cipher_struct);
459
460        for file in fscrypt_test_data::CASEFOLD_FILES {
461            let mut buffer = file.unencrypted_name.as_bytes().to_vec();
462            cipher.encrypt_filename(fscrypt_test_data::CASEFOLD_DIR_INODE, &mut buffer).unwrap();
463
464            let expected_proxy: ProxyFilename = file.proxy_name.try_into().unwrap();
465            let mut hash_code = cipher.hash_code_casefold(file.unencrypted_name);
466            if file.unencrypted_name.len() == 255 {
467                // There's an f2fs bug for filenames that are 255 bytes long.  The bug means that
468                // the name isn't case folded before the hash is computed.  For now, we just copy
469                // f2fs's hash code computation.
470                hash_code = expected_proxy.hash_code as u32;
471            }
472            let actual_proxy = ProxyFilename::new_with_hash_code(hash_code as u64, &buffer);
473
474            assert_eq!(
475                actual_proxy,
476                expected_proxy,
477                "Proxy name mismatch for (len {}) {}",
478                file.unencrypted_name.len(),
479                file.unencrypted_name
480            );
481            cipher.decrypt_filename(fscrypt_test_data::CASEFOLD_DIR_INODE, &mut buffer).unwrap();
482            assert_eq!(String::from_utf8(buffer).unwrap(), file.unencrypted_name);
483        }
484    }
485
486    #[test]
487    fn test_generated_casefold_symlinks() {
488        let unwrapped = UnwrappedKey(
489            fscrypt::to_directory_keys(
490                fscrypt_test_data::KEY,
491                fscrypt_test_data::UUID,
492                fscrypt_test_data::CASEFOLD_DIR_NONCE,
493            )
494            .to_unwrapped_key(),
495        );
496        let cipher_struct = FscryptInoLblk32DirCipher::new(&unwrapped);
497        let cipher: Arc<dyn Cipher> = Arc::new(cipher_struct);
498
499        for file in fscrypt_test_data::SYMLINKS {
500            // Verify symlink target encryption/decryption
501            // Symlink targets are encrypted using the same mechanism as filenames,
502            // using the symlink's own inode as the IV.
503            let mut target_buffer = file.target.as_bytes().to_vec();
504            cipher.encrypt_symlink(file.inode, &mut target_buffer).unwrap();
505
506            let expected_proxy: ProxyFilename =
507                file.encrypted_target_proxy_name.try_into().unwrap();
508            // Symlinks don't have a hash code, so we use 0.
509            let actual_proxy = ProxyFilename::new_with_hash_code(0, &target_buffer);
510
511            assert_eq!(
512                actual_proxy,
513                expected_proxy,
514                "Proxy name mismatch for symlink length {}",
515                file.target.len()
516            );
517
518            cipher.decrypt_symlink(file.inode, &mut target_buffer).unwrap();
519            assert_eq!(
520                String::from_utf8(target_buffer).unwrap(),
521                file.target,
522                "Decrypted target mismatch for symlink {}",
523                file.target
524            );
525        }
526    }
527}