fxfs_crypto/cipher/
fxfs.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::{
5    CbcDecryptProcessor, CbcEncryptProcessor, Cipher, FSCRYPT_PADDING, SECTOR_SIZE, Tweak,
6    UnwrappedKey, XtsProcessor,
7};
8use aes::Aes256;
9use aes::cipher::generic_array::GenericArray;
10use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
11use anyhow::Error;
12use std::hash::{Hash, Hasher};
13use zerocopy::IntoBytes;
14
15#[derive(Debug)]
16pub struct FxfsCipher {
17    key: Aes256,
18}
19impl FxfsCipher {
20    pub fn new(key: &UnwrappedKey) -> Self {
21        Self { key: Aes256::new(GenericArray::from_slice(key)) }
22    }
23}
24impl Cipher for FxfsCipher {
25    fn encrypt(
26        &self,
27        _ino: u64,
28        _device_offset: u64,
29        file_offset: u64,
30        buffer: &mut [u8],
31    ) -> Result<(), Error> {
32        fxfs_trace::duration!(c"encrypt", "len" => buffer.len());
33        assert_eq!(file_offset % SECTOR_SIZE, 0);
34        let mut sector_offset = file_offset / SECTOR_SIZE;
35        assert_eq!(buffer.len() % (SECTOR_SIZE as usize), 0);
36        for sector in buffer.chunks_exact_mut(SECTOR_SIZE as usize) {
37            let mut tweak = Tweak(sector_offset as u128);
38            // The same key is used for encrypting the data and computing the tweak.
39            self.key.encrypt_block(GenericArray::from_mut_slice(tweak.as_mut_bytes()));
40            self.key.encrypt_with_backend(XtsProcessor::new(tweak, sector));
41            sector_offset += 1;
42        }
43        Ok(())
44    }
45
46    fn decrypt(
47        &self,
48        _ino: u64,
49        _device_offset: u64,
50        file_offset: u64,
51        buffer: &mut [u8],
52    ) -> Result<(), Error> {
53        fxfs_trace::duration!(c"decrypt", "len" => buffer.len());
54        assert_eq!(file_offset % SECTOR_SIZE, 0);
55        let mut sector_offset = file_offset / SECTOR_SIZE;
56        assert_eq!(buffer.len() % (SECTOR_SIZE as usize), 0);
57        for sector in buffer.chunks_exact_mut(SECTOR_SIZE as usize) {
58            let mut tweak = Tweak(sector_offset as u128);
59            // The same key is used for encrypting the data and computing the tweak.
60            self.key.encrypt_block(GenericArray::from_mut_slice(tweak.as_mut_bytes()));
61            self.key.decrypt_with_backend(XtsProcessor::new(tweak, sector));
62            sector_offset += 1;
63        }
64        Ok(())
65    }
66
67    fn encrypt_filename(&self, object_id: u64, buffer: &mut Vec<u8>) -> Result<(), Error> {
68        // Pad the buffer such that its length is a multiple of FSCRYPT_PADDING.
69        buffer.resize(buffer.len().next_multiple_of(FSCRYPT_PADDING), 0);
70        self.key.encrypt_with_backend(CbcEncryptProcessor::new(Tweak(object_id as u128), buffer));
71        Ok(())
72    }
73
74    fn decrypt_filename(&self, object_id: u64, buffer: &mut Vec<u8>) -> Result<(), Error> {
75        self.key.decrypt_with_backend(CbcDecryptProcessor::new(Tweak(object_id as u128), buffer));
76        // Remove the padding
77        if let Some(i) = buffer.iter().rposition(|x| *x != 0) {
78            let new_len = i + 1;
79            buffer.truncate(new_len);
80        }
81        Ok(())
82    }
83
84    fn hash_code(&self, _raw_filename: &[u8], filename: &str) -> u32 {
85        if filename.is_empty() {
86            return 0;
87        }
88        let mut hasher = rustc_hash::FxHasher::default();
89        filename.hash(&mut hasher);
90        hasher.finish() as u32
91    }
92
93    fn hash_code_casefold(&self, filename: &str) -> u32 {
94        if filename.is_empty() {
95            return 0;
96        }
97        let mut hasher = rustc_hash::FxHasher::default();
98        for ch in fxfs_unicode::casefold(filename.chars()) {
99            ch.hash(&mut hasher);
100        }
101        let hash = hasher.finish() as u32;
102        // TODO(https://fxbug.dev/427319626): This used to call 'encrypt()' but that doesn't
103        // work on blocks smaller than 16 bytes and there was no assert! to catch that until
104        // until this CL. Removing the ineffective call for now, but we need to encrypt or
105        // seed the hash here to avoid leaking data.
106        hash
107    }
108
109    fn supports_inline_encryption(&self) -> bool {
110        false
111    }
112
113    fn crypt_ctx(&self, _ino: u64, _file_offset: u64) -> Option<(u32, u8)> {
114        None
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::{FxfsCipher, UnwrappedKey};
121    use crate::Cipher;
122    use std::sync::Arc;
123
124    /// Output produced via:
125    /// echo -n filename > in.txt ; truncate -s 16 in.txt
126    /// openssl aes-256-cbc -e -iv 02000000000000000000000000000000 -nosalt -K 1fcdf30b7d191bd95d3161fe08513b864aa15f27f910f1c66eec8cfa93e9893b -in in.txt -out out.txt -nopad
127    /// hexdump out.txt -e "16/1 \"%02x\" \"\n\"" -v
128    #[test]
129    fn test_encrypt_filename() {
130        let raw_key_hex = "1fcdf30b7d191bd95d3161fe08513b864aa15f27f910f1c66eec8cfa93e9893b";
131        let raw_key_bytes: [u8; 32] =
132            hex::decode(raw_key_hex).expect("decode failed").try_into().unwrap();
133        let unwrapped_key = UnwrappedKey::new(raw_key_bytes.to_vec());
134        let cipher: Arc<dyn Cipher> = Arc::new(FxfsCipher::new(&unwrapped_key));
135        let object_id = 2;
136        let mut text = "filename".to_string().as_bytes().to_vec();
137        cipher.encrypt_filename(object_id, &mut text).expect("encrypt filename failed");
138        assert_eq!(text, hex::decode("52d56369103a39b3ea1e09c85dd51546").expect("decode failed"));
139    }
140
141    /// Output produced via:
142    /// openssl aes-256-cbc -d -iv 02000000000000000000000000000000 -nosalt -K 1fcdf30b7d191bd95d3161fe08513b864aa15f27f910f1c66eec8cfa93e9893b -in out.txt -out in.txt
143    /// cat in.txt
144    #[test]
145    fn test_decrypt_filename() {
146        let raw_key_hex = "1fcdf30b7d191bd95d3161fe08513b864aa15f27f910f1c66eec8cfa93e9893b";
147        let raw_key_bytes: [u8; 32] =
148            hex::decode(raw_key_hex).expect("decode failed").try_into().unwrap();
149        let unwrapped_key = UnwrappedKey::new(raw_key_bytes.to_vec());
150        let cipher: Arc<dyn Cipher> = Arc::new(FxfsCipher::new(&unwrapped_key));
151        let object_id = 2;
152        let mut text = hex::decode("52d56369103a39b3ea1e09c85dd51546").expect("decode failed");
153        cipher.decrypt_filename(object_id, &mut text).expect("encrypt filename failed");
154        assert_eq!(text, "filename".to_string().as_bytes().to_vec());
155    }
156    #[test]
157    fn test_hash_code() {
158        // This test just ensures that we don't change hash_code() by mistake.
159        // We cannot change these hash functions without breaking lookup() on directories as
160        // the lookup code will only search within one hash_code prefix/bucket.
161        let unwrapped_key = UnwrappedKey::new(vec![0; 32]);
162        let cipher: Arc<dyn Cipher> = Arc::new(FxfsCipher::new(&unwrapped_key));
163        assert_eq!(cipher.hash_code("Straße".as_bytes(), "Straße"), 433651741);
164    }
165
166    #[test]
167    fn test_hash_code_casefold() {
168        // Note: This ensures hash_code stability but the current casefold hash lacks encryption
169        // so this is not actually dependent on the key until https://fxbug.dev/427319626 is
170        // resolved.
171        let unwrapped_key = UnwrappedKey::new(vec![0; 32]);
172        let cipher: Arc<dyn Cipher> = Arc::new(FxfsCipher::new(&unwrapped_key));
173        assert_eq!(cipher.hash_code_casefold("Straße"), 3602031996);
174    }
175}