Skip to main content

fxfs_crypto/cipher/
fscrypt_ino_lblk64.rs

1// Copyright 2026 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::{
6    BLOCK_SIZE, Cipher, MAX_FILENAME_LEN, MAX_SYMLINK_LEN, UnwrappedKey, decrypt_filename_cts,
7    encrypt_filename_cts,
8};
9use anyhow::{Context, Error};
10use storage_ptr_slice::MutPtrByteSlice;
11
12/// Directory and symlink cipher for `FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64`.
13///
14/// Per the Linux fscrypt specification (`fs/crypto/keysetup.c` and `fs/crypto/crypto.c`),
15/// `IV_INO_LBLK_64` derives a shared per-mode `cts_key` using `HKDF_CONTEXT_IV_INO_LBLK_64_KEY`
16/// (`4`) and constructs the 16-byte AES-256-CBC-CTS IV by placing the unhashed 32-bit inode
17/// number in bits `32..63` (`[0, object_id as u32, 0, 0]`, with `lblk_num = 0` in bits `0..31`).
18#[derive(Debug)]
19pub struct FscryptInoLblk64DirCipher {
20    cts_key: [u8; 32],
21    dir_hash_key: [u8; 16],
22}
23
24impl FscryptInoLblk64DirCipher {
25    pub fn new(key: &UnwrappedKey) -> Self {
26        assert_eq!(key.len(), 48, "Expected 48-byte unwrapped key for FscryptInoLblk64DirCipher");
27        Self {
28            cts_key: key[..32].try_into().unwrap(),
29            dir_hash_key: key[32..48].try_into().unwrap(),
30        }
31    }
32
33    #[inline(always)]
34    fn iv(object_id: u64) -> Result<[u32; 4], Error> {
35        let ino = u32::try_from(object_id).with_context(|| {
36            format!("IV_INO_LBLK_64 requires 32-bit inode numbers, got {object_id:#x}")
37        })?;
38        Ok([0, ino.to_le(), 0, 0])
39    }
40}
41
42impl Cipher for FscryptInoLblk64DirCipher {
43    fn encrypt(
44        &self,
45        _ino: u64,
46        _attribute_id: u64,
47        _device_offset: u64,
48        _file_offset: u64,
49        _buffer: MutPtrByteSlice<'_>,
50    ) -> Result<(), Error> {
51        Err(zx_status::Status::NOT_SUPPORTED).context("encrypt not supported for InoLblk64Dir")
52    }
53
54    fn decrypt(
55        &self,
56        _ino: u64,
57        _attribute_id: u64,
58        _device_offset: u64,
59        _file_offset: u64,
60        _buffer: MutPtrByteSlice<'_>,
61    ) -> Result<(), Error> {
62        Err(zx_status::Status::NOT_SUPPORTED).context("decrypt not supported for InoLblk64Dir")
63    }
64
65    fn encrypt_filename(&self, object_id: u64, buffer: &mut Vec<u8>) -> Result<(), Error> {
66        let iv = Self::iv(object_id)?;
67        encrypt_filename_cts(&self.cts_key, &iv, buffer, MAX_FILENAME_LEN)
68    }
69
70    fn decrypt_filename(&self, object_id: u64, buffer: &mut Vec<u8>) -> Result<(), Error> {
71        let iv = Self::iv(object_id)?;
72        decrypt_filename_cts(&self.cts_key, &iv, buffer, MAX_FILENAME_LEN)
73    }
74
75    fn encrypt_symlink(&self, object_id: u64, buffer: &mut Vec<u8>) -> Result<(), Error> {
76        let iv = Self::iv(object_id)?;
77        encrypt_filename_cts(&self.cts_key, &iv, buffer, MAX_SYMLINK_LEN)
78    }
79
80    fn decrypt_symlink(&self, object_id: u64, buffer: &mut Vec<u8>) -> Result<(), Error> {
81        let iv = Self::iv(object_id)?;
82        decrypt_filename_cts(&self.cts_key, &iv, buffer, MAX_SYMLINK_LEN)
83    }
84
85    fn hash_code(&self, _raw_filename: &[u8], _filename: &str) -> Option<u32> {
86        None
87    }
88
89    fn hash_code_casefold(&self, filename: &str) -> u32 {
90        fscrypt::direntry::casefold_encrypt_hash_filename(filename.into(), &self.dir_hash_key)
91    }
92
93    fn supports_inline_encryption(&self) -> bool {
94        false
95    }
96
97    fn crypt_ctx(&self, _ino: u64, _attribute_id: u64, _file_offset: u64) -> Option<(u64, u8)> {
98        None
99    }
100}
101
102/// File cipher for `FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64`.
103///
104/// Produces a 64-bit DUN with the 32-bit inode number in bits `32..63` and the 32-bit file logical
105/// block number (`file_offset / 4096`) in bits `0..31`: `(ino << 32) | block_num`.
106#[derive(Debug)]
107pub struct FscryptInoLblk64FileCipher {
108    slot: u8,
109}
110
111impl FscryptInoLblk64FileCipher {
112    pub fn new(key: &UnwrappedKey) -> Self {
113        Self { slot: key.slot().unwrap() }
114    }
115
116    pub fn new_with_slot(slot: u8) -> Self {
117        Self { slot }
118    }
119
120    #[inline(always)]
121    fn tweak(&self, ino: u64, block_num: u64) -> u64 {
122        let ino = u32::try_from(ino).unwrap_or_else(|_| {
123            panic!("IV_INO_LBLK_64 requires 32-bit inode numbers, got {ino:#x}")
124        }) as u64;
125        let block_num = u32::try_from(block_num).unwrap_or_else(|_| {
126            panic!("IV_INO_LBLK_64 requires 32-bit logical block numbers, got {block_num:#x}")
127        }) as u64;
128        (ino << 32) | block_num
129    }
130}
131
132impl Cipher for FscryptInoLblk64FileCipher {
133    fn encrypt(
134        &self,
135        _ino: u64,
136        _attribute_id: u64,
137        _device_offset: u64,
138        _file_offset: u64,
139        _buffer: MutPtrByteSlice<'_>,
140    ) -> Result<(), Error> {
141        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
142        Err(e.context("encrypt not supported for InoLblk64File"))
143    }
144
145    fn decrypt(
146        &self,
147        _ino: u64,
148        _attribute_id: u64,
149        _device_offset: u64,
150        _file_offset: u64,
151        _buffer: MutPtrByteSlice<'_>,
152    ) -> Result<(), Error> {
153        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
154        Err(e.context("decrypt not supported for InoLblk64File"))
155    }
156
157    fn encrypt_filename(&self, _object_id: u64, _buffer: &mut Vec<u8>) -> Result<(), Error> {
158        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
159        Err(e.context("encrypt_filename not supported for InoLblk64File"))
160    }
161
162    fn decrypt_filename(&self, _object_id: u64, _buffer: &mut Vec<u8>) -> Result<(), Error> {
163        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
164        Err(e.context("decrypt_filename not supported for InoLblk64File"))
165    }
166
167    fn encrypt_symlink(&self, _object_id: u64, _buffer: &mut Vec<u8>) -> Result<(), Error> {
168        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
169        Err(e.context("encrypt_symlink not supported for InoLblk64File"))
170    }
171
172    fn decrypt_symlink(&self, _object_id: u64, _buffer: &mut Vec<u8>) -> Result<(), Error> {
173        let e: Error = zx_status::Status::NOT_SUPPORTED.into();
174        Err(e.context("decrypt_symlink not supported for InoLblk64File"))
175    }
176
177    fn hash_code(&self, _raw_filename: &[u8], _filename: &str) -> Option<u32> {
178        debug_assert!(false, "hash_code called on file cipher");
179        None
180    }
181
182    fn hash_code_casefold(&self, _filename: &str) -> u32 {
183        debug_assert!(false, "hash_code_casefold called on file cipher");
184        0
185    }
186
187    fn supports_inline_encryption(&self) -> bool {
188        true
189    }
190
191    fn crypt_ctx(&self, ino: u64, _attribute_id: u64, file_offset: u64) -> Option<(u64, u8)> {
192        assert_eq!(file_offset % BLOCK_SIZE as u64, 0);
193        let block_num = file_offset / BLOCK_SIZE as u64;
194        let tweak = self.tweak(ino, block_num);
195        Some((tweak, self.slot))
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::{FscryptInoLblk64DirCipher, FscryptInoLblk64FileCipher};
202    use crate::cipher::fscrypt_test_data;
203    use crate::{Cipher, UnwrappedKey};
204
205    #[test]
206    fn test_fscrypt_ino_lblk64_file_cipher_dun() {
207        let ino = 0x1234_5678u64;
208        let file_offset = 10 * 4096;
209
210        let cipher = FscryptInoLblk64FileCipher::new(&UnwrappedKey::new_with_slot(vec![], Some(4)));
211        assert_eq!(cipher.crypt_ctx(ino, 0, file_offset), Some(((ino << 32) | 10, 4)));
212    }
213
214    #[test]
215    fn test_fscrypt_ino_lblk64_dir_cipher_roundtrip() {
216        let mut unwrapped = vec![0x11u8; 48];
217        unwrapped[0] = 0x42;
218        let cipher = FscryptInoLblk64DirCipher::new(&UnwrappedKey::new(unwrapped));
219
220        let original = b"example_encrypted_filename.txt".to_vec();
221        let mut buf1 = original.clone();
222        let mut buf2 = original.clone();
223
224        cipher.encrypt_filename(100, &mut buf1).expect("encrypt_filename failed");
225        cipher.encrypt_filename(200, &mut buf2).expect("encrypt_filename failed");
226        // Different directory object_ids must produce distinct ciphertexts under IV_INO_LBLK_64.
227        assert_ne!(buf1, buf2);
228
229        cipher.decrypt_filename(100, &mut buf1).expect("decrypt_filename failed");
230        assert_eq!(buf1, original);
231    }
232
233    #[test]
234    fn test_encrypt_and_decrypt_filename_golden() {
235        // Golden test vectors generated with OpenSSL 3.6.3.
236        // Under IV_INO_LBLK_64, the 16-byte IV has `lblk_num = 0` in bits 0..31 and the unhashed
237        // 32-bit `object_id` in bits 32..63 (little-endian), so `object_id = 2` produces
238        // IV `00000000020000000000000000000000`.
239        let mut unwrapped_key = UnwrappedKey::new([0; 48].to_vec());
240        unwrapped_key[0] = 0x10;
241        let cipher = FscryptInoLblk64DirCipher::new(&unwrapped_key);
242        let object_id = 2;
243
244        // 1. One-block case (16 bytes after zero-padding):
245        // ```shell
246        // echo -n filename > in.txt ; truncate -s 16 in.txt
247        // openssl aes-256-cbc -e -iv 00000000020000000000000000000000 -nosalt \
248        //   -K 1000000000000000000000000000000000000000000000000000000000000000 \
249        //   -in in.txt -out out.txt -nopad
250        // hexdump out.txt -e "16/1 \"%02x\" \"\n\"" -v
251        // ```
252        let mut text = b"filename".to_vec();
253        let expected_1block =
254            hex::decode("23cf0ebf11b011f471c3e6be7b2d7d4a").expect("decode failed");
255        cipher.encrypt_filename(object_id, &mut text).expect("encrypt filename failed");
256        assert_eq!(text, expected_1block);
257        cipher.decrypt_filename(object_id, &mut text).expect("decrypt filename failed");
258        assert_eq!(text, b"filename".to_vec());
259
260        // 2. Two-block case (32 bytes after zero-padding, last two 16B blocks swapped for CTS):
261        // ```shell
262        // echo -n "0123456789abcdef_filename" > in.txt ; truncate -s 32 in.txt
263        // openssl aes-256-cbc -e -iv 00000000020000000000000000000000 -nosalt \
264        //   -K 1000000000000000000000000000000000000000000000000000000000000000 \
265        //   -in in.txt -out out.txt -nopad
266        // hexdump out.txt -e "16/1 \"%02x\" \"\n\"" -v
267        // 4869c980dcb984074c3b977100cc9334
268        // 1029e8d0cf2440b023f24e080d207704
269        // <Swap the last two blocks and concatenate>
270        // ```
271        let mut text = b"0123456789abcdef_filename".to_vec();
272        let expected_2block =
273            hex::decode("1029e8d0cf2440b023f24e080d2077044869c980dcb984074c3b977100cc9334")
274                .expect("decode failed");
275        cipher.encrypt_filename(object_id, &mut text).expect("encrypt filename failed");
276        assert_eq!(text, expected_2block);
277        cipher.decrypt_filename(object_id, &mut text).expect("decrypt filename failed");
278        assert_eq!(text, b"0123456789abcdef_filename".to_vec());
279
280        // 3. Multi-block (192-byte) filename case:
281        // ```shell
282        // export LONG_NAME_16=xxxxxxxxyyyyyyyy
283        // export LONG_NAME_32=${LONG_NAME_16}${LONG_NAME_16}
284        // export LONG_NAME_64=${LONG_NAME_32}${LONG_NAME_32}
285        // export LONG_NAME_192=${LONG_NAME_64}${LONG_NAME_64}${LONG_NAME_64}
286        // echo -n "${LONG_NAME_192}" > in.txt
287        // openssl aes-256-cbc -e -iv 00000000020000000000000000000000 -nosalt \
288        //   -K 1000000000000000000000000000000000000000000000000000000000000000 \
289        //   -in in.txt -out out.txt -nopad
290        // <Swap the last two 16-byte blocks and concatenate>
291        // ```
292        let long_name_64 = b"xxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyy";
293        let mut text = vec![];
294        for _ in 0..3 {
295            text.extend_from_slice(long_name_64);
296        }
297        let original_192 = text.clone();
298        let expected_192 = hex::decode(concat!(
299            "7b379f5e2cdb7f89bd3ee46fb7bac8302c0c9c1008a7080d3bd606aa16192d7c",
300            "373dad8f8a9ae6229472be9cce1e3326e338afbec4b55d1ea789d42da2375ecf",
301            "2711452440dc6264f0093c5554a44d85c45c2cb23dfa1638882c63684f858c7a",
302            "4f0f77bc5ccf62b5eb2f63e7d7b713667fc546cbb83e81b97009f611af738e52",
303            "1fdd34a3fc92954875dfa2837e904177e9f013fe9374ec0d963e7711473cc271",
304            "9d2b35196eb4af074ae22db4334813fecceb130b0d3108d5c6f96bb73fdcd419"
305        ))
306        .expect("decode failed");
307        cipher.encrypt_filename(object_id, &mut text).expect("encrypt filename failed");
308        assert_eq!(text, expected_192);
309        cipher.decrypt_filename(object_id, &mut text).expect("decrypt filename failed");
310        assert_eq!(text, original_192);
311    }
312
313    #[test]
314    fn test_hkdf_and_filename_encryption_golden() {
315        // End-to-end HKDF + AES-256-CBC-CTS golden vector generated with OpenSSL 3.6.3
316        // using `fscrypt_test_data::{KEY, UUID, DIR_NONCE, DIR_INODE}`:
317        // ```shell
318        // # HKDF-SHA512 with info = "fscrypt\0" (6673637279707400) ||
319        // #   HKDF_CONTEXT_IV_INO_LBLK_64_KEY (04) || ENCRYPTION_MODE_AES_256_CTS (04) || UUID
320        // KEY=9cdfd86c8d7023a0c11653501d81a21ee318eee2318ac17d2a44350758a2d47b\
321        // f15ff524cb07478ef5dec4df7b63cef46bd466c0909e243f23454780a7616a93
322        // INFO=667363727970740004048409849be0894765acc1d6d0cc2f2c97
323        // openssl kdf -keylen 32 -kdfopt digest:SHA2-512 \
324        //   -kdfopt "hexkey:${KEY}" -kdfopt "hexinfo:${INFO}" HKDF
325        // ```
326        let unwrapped = UnwrappedKey::new(
327            fscrypt::to_directory_keys_lblk64(
328                fscrypt_test_data::KEY,
329                fscrypt_test_data::UUID,
330                fscrypt_test_data::DIR_NONCE,
331            )
332            .to_unwrapped_key(),
333        );
334        assert_eq!(
335            &unwrapped[..32],
336            &hex::decode("9f5d59cdaac1960b57edcbd102ab97f58bf921975a0280a20bc41f649152d196")
337                .unwrap()[..]
338        );
339
340        let cipher = FscryptInoLblk64DirCipher::new(&unwrapped);
341
342        let mut short_name = b"A".to_vec();
343        cipher.encrypt_filename(fscrypt_test_data::DIR_INODE, &mut short_name).unwrap();
344        assert_eq!(short_name, hex::decode("882c820a8063b71194dafebb0e8c9858").unwrap());
345        cipher.decrypt_filename(fscrypt_test_data::DIR_INODE, &mut short_name).unwrap();
346        assert_eq!(short_name, b"A");
347
348        let mut two_block_name = b"AAAAAAAAAAAAAAAAA".to_vec();
349        cipher.encrypt_filename(fscrypt_test_data::DIR_INODE, &mut two_block_name).unwrap();
350        assert_eq!(
351            two_block_name,
352            hex::decode("b3bac148afddc13adeb7fd8a324d2aeafbe599ec70a6be84e0769c00689ca7f9")
353                .unwrap()
354        );
355        cipher.decrypt_filename(fscrypt_test_data::DIR_INODE, &mut two_block_name).unwrap();
356        assert_eq!(two_block_name, b"AAAAAAAAAAAAAAAAA");
357    }
358}