Skip to main content

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::{Cipher, SECTOR_SIZE, Tweak, UnwrappedKey, XtsProcessor};
5use aes::Aes256;
6use aes::cipher::{BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
7use anyhow::Error;
8use log::warn;
9use zerocopy::IntoBytes;
10
11#[derive(Debug)]
12pub struct FxfsCipher {
13    key: Aes256,
14    legacy: bool,
15}
16impl FxfsCipher {
17    pub fn new(key: &UnwrappedKey) -> Self {
18        Self { key: Aes256::new(key.as_slice().try_into().unwrap()), legacy: false }
19    }
20
21    pub fn new_legacy(key: &UnwrappedKey) -> Self {
22        Self { key: Aes256::new(key.as_slice().try_into().unwrap()), legacy: true }
23    }
24}
25impl Cipher for FxfsCipher {
26    fn encrypt(
27        &self,
28        _ino: u64,
29        attribute_id: u64,
30        _device_offset: u64,
31        file_offset: u64,
32        buffer: &mut [u8],
33    ) -> Result<(), Error> {
34        fxfs_trace::duration!("encrypt", "len" => buffer.len());
35        assert_eq!(file_offset % SECTOR_SIZE, 0);
36        let mut sector_offset = file_offset / SECTOR_SIZE;
37        assert_eq!(buffer.len() % (SECTOR_SIZE as usize), 0);
38        let upper_tweak = if self.legacy { 0 } else { (attribute_id as u128) << 64 };
39        for sector in buffer.chunks_exact_mut(SECTOR_SIZE as usize) {
40            let mut tweak = Tweak(upper_tweak | (sector_offset as u128));
41            // The same key is used for encrypting the data and computing the tweak.
42            self.key.encrypt_block(tweak.as_mut_bytes().try_into().unwrap());
43            self.key.encrypt_with_backend(XtsProcessor::new(tweak, sector));
44            sector_offset += 1;
45        }
46        Ok(())
47    }
48
49    fn decrypt(
50        &self,
51        _ino: u64,
52        attribute_id: u64,
53        _device_offset: u64,
54        file_offset: u64,
55        buffer: &mut [u8],
56    ) -> Result<(), Error> {
57        fxfs_trace::duration!("decrypt", "len" => buffer.len());
58        assert_eq!(file_offset % SECTOR_SIZE, 0);
59        let mut sector_offset = file_offset / SECTOR_SIZE;
60        assert_eq!(buffer.len() % (SECTOR_SIZE as usize), 0);
61        let upper_tweak = if self.legacy { 0 } else { (attribute_id as u128) << 64 };
62        for sector in buffer.chunks_exact_mut(SECTOR_SIZE as usize) {
63            let mut tweak = Tweak(upper_tweak | (sector_offset as u128));
64            // The same key is used for encrypting the data and computing the tweak.
65            self.key.encrypt_block(tweak.as_mut_bytes().try_into().unwrap());
66            self.key.decrypt_with_backend(XtsProcessor::new(tweak, sector));
67            sector_offset += 1;
68        }
69        Ok(())
70    }
71
72    fn encrypt_filename(&self, _object_id: u64, _buffer: &mut Vec<u8>) -> Result<(), Error> {
73        debug_assert!(false, "encrypt_filename called on fxfs cipher");
74        Err(zx_status::Status::NOT_SUPPORTED.into())
75    }
76
77    fn decrypt_filename(&self, _object_id: u64, _buffer: &mut Vec<u8>) -> Result<(), Error> {
78        // NOTE: This isn't a debug assertion because it would trip on the golden image tests.
79        warn!("decrypt_filename called on fxfs cipher");
80        Err(zx_status::Status::NOT_SUPPORTED.into())
81    }
82
83    fn hash_code(&self, _raw_filename: &[u8], _filename: &str) -> Option<u32> {
84        debug_assert!(false, "hash_code called on fxfs cipher");
85        None
86    }
87
88    fn hash_code_casefold(&self, _filename: &str) -> u32 {
89        debug_assert!(false, "hash_code_casefold called on fxfs cipher");
90        0
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<(u32, u8)> {
98        None
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::{Cipher, FxfsCipher, SECTOR_SIZE};
105    use crate::UnwrappedKey;
106
107    #[test]
108    fn test_legacy_fxfs_cipher_ignores_attribute_id() {
109        let key = UnwrappedKey::new(vec![0x42; 32]);
110        let cipher = FxfsCipher::new_legacy(&key);
111        let mut buf0 = vec![0x12; SECTOR_SIZE as usize];
112        let mut buf1 = vec![0x12; SECTOR_SIZE as usize];
113
114        cipher.encrypt(1, 0, 0, 0, &mut buf0).expect("encrypt attr 0");
115        cipher.encrypt(1, 4, 0, 0, &mut buf1).expect("encrypt attr 4");
116        assert_eq!(
117            buf0, buf1,
118            "LegacyFxfsCipher should produce identical ciphertext for same file_offset regardless of attribute_id"
119        );
120    }
121
122    #[test]
123    fn test_fxfs_cipher_domain_separates_attribute_id() {
124        let key = UnwrappedKey::new(vec![0x42; 32]);
125        let cipher = FxfsCipher::new(&key);
126        let mut buf0 = vec![0x12; SECTOR_SIZE as usize];
127        let mut buf1 = vec![0x12; SECTOR_SIZE as usize];
128
129        cipher.encrypt(1, 0, 0, 0, &mut buf0).expect("encrypt attr 0");
130        cipher.encrypt(1, 4, 0, 0, &mut buf1).expect("encrypt attr 4");
131        assert_ne!(buf0, buf1, "FxfsCipher should domain-separate tweaks across attribute_id");
132
133        // Verify decryption works correctly for each attribute_id
134        cipher.decrypt(1, 0, 0, 0, &mut buf0).expect("decrypt attr 0");
135        assert_eq!(buf0, vec![0x12; SECTOR_SIZE as usize]);
136
137        cipher.decrypt(1, 4, 0, 0, &mut buf1).expect("decrypt attr 4");
138        assert_eq!(buf1, vec![0x12; SECTOR_SIZE as usize]);
139    }
140}