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