Skip to main content

appcore_security/
secret_file.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: secret_file.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/23 23:50:45 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/02 14:12:17 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Hardened deployment-local file secret resolution.
12
13use crate::{SecretBytes, SecretResolver, SecurityError, SecurityResult, SecuritySecretRef};
14use std::fs::{self, File, OpenOptions};
15use std::io::Read;
16use std::path::{Component, Path, PathBuf};
17
18const MAX_FILE_SECRET_BYTES: u64 = 65_536;
19
20/// Resolves relative security references below one owner-only root.
21#[derive(Debug, Clone)]
22pub struct FileSecretResolver {
23    root: PathBuf,
24}
25
26impl FileSecretResolver {
27    /// Creates a resolver rooted at `root`.
28    pub fn new(root: impl Into<PathBuf>) -> Self {
29        Self { root: root.into() }
30    }
31}
32
33impl SecretResolver for FileSecretResolver {
34    fn resolve(&self, reference: &SecuritySecretRef) -> SecurityResult<SecretBytes> {
35        let relative = validate_relative_reference(&reference.0)?;
36        validate_private_directory(&self.root)?;
37        let canonical_root = fs::canonicalize(&self.root).map_err(unavailable)?;
38        let path = self.root.join(relative);
39        reject_symlink_components(&self.root, relative)?;
40        let canonical_path = fs::canonicalize(&path).map_err(unavailable)?;
41        if !canonical_path.starts_with(&canonical_root) {
42            return Err(SecurityError::InvalidSecretRef);
43        }
44        let mut file = open_no_follow(&path)?;
45        validate_private_file(&file)?;
46        let length = file.metadata().map_err(unavailable)?.len();
47        if length == 0 || length > MAX_FILE_SECRET_BYTES {
48            return Err(SecurityError::SecretUnavailable);
49        }
50        let mut value = Vec::with_capacity(length as usize);
51        file.read_to_end(&mut value).map_err(unavailable)?;
52        Ok(SecretBytes::new(value))
53    }
54}
55
56fn validate_relative_reference(value: &str) -> SecurityResult<&Path> {
57    let path = Path::new(value);
58    if value.is_empty()
59        || path.components().any(|component| {
60            matches!(
61                component,
62                Component::ParentDir | Component::RootDir | Component::Prefix(_)
63            )
64        })
65    {
66        return Err(SecurityError::InvalidSecretRef);
67    }
68    Ok(path)
69}
70
71fn reject_symlink_components(root: &Path, relative: &Path) -> SecurityResult<()> {
72    let mut current = root.to_path_buf();
73    for component in relative.components() {
74        current.push(component);
75        let metadata = fs::symlink_metadata(&current).map_err(unavailable)?;
76        if metadata.file_type().is_symlink() || is_reparse_point(&metadata) {
77            return Err(SecurityError::InvalidSecretRef);
78        }
79    }
80    Ok(())
81}
82
83#[cfg(windows)]
84fn is_reparse_point(metadata: &fs::Metadata) -> bool {
85    use std::os::windows::fs::MetadataExt;
86    use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
87    metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
88}
89
90#[cfg(not(windows))]
91fn is_reparse_point(_metadata: &fs::Metadata) -> bool {
92    false
93}
94
95#[cfg(unix)]
96fn open_no_follow(path: &Path) -> SecurityResult<File> {
97    use std::os::unix::fs::OpenOptionsExt;
98    OpenOptions::new()
99        .read(true)
100        .custom_flags(libc::O_NOFOLLOW)
101        .open(path)
102        .map_err(unavailable)
103}
104
105#[cfg(windows)]
106fn open_no_follow(path: &Path) -> SecurityResult<File> {
107    use std::os::windows::fs::OpenOptionsExt;
108    use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
109    OpenOptions::new()
110        .read(true)
111        .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
112        .open(path)
113        .map_err(unavailable)
114}
115
116#[cfg(all(not(unix), not(windows)))]
117fn open_no_follow(_path: &Path) -> SecurityResult<File> {
118    Err(SecurityError::SecretUnavailable)
119}
120
121#[cfg(unix)]
122fn validate_private_directory(path: &Path) -> SecurityResult<()> {
123    use std::os::unix::fs::{MetadataExt, PermissionsExt};
124    let metadata = fs::symlink_metadata(path).map_err(unavailable)?;
125    if metadata.file_type().is_symlink()
126        || !metadata.is_dir()
127        || metadata.permissions().mode() & 0o077 != 0
128        || metadata.uid() != unsafe { libc::geteuid() }
129    {
130        return Err(SecurityError::InvalidSecretRef);
131    }
132    Ok(())
133}
134
135#[cfg(windows)]
136fn validate_private_directory(path: &Path) -> SecurityResult<()> {
137    let metadata = fs::symlink_metadata(path).map_err(unavailable)?;
138    if metadata.file_type().is_symlink() || is_reparse_point(&metadata) || !metadata.is_dir() {
139        return Err(SecurityError::InvalidSecretRef);
140    }
141    windows_acl::validate_path_owner_acl(path)
142}
143
144#[cfg(all(not(unix), not(windows)))]
145fn validate_private_directory(_path: &Path) -> SecurityResult<()> {
146    Err(SecurityError::SecretUnavailable)
147}
148
149#[cfg(unix)]
150fn validate_private_file(file: &File) -> SecurityResult<()> {
151    use std::os::unix::fs::{MetadataExt, PermissionsExt};
152    let metadata = file.metadata().map_err(unavailable)?;
153    if !metadata.is_file()
154        || metadata.permissions().mode() & 0o077 != 0
155        || metadata.uid() != unsafe { libc::geteuid() }
156    {
157        return Err(SecurityError::InvalidSecretRef);
158    }
159    Ok(())
160}
161
162#[cfg(windows)]
163fn validate_private_file(file: &File) -> SecurityResult<()> {
164    let metadata = file.metadata().map_err(unavailable)?;
165    if !metadata.is_file() || is_reparse_point(&metadata) {
166        return Err(SecurityError::InvalidSecretRef);
167    }
168    windows_acl::validate_file_owner_acl(file)
169}
170
171#[cfg(all(not(unix), not(windows)))]
172fn validate_private_file(_file: &File) -> SecurityResult<()> {
173    Err(SecurityError::SecretUnavailable)
174}
175
176fn unavailable<T>(_error: T) -> SecurityError {
177    SecurityError::SecretUnavailable
178}
179
180#[cfg(windows)]
181mod windows_acl {
182    use super::*;
183    use std::ffi::c_void;
184    use std::os::windows::ffi::OsStrExt;
185    use std::os::windows::io::AsRawHandle;
186    use std::ptr;
187    use windows_sys::Win32::Foundation::{CloseHandle, LocalFree, ERROR_SUCCESS, HANDLE};
188    use windows_sys::Win32::Security::Authorization::{
189        GetNamedSecurityInfoW, GetSecurityInfo, SE_FILE_OBJECT,
190    };
191    use windows_sys::Win32::Security::{
192        EqualSid, GetAce, GetTokenInformation, TokenUser, ACCESS_ALLOWED_ACE, ACL,
193        DACL_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID,
194        TOKEN_QUERY, TOKEN_USER,
195    };
196    use windows_sys::Win32::System::SystemServices::{
197        ACCESS_ALLOWED_ACE_TYPE, ACCESS_ALLOWED_CALLBACK_ACE_TYPE,
198        ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE, ACCESS_ALLOWED_OBJECT_ACE_TYPE,
199    };
200    use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
201
202    pub(super) fn validate_path_owner_acl(path: &Path) -> SecurityResult<()> {
203        let wide = path
204            .as_os_str()
205            .encode_wide()
206            .chain(std::iter::once(0))
207            .collect::<Vec<_>>();
208        let mut owner = ptr::null_mut();
209        let mut dacl = ptr::null_mut();
210        let mut descriptor = ptr::null_mut();
211        let result = unsafe {
212            GetNamedSecurityInfoW(
213                wide.as_ptr(),
214                SE_FILE_OBJECT,
215                OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
216                &mut owner,
217                ptr::null_mut(),
218                &mut dacl,
219                ptr::null_mut(),
220                &mut descriptor,
221            )
222        };
223        validate_security_result(result, descriptor, owner, dacl)
224    }
225
226    pub(super) fn validate_file_owner_acl(file: &File) -> SecurityResult<()> {
227        let mut owner = ptr::null_mut();
228        let mut dacl = ptr::null_mut();
229        let mut descriptor = ptr::null_mut();
230        let result = unsafe {
231            GetSecurityInfo(
232                file.as_raw_handle() as HANDLE,
233                SE_FILE_OBJECT,
234                OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
235                &mut owner,
236                ptr::null_mut(),
237                &mut dacl,
238                ptr::null_mut(),
239                &mut descriptor,
240            )
241        };
242        validate_security_result(result, descriptor, owner, dacl)
243    }
244
245    fn validate_security_result(
246        result: u32,
247        descriptor: PSECURITY_DESCRIPTOR,
248        owner: PSID,
249        dacl: *mut ACL,
250    ) -> SecurityResult<()> {
251        if result != ERROR_SUCCESS || descriptor.is_null() {
252            return Err(SecurityError::SecretUnavailable);
253        }
254        let descriptor = SecurityDescriptor(descriptor);
255        let validation = validate_owner_and_acl(owner, dacl);
256        drop(descriptor);
257        validation
258    }
259
260    fn validate_owner_and_acl(owner: PSID, dacl: *mut ACL) -> SecurityResult<()> {
261        if owner.is_null() || dacl.is_null() {
262            return Err(SecurityError::InvalidSecretRef);
263        }
264        let user = current_user()?;
265        if unsafe { EqualSid(owner, user.sid()?) } == 0 {
266            return Err(SecurityError::InvalidSecretRef);
267        }
268        validate_owner_only_acl(dacl, owner)
269    }
270
271    fn validate_owner_only_acl(dacl: *mut ACL, owner: PSID) -> SecurityResult<()> {
272        let mut owner_allowed = false;
273        let ace_count = unsafe { (*dacl).AceCount };
274        for index in 0..u32::from(ace_count) {
275            let mut raw_ace: *mut c_void = ptr::null_mut();
276            if unsafe { GetAce(dacl, index, &mut raw_ace) } == 0 || raw_ace.is_null() {
277                return Err(SecurityError::SecretUnavailable);
278            }
279            let ace_type = unsafe { (*(raw_ace.cast::<ACCESS_ALLOWED_ACE>())).Header.AceType };
280            if ace_type == ACCESS_ALLOWED_ACE_TYPE as u8 {
281                let ace = raw_ace.cast::<ACCESS_ALLOWED_ACE>();
282                let sid = unsafe { ptr::addr_of_mut!((*ace).SidStart).cast::<c_void>() };
283                if unsafe { EqualSid(owner, sid) } == 0 {
284                    return Err(SecurityError::InvalidSecretRef);
285                }
286                owner_allowed = true;
287            } else if is_other_allow_ace(ace_type) {
288                return Err(SecurityError::InvalidSecretRef);
289            }
290        }
291        if owner_allowed {
292            Ok(())
293        } else {
294            Err(SecurityError::InvalidSecretRef)
295        }
296    }
297
298    fn is_other_allow_ace(ace_type: u8) -> bool {
299        [
300            ACCESS_ALLOWED_OBJECT_ACE_TYPE,
301            ACCESS_ALLOWED_CALLBACK_ACE_TYPE,
302            ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE,
303        ]
304        .contains(&u32::from(ace_type))
305    }
306
307    fn current_user() -> SecurityResult<TokenUser> {
308        let mut token: HANDLE = ptr::null_mut();
309        if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
310            return Err(SecurityError::SecretUnavailable);
311        }
312        let token = TokenHandle(token);
313        let mut required = 0;
314        unsafe {
315            GetTokenInformation(token.0, TokenUser, ptr::null_mut(), 0, &mut required);
316        }
317        if required == 0 {
318            return Err(SecurityError::SecretUnavailable);
319        }
320        let mut buffer = vec![0_u8; required as usize];
321        if unsafe {
322            GetTokenInformation(
323                token.0,
324                TokenUser,
325                buffer.as_mut_ptr().cast(),
326                required,
327                &mut required,
328            )
329        } == 0
330        {
331            return Err(SecurityError::SecretUnavailable);
332        }
333        Ok(TokenUser { buffer })
334    }
335
336    struct TokenUser {
337        buffer: Vec<u8>,
338    }
339
340    impl TokenUser {
341        fn sid(&self) -> SecurityResult<PSID> {
342            if self.buffer.len() < std::mem::size_of::<TOKEN_USER>() {
343                return Err(SecurityError::SecretUnavailable);
344            }
345            // SAFETY: GetTokenInformation initialized at least TOKEN_USER bytes.
346            // The byte buffer has no TOKEN_USER alignment guarantee, so use an
347            // unaligned value read instead of constructing a misaligned reference.
348            let user = unsafe { self.buffer.as_ptr().cast::<TOKEN_USER>().read_unaligned() };
349            Ok(user.User.Sid)
350        }
351    }
352
353    struct TokenHandle(HANDLE);
354
355    impl Drop for TokenHandle {
356        fn drop(&mut self) {
357            unsafe {
358                CloseHandle(self.0);
359            }
360        }
361    }
362
363    struct SecurityDescriptor(PSECURITY_DESCRIPTOR);
364
365    impl Drop for SecurityDescriptor {
366        fn drop(&mut self) {
367            unsafe {
368                LocalFree(self.0);
369            }
370        }
371    }
372}