proxy_filename/
lib.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 anyhow::{anyhow, ensure, Error};
5use base64::engine::general_purpose::URL_SAFE_NO_PAD;
6use base64::Engine as _;
7use sha2::Digest;
8use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
9
10/// A proxy filename is used when we don't have the keys to decrypt the actual filename.
11///
12/// When working with locked directories, we encode both Dentry and user provided
13/// filenames using this struct before comparing them.
14///
15/// The hash code is encoded directly, allowing an index in some cases, even when entries
16/// are encrypted.
17///
18/// As a work around to keep uniqueness, for filenames longer than 149 bytes, we use the filename
19/// prefix and calculate the sha256 of the full encrypted name.  This produces a filename that is
20/// under the limit and very likely to remain unique.
21///
22/// The filename prefix size is chosen here for compatibility.
23#[repr(C, packed)]
24#[derive(
25    Copy, Clone, Debug, Eq, PartialEq, FromBytes, Immutable, KnownLayout, IntoBytes, Unaligned,
26)]
27pub struct ProxyFilename {
28    pub hash_code: u64,
29    pub filename: [u8; 149],
30    pub sha256: [u8; 32],
31    // 'len' holds the length in bytes of the material that gets base64 encoded.
32    //
33    // The fields above are treated as an array of bytes and directly base64 encoded to give
34    // a proxy filename. The length of this encoding varies. It is always at least 8 bytes
35    // (covering hash_code) and covers the filename if 149 bytes or shorter, but if the filename
36    // exceeds 149 characters then the length will always be the full size of hash_code, filename
37    // prefix and sha256.
38    // This length is not stored anywhere and cannot be implied from the encrypted filename as
39    // it may contain NULL characters so we tack it onto the end here. It is not serialized.
40    len: usize,
41}
42
43/// The maximum length of ProxyFilename before being base64 encoded.
44const PROXY_FILENAME_MAX_SIZE: usize = 8 + 149 + 32;
45
46impl ProxyFilename {
47    pub fn new(hash_code: u64, raw_filename: &[u8]) -> Self {
48        let mut filename = [0u8; 149];
49        let mut sha256 = [0u8; 32];
50        let len = if raw_filename.len() <= filename.len() {
51            filename[..raw_filename.len()].copy_from_slice(raw_filename);
52            std::mem::size_of::<u64>() + raw_filename.len()
53        } else {
54            let len = filename.len();
55            filename.copy_from_slice(&raw_filename[..len]);
56            sha256 = sha2::Sha256::digest(&raw_filename[len..]).into();
57            PROXY_FILENAME_MAX_SIZE
58        };
59        Self { hash_code, filename, sha256, len }
60    }
61}
62
63impl Into<String> for ProxyFilename {
64    fn into(self) -> String {
65        URL_SAFE_NO_PAD.encode(&self.as_bytes()[..self.len])
66    }
67}
68
69impl TryFrom<&str> for ProxyFilename {
70    type Error = Error;
71    fn try_from(s: &str) -> Result<Self, Self::Error> {
72        let mut bytes = URL_SAFE_NO_PAD.decode(s).map_err(|_| anyhow!("Invalid proxy filename"))?;
73        ensure!(
74            (bytes.len() >= 8 && bytes.len() <= 8 + 149) || bytes.len() == PROXY_FILENAME_MAX_SIZE,
75            "Invalid proxy filename length {}",
76            bytes.len()
77        );
78        let len = bytes.len();
79        bytes.resize(std::mem::size_of::<ProxyFilename>(), 0);
80        let mut instance = Self::read_from_bytes(&bytes).unwrap();
81        instance.len = len;
82        Ok(instance)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    /// The maximum length of ProxyFilename encoded with base64.
91    const PROXY_FILENAME_MAX_ENCODED_SIZE: usize = (PROXY_FILENAME_MAX_SIZE * 4).div_ceil(3);
92
93    #[test]
94    fn test_proxy_filename() {
95        // Invalid filename works when encoded.
96        let a = ProxyFilename::new(1, b"fo!$obar");
97        let encoded: String = a.into();
98        let b: ProxyFilename = encoded.as_str().try_into().unwrap();
99        assert_eq!(a, b);
100
101        // Short filename.
102        let a = ProxyFilename::new(1, b"foobar");
103        let encoded: String = a.into();
104        let b: ProxyFilename = encoded.as_str().try_into().unwrap();
105        assert_eq!(a, b);
106
107        // 149 length filename.
108        let a = ProxyFilename::new(1, &[0xff; 149]);
109        let encoded: String = a.into();
110        let b: ProxyFilename = encoded.as_str().try_into().unwrap();
111        assert_eq!(a, b);
112        // Account for base64 bloat (Length growth by 4/3rd, rounded up)
113        assert_eq!(encoded.len(), ((8 + 149) * 4usize).div_ceil(3));
114
115        // 150 length filename -- now has sha256 suffix.
116        // Note the filename is all zeros. This should not affect the output.
117        let a = ProxyFilename::new(1, &[0; 150]);
118        let encoded: String = a.into();
119        let b: ProxyFilename = encoded.as_str().try_into().unwrap();
120        assert_eq!(a, b);
121        assert_eq!(encoded.len(), PROXY_FILENAME_MAX_ENCODED_SIZE);
122
123        // 255 length filename
124        let a = ProxyFilename::new(1, &[b'a'; 255]);
125        let encoded: String = a.into();
126        let b: ProxyFilename = encoded.as_str().try_into().unwrap();
127        assert_eq!(a, b);
128        assert_eq!(encoded.len(), PROXY_FILENAME_MAX_ENCODED_SIZE);
129
130        // Decoding of bad base64 strings.
131        assert!(ProxyFilename::try_from("$$dda123=").is_err());
132
133        assert_eq!(
134            URL_SAFE_NO_PAD.encode(&[0; PROXY_FILENAME_MAX_SIZE]).len(),
135            PROXY_FILENAME_MAX_ENCODED_SIZE
136        );
137
138        // Decoding of bad lengths.
139        // Valid lengths include the 8 byte hash_code and a filename up to 149 characters.
140        // If the filename exceeds that, the length should be exactly PROXY_FILENAME_MAX_SIZE.
141        assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 0]).as_str()).is_err());
142        assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 7]).as_str()).is_err());
143        assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 8]).as_str()).is_ok());
144        assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 9]).as_str()).is_ok());
145        assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 8 + 148]).as_str()).is_ok());
146        assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 8 + 149]).as_str()).is_ok());
147        assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 8 + 150]).as_str()).is_err());
148        assert!(ProxyFilename::try_from(
149            URL_SAFE_NO_PAD.encode(&[b'a'; PROXY_FILENAME_MAX_SIZE - 1]).as_str()
150        )
151        .is_err());
152        assert!(ProxyFilename::try_from(
153            URL_SAFE_NO_PAD.encode(&[b'a'; PROXY_FILENAME_MAX_SIZE]).as_str()
154        )
155        .is_ok());
156        assert!(ProxyFilename::try_from(
157            URL_SAFE_NO_PAD.encode(&[b'a'; PROXY_FILENAME_MAX_SIZE + 1]).as_str()
158        )
159        .is_err());
160    }
161}