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::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        fscrypt::direntry::casefold_encrypt_hash_filename(filename.into(), &self.dir_hash_key)
80    }
81
82    fn supports_inline_encryption(&self) -> bool {
83        false
84    }
85
86    fn crypt_ctx(&self, _ino: u64, _file_offset: u64) -> Option<(u32, u8)> {
87        None
88    }
89}
90
91impl FscryptInoLblk32DirCipher {
92    fn encrypt_filename_with_max_len(
93        &self,
94        object_id: u64,
95        buffer: &mut Vec<u8>,
96        max_len: usize,
97    ) -> Result<(), Error> {
98        ensure!(buffer.len() <= max_len, "Filename too long");
99
100        let mut hasher = SipHasher::new_with_key(&self.ino_hash_key);
101        hasher.write(object_id.as_bytes());
102        let iv = [hasher.finish() as u32, 0, 0, 0];
103
104        buffer.resize(buffer.len().next_multiple_of(NAME_PADDING), 0);
105
106        let mut cbc =
107            cbc::Encryptor::<aes::Aes256>::new((&self.cts_key).into(), iv.as_bytes().into());
108        let inout = InOutBuf::<'_, '_, u8>::from(&mut buffer[..]);
109        let (mut blocks, _): (InOutBuf<'_, '_, Block<aes::Aes256>>, _) = inout.into_chunks();
110        let mut chunks = blocks.get_out();
111        cbc.encrypt_blocks_mut(&mut chunks);
112        if chunks.len() >= 2 {
113            // We are encrypting with CTS.  In most cases, the padding will mean it's a multiple of
114            // NAME_PADDING bytes, so all we need to do is swap the last two chunks.  There is one
115            // exception: when the filename ends up being longer than max_len after padding.  In
116            // that case, all we have to do is trim the end after swapping the last two chunks.
117            chunks.swap(chunks.len() - 1, chunks.len() - 2);
118            buffer.truncate(max_len);
119        }
120        Ok(())
121    }
122
123    fn decrypt_filename_with_max_len(
124        &self,
125        object_id: u64,
126        buffer: &mut Vec<u8>,
127        max_len: usize,
128    ) -> Result<(), Error> {
129        let alignment = buffer.len() % NAME_PADDING;
130        if alignment != 0 {
131            // For CTS, the only case we need to care about is when the encrypted filename is
132            // max_len bytes. In all other cases, the filename should be a multiple of NAME_PADDING
133            // bytes.
134            ensure!(buffer.len() == max_len, "Unexpected filename length");
135
136            // Decrypt the second to last block.
137            let mut cipher = aes::Aes256::new(&self.cts_key.into());
138            let mut out = GenericArray::<u8, U16>::default();
139            cipher.decrypt_block_inout_mut(
140                (
141                    GenericArray::from_slice(
142                        &buffer[max_len - alignment - NAME_PADDING..max_len - alignment],
143                    ),
144                    &mut out,
145                )
146                    .into(),
147            );
148
149            // Copy the extra bytes we need.
150            buffer.extend_from_slice(&out[alignment..]);
151        }
152
153        let mut hasher = SipHasher::new_with_key(&self.ino_hash_key);
154        hasher.write(object_id.as_bytes());
155        let iv = [hasher.finish() as u32, 0, 0, 0];
156
157        let mut cbc =
158            cbc::Decryptor::<aes::Aes256>::new((&self.cts_key).into(), iv.as_bytes().into());
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(&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        _device_offset: u64,
201        _file_offset: u64,
202        _buffer: &mut [u8],
203    ) -> Result<(), Error> {
204        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
205        Err(e.context("encrypt not supported for InoLblk32File"))
206    }
207
208    fn decrypt(
209        &self,
210        _ino: 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, 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(GenericArray::from_slice(&key[..32])),
272            xts_key2: Aes256::new(GenericArray::from_slice(&key[32..64])),
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(GenericArray::from_mut_slice(tweak.as_mut_bytes()));
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(GenericArray::from_mut_slice(tweak.as_mut_bytes()));
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}