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)]
181pub(crate) mod 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::{
188        CloseHandle, LocalFree, ERROR_SUCCESS, GENERIC_ALL, HANDLE,
189    };
190    use windows_sys::Win32::Security::Authorization::{
191        GetNamedSecurityInfoW, GetSecurityInfo, SetEntriesInAclW, SetNamedSecurityInfoW,
192        EXPLICIT_ACCESS_W, NO_MULTIPLE_TRUSTEE, SET_ACCESS, SE_FILE_OBJECT, TRUSTEE_IS_SID,
193        TRUSTEE_IS_USER, TRUSTEE_W,
194    };
195    use windows_sys::Win32::Security::{
196        EqualSid, GetAce, GetSecurityDescriptorControl, GetTokenInformation, TokenUser,
197        ACCESS_ALLOWED_ACE, ACL, DACL_SECURITY_INFORMATION, NO_INHERITANCE,
198        OWNER_SECURITY_INFORMATION, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR,
199        PSID, SE_DACL_PROTECTED, SUB_CONTAINERS_AND_OBJECTS_INHERIT, TOKEN_QUERY, TOKEN_USER,
200    };
201    use windows_sys::Win32::System::SystemServices::{
202        ACCESS_ALLOWED_ACE_TYPE, ACCESS_ALLOWED_CALLBACK_ACE_TYPE,
203        ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE, ACCESS_ALLOWED_COMPOUND_ACE_TYPE,
204        ACCESS_ALLOWED_OBJECT_ACE_TYPE,
205    };
206    use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
207
208    pub(crate) fn validate_path_owner_acl(path: &Path) -> SecurityResult<()> {
209        let wide = path
210            .as_os_str()
211            .encode_wide()
212            .chain(std::iter::once(0))
213            .collect::<Vec<_>>();
214        let mut owner = ptr::null_mut();
215        let mut dacl = ptr::null_mut();
216        let mut descriptor = ptr::null_mut();
217        let result = unsafe {
218            GetNamedSecurityInfoW(
219                wide.as_ptr(),
220                SE_FILE_OBJECT,
221                OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
222                &mut owner,
223                ptr::null_mut(),
224                &mut dacl,
225                ptr::null_mut(),
226                &mut descriptor,
227            )
228        };
229        validate_security_result(result, descriptor, owner, dacl)
230    }
231
232    pub(crate) fn validate_file_owner_acl(file: &File) -> SecurityResult<()> {
233        let mut owner = ptr::null_mut();
234        let mut dacl = ptr::null_mut();
235        let mut descriptor = ptr::null_mut();
236        let result = unsafe {
237            GetSecurityInfo(
238                file.as_raw_handle() as HANDLE,
239                SE_FILE_OBJECT,
240                OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
241                &mut owner,
242                ptr::null_mut(),
243                &mut dacl,
244                ptr::null_mut(),
245                &mut descriptor,
246            )
247        };
248        validate_security_result(result, descriptor, owner, dacl)
249    }
250
251    fn validate_security_result(
252        result: u32,
253        descriptor: PSECURITY_DESCRIPTOR,
254        owner: PSID,
255        dacl: *mut ACL,
256    ) -> SecurityResult<()> {
257        if result != ERROR_SUCCESS || descriptor.is_null() {
258            return Err(SecurityError::SecretUnavailable);
259        }
260        let descriptor = SecurityDescriptor(descriptor);
261        let validation = validate_owner_and_acl(descriptor.0, owner, dacl);
262        drop(descriptor);
263        validation
264    }
265
266    fn validate_owner_and_acl(
267        descriptor: PSECURITY_DESCRIPTOR,
268        owner: PSID,
269        dacl: *mut ACL,
270    ) -> SecurityResult<()> {
271        if owner.is_null() || dacl.is_null() {
272            return Err(SecurityError::InvalidSecretRef);
273        }
274        let mut control = 0;
275        let mut revision = 0;
276        if unsafe { GetSecurityDescriptorControl(descriptor, &mut control, &mut revision) } == 0
277            || control & SE_DACL_PROTECTED == 0
278        {
279            return Err(SecurityError::InvalidSecretRef);
280        }
281        let user = current_user()?;
282        if unsafe { EqualSid(owner, user.sid()?) } == 0 {
283            return Err(SecurityError::InvalidSecretRef);
284        }
285        validate_owner_only_acl(dacl, owner)
286    }
287
288    pub(crate) fn set_path_owner_acl(path: &Path, directory: bool) -> SecurityResult<()> {
289        let user = current_user()?;
290        let sid = user.sid()?;
291        let trustee = TRUSTEE_W {
292            pMultipleTrustee: ptr::null_mut(),
293            MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE,
294            TrusteeForm: TRUSTEE_IS_SID,
295            TrusteeType: TRUSTEE_IS_USER,
296            ptstrName: sid.cast(),
297        };
298        let entry = EXPLICIT_ACCESS_W {
299            grfAccessPermissions: GENERIC_ALL,
300            grfAccessMode: SET_ACCESS,
301            grfInheritance: if directory {
302                SUB_CONTAINERS_AND_OBJECTS_INHERIT
303            } else {
304                NO_INHERITANCE
305            },
306            Trustee: trustee,
307        };
308        let mut dacl = ptr::null_mut();
309        if unsafe { SetEntriesInAclW(1, &entry, ptr::null(), &mut dacl) } != ERROR_SUCCESS
310            || dacl.is_null()
311        {
312            return Err(SecurityError::SecretUnavailable);
313        }
314        let dacl = SecurityDescriptor(dacl.cast());
315        let mut wide = path
316            .as_os_str()
317            .encode_wide()
318            .chain(std::iter::once(0))
319            .collect::<Vec<_>>();
320        let result = unsafe {
321            SetNamedSecurityInfoW(
322                wide.as_mut_ptr(),
323                SE_FILE_OBJECT,
324                DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
325                ptr::null_mut(),
326                ptr::null_mut(),
327                dacl.0.cast(),
328                ptr::null(),
329            )
330        };
331        if result == ERROR_SUCCESS {
332            validate_path_owner_acl(path)
333        } else {
334            Err(SecurityError::SecretUnavailable)
335        }
336    }
337
338    fn validate_owner_only_acl(dacl: *mut ACL, owner: PSID) -> SecurityResult<()> {
339        let mut owner_allowed = false;
340        let ace_count = unsafe { (*dacl).AceCount };
341        for index in 0..u32::from(ace_count) {
342            let mut raw_ace: *mut c_void = ptr::null_mut();
343            if unsafe { GetAce(dacl, index, &mut raw_ace) } == 0 || raw_ace.is_null() {
344                return Err(SecurityError::SecretUnavailable);
345            }
346            let ace_type = unsafe { (*(raw_ace.cast::<ACCESS_ALLOWED_ACE>())).Header.AceType };
347            if ace_type == ACCESS_ALLOWED_ACE_TYPE as u8 {
348                let ace = raw_ace.cast::<ACCESS_ALLOWED_ACE>();
349                let sid = unsafe { ptr::addr_of_mut!((*ace).SidStart).cast::<c_void>() };
350                if unsafe { EqualSid(owner, sid) } == 0 {
351                    return Err(SecurityError::InvalidSecretRef);
352                }
353                owner_allowed = true;
354            } else if is_other_allow_ace(ace_type) {
355                return Err(SecurityError::InvalidSecretRef);
356            }
357        }
358        if owner_allowed {
359            Ok(())
360        } else {
361            Err(SecurityError::InvalidSecretRef)
362        }
363    }
364
365    fn is_other_allow_ace(ace_type: u8) -> bool {
366        [
367            ACCESS_ALLOWED_COMPOUND_ACE_TYPE,
368            ACCESS_ALLOWED_OBJECT_ACE_TYPE,
369            ACCESS_ALLOWED_CALLBACK_ACE_TYPE,
370            ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE,
371        ]
372        .contains(&u32::from(ace_type))
373    }
374
375    fn current_user() -> SecurityResult<TokenUser> {
376        let mut token: HANDLE = ptr::null_mut();
377        if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
378            return Err(SecurityError::SecretUnavailable);
379        }
380        let token = TokenHandle(token);
381        let mut required = 0;
382        unsafe {
383            GetTokenInformation(token.0, TokenUser, ptr::null_mut(), 0, &mut required);
384        }
385        if required == 0 {
386            return Err(SecurityError::SecretUnavailable);
387        }
388        let mut buffer = vec![0_u8; required as usize];
389        if unsafe {
390            GetTokenInformation(
391                token.0,
392                TokenUser,
393                buffer.as_mut_ptr().cast(),
394                required,
395                &mut required,
396            )
397        } == 0
398        {
399            return Err(SecurityError::SecretUnavailable);
400        }
401        Ok(TokenUser { buffer })
402    }
403
404    struct TokenUser {
405        buffer: Vec<u8>,
406    }
407
408    impl TokenUser {
409        fn sid(&self) -> SecurityResult<PSID> {
410            if self.buffer.len() < std::mem::size_of::<TOKEN_USER>() {
411                return Err(SecurityError::SecretUnavailable);
412            }
413            // SAFETY: GetTokenInformation initialized at least TOKEN_USER bytes.
414            // The byte buffer has no TOKEN_USER alignment guarantee, so use an
415            // unaligned value read instead of constructing a misaligned reference.
416            let user = unsafe { self.buffer.as_ptr().cast::<TOKEN_USER>().read_unaligned() };
417            Ok(user.User.Sid)
418        }
419    }
420
421    struct TokenHandle(HANDLE);
422
423    impl Drop for TokenHandle {
424        fn drop(&mut self) {
425            unsafe {
426                CloseHandle(self.0);
427            }
428        }
429    }
430
431    struct SecurityDescriptor(PSECURITY_DESCRIPTOR);
432
433    impl Drop for SecurityDescriptor {
434        fn drop(&mut self) {
435            unsafe {
436                LocalFree(self.0);
437            }
438        }
439    }
440}