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};
910/// 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 {
28pub hash_code: u64,
29pub filename: [u8; 149],
30pub 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.
40len: usize,
41}
4243/// The maximum length of ProxyFilename before being base64 encoded.
44const PROXY_FILENAME_MAX_SIZE: usize = 8 + 149 + 32;
4546impl ProxyFilename {
47pub fn new(hash_code: u64, raw_filename: &[u8]) -> Self {
48let mut filename = [0u8; 149];
49let mut sha256 = [0u8; 32];
50let 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 {
54let 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 };
59Self { hash_code, filename, sha256, len }
60 }
61}
6263impl Into<String> for ProxyFilename {
64fn into(self) -> String {
65 URL_SAFE_NO_PAD.encode(&self.as_bytes()[..self.len])
66 }
67}
6869impl TryFrom<&str> for ProxyFilename {
70type Error = Error;
71fn try_from(s: &str) -> Result<Self, Self::Error> {
72let mut bytes = URL_SAFE_NO_PAD.decode(s).map_err(|_| anyhow!("Invalid proxy filename"))?;
73ensure!(
74 (bytes.len() >= 8 && bytes.len() <= 8 + 149) || bytes.len() == PROXY_FILENAME_MAX_SIZE,
75"Invalid proxy filename length {}",
76 bytes.len()
77 );
78let len = bytes.len();
79 bytes.resize(std::mem::size_of::<ProxyFilename>(), 0);
80let mut instance = Self::read_from_bytes(&bytes).unwrap();
81 instance.len = len;
82Ok(instance)
83 }
84}
8586#[cfg(test)]
87mod tests {
88use super::*;
8990/// The maximum length of ProxyFilename encoded with base64.
91const PROXY_FILENAME_MAX_ENCODED_SIZE: usize = (PROXY_FILENAME_MAX_SIZE * 4).div_ceil(3);
9293#[test]
94fn test_proxy_filename() {
95// Invalid filename works when encoded.
96let a = ProxyFilename::new(1, b"fo!$obar");
97let encoded: String = a.into();
98let b: ProxyFilename = encoded.as_str().try_into().unwrap();
99assert_eq!(a, b);
100101// Short filename.
102let a = ProxyFilename::new(1, b"foobar");
103let encoded: String = a.into();
104let b: ProxyFilename = encoded.as_str().try_into().unwrap();
105assert_eq!(a, b);
106107// 149 length filename.
108let a = ProxyFilename::new(1, &[0xff; 149]);
109let encoded: String = a.into();
110let b: ProxyFilename = encoded.as_str().try_into().unwrap();
111assert_eq!(a, b);
112// Account for base64 bloat (Length growth by 4/3rd, rounded up)
113assert_eq!(encoded.len(), ((8 + 149) * 4usize).div_ceil(3));
114115// 150 length filename -- now has sha256 suffix.
116 // Note the filename is all zeros. This should not affect the output.
117let a = ProxyFilename::new(1, &[0; 150]);
118let encoded: String = a.into();
119let b: ProxyFilename = encoded.as_str().try_into().unwrap();
120assert_eq!(a, b);
121assert_eq!(encoded.len(), PROXY_FILENAME_MAX_ENCODED_SIZE);
122123// 255 length filename
124let a = ProxyFilename::new(1, &[b'a'; 255]);
125let encoded: String = a.into();
126let b: ProxyFilename = encoded.as_str().try_into().unwrap();
127assert_eq!(a, b);
128assert_eq!(encoded.len(), PROXY_FILENAME_MAX_ENCODED_SIZE);
129130// Decoding of bad base64 strings.
131assert!(ProxyFilename::try_from("$$dda123=").is_err());
132133assert_eq!(
134 URL_SAFE_NO_PAD.encode(&[0; PROXY_FILENAME_MAX_SIZE]).len(),
135 PROXY_FILENAME_MAX_ENCODED_SIZE
136 );
137138// 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.
141assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 0]).as_str()).is_err());
142assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 7]).as_str()).is_err());
143assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 8]).as_str()).is_ok());
144assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 9]).as_str()).is_ok());
145assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 8 + 148]).as_str()).is_ok());
146assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 8 + 149]).as_str()).is_ok());
147assert!(ProxyFilename::try_from(URL_SAFE_NO_PAD.encode(&[b'a'; 8 + 150]).as_str()).is_err());
148assert!(ProxyFilename::try_from(
149 URL_SAFE_NO_PAD.encode(&[b'a'; PROXY_FILENAME_MAX_SIZE - 1]).as_str()
150 )
151 .is_err());
152assert!(ProxyFilename::try_from(
153 URL_SAFE_NO_PAD.encode(&[b'a'; PROXY_FILENAME_MAX_SIZE]).as_str()
154 )
155 .is_ok());
156assert!(ProxyFilename::try_from(
157 URL_SAFE_NO_PAD.encode(&[b'a'; PROXY_FILENAME_MAX_SIZE + 1]).as_str()
158 )
159 .is_err());
160 }
161}