envseal 0.3.8

Write-only secret vault with process-level access control — post-agent secret management
Documentation
//! Windows NTFS ACL helpers — the 0o600-equivalent for vault files.
//!
//! Unix `chmod 0o600` says "owner can read/write; nobody else can do
//! anything." On NTFS the same intent is expressed as a discretionary ACL
//! (DACL) containing exactly one access-allowed ACE for the file owner,
//! flagged `PROTECTED_DACL_SECURITY_INFORMATION` so it does not inherit
//! permissive entries from the parent directory.
//!
//! [`set_owner_only_dacl`] is the entry point. It is called by
//! [`super::rules::Policy::save`] / [`super::rules::Policy::save_signed`]
//! and (transitively) every vault file that needs to be owner-private.

#![cfg(windows)]

use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::ptr;

use windows_sys::Win32::Foundation::{
    CloseHandle, GetLastError, LocalFree, ERROR_INSUFFICIENT_BUFFER, ERROR_SUCCESS, GENERIC_ALL,
    HANDLE, HLOCAL, INVALID_HANDLE_VALUE,
};
use windows_sys::Win32::Security::Authorization::{
    SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W, GRANT_ACCESS, NO_MULTIPLE_TRUSTEE,
    SE_FILE_OBJECT, TRUSTEE_IS_SID, TRUSTEE_IS_USER, TRUSTEE_W,
};
use windows_sys::Win32::Security::{
    GetTokenInformation, TokenUser, ACL, DACL_SECURITY_INFORMATION, NO_INHERITANCE,
    PROTECTED_DACL_SECURITY_INFORMATION, TOKEN_QUERY, TOKEN_USER,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};

use crate::error::Error;

/// Replace `path`'s DACL with one ACE that grants full access to the
/// current process owner only. The DACL is marked protected so the parent
/// directory's ACL does not re-introduce broader access.
pub fn set_owner_only_dacl(path: &Path) -> Result<(), Error> {
    let owner_sid = OwnerSid::current()?;

    // Build a single explicit access entry: GRANT, GENERIC_ALL, the owner's
    // SID, no inheritance.
    let explicit = EXPLICIT_ACCESS_W {
        grfAccessPermissions: GENERIC_ALL,
        grfAccessMode: GRANT_ACCESS,
        grfInheritance: NO_INHERITANCE,
        Trustee: TRUSTEE_W {
            pMultipleTrustee: ptr::null_mut(),
            MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE,
            TrusteeForm: TRUSTEE_IS_SID,
            TrusteeType: TRUSTEE_IS_USER,
            // The TRUSTEE form is `TRUSTEE_IS_SID`, so `ptstrName` is
            // reinterpreted as a SID pointer rather than a name string.
            // The function signature still types it as `*mut u16`.
            ptstrName: owner_sid.as_ptr() as *mut u16,
        },
    };

    // Build the ACL. SetEntriesInAclW allocates via LocalAlloc; we must
    // LocalFree it after we're done.
    let mut new_acl: *mut ACL = ptr::null_mut();
    let rc = unsafe { SetEntriesInAclW(1, &explicit, ptr::null_mut(), &mut new_acl) };
    if rc != ERROR_SUCCESS || new_acl.is_null() {
        return Err(Error::StorageIo(std::io::Error::other(format!(
            "SetEntriesInAclW failed: rc={rc}"
        ))));
    }
    let _acl_guard = LocalFreeOnDrop(new_acl.cast());

    // Apply the protected DACL to the file by name.
    let wide: Vec<u16> = path
        .as_os_str()
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    let info = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION;
    let rc = unsafe {
        SetNamedSecurityInfoW(
            wide.as_ptr(),
            SE_FILE_OBJECT,
            info,
            ptr::null_mut(), // owner — leave as-is
            ptr::null_mut(), // group — leave as-is
            new_acl,         // dacl
            ptr::null_mut(), // sacl
        )
    };
    if rc != ERROR_SUCCESS {
        return Err(Error::StorageIo(std::io::Error::other(format!(
            "SetNamedSecurityInfoW({}) failed: rc={rc}",
            path.display()
        ))));
    }

    Ok(())
}

/// RAII wrapper around the current process owner's SID.
///
/// The `TOKEN_USER` buffer that holds the SID must outlive the
/// `EXPLICIT_ACCESS_W` that points into it.
struct OwnerSid {
    buffer: Vec<u8>,
}

impl OwnerSid {
    fn current() -> Result<Self, Error> {
        let mut token: HANDLE = INVALID_HANDLE_VALUE;
        let process = unsafe { GetCurrentProcess() };
        if unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } == 0 {
            return Err(Error::StorageIo(last_os_error("OpenProcessToken")));
        }
        let _token_guard = HandleGuard(token);

        // Probe required size.
        let mut needed: u32 = 0;
        let probe =
            unsafe { GetTokenInformation(token, TokenUser, ptr::null_mut(), 0, &mut needed) };
        if probe == 0 {
            let err = unsafe { GetLastError() };
            if err != ERROR_INSUFFICIENT_BUFFER {
                return Err(Error::StorageIo(std::io::Error::other(format!(
                    "GetTokenInformation probe failed: rc={err}"
                ))));
            }
        }

        let mut buffer = vec![0u8; needed as usize];
        if unsafe {
            GetTokenInformation(
                token,
                TokenUser,
                buffer.as_mut_ptr().cast(),
                needed,
                &mut needed,
            )
        } == 0
        {
            return Err(Error::StorageIo(last_os_error("GetTokenInformation")));
        }

        Ok(Self { buffer })
    }

    /// Pointer to the SID inside the `TOKEN_USER` buffer.
    fn as_ptr(&self) -> *const std::ffi::c_void {
        // SAFETY: `GetTokenInformation(TokenUser, …)` returns a `TOKEN_USER`
        // structure starting at offset 0 of the buffer it filled. Its
        // alignment requirement matches the API contract; the buffer is
        // produced by a Windows API call rather than `Vec<u8>` reuse, so
        // the cast through a `*const u8` is necessary but the pointer is
        // sufficiently aligned in practice. We allow the lint here.
        #[allow(clippy::cast_ptr_alignment)]
        let token_user = self.buffer.as_ptr().cast::<TOKEN_USER>();
        unsafe { (*token_user).User.Sid.cast() }
    }
}

struct HandleGuard(HANDLE);
impl Drop for HandleGuard {
    fn drop(&mut self) {
        if self.0 != INVALID_HANDLE_VALUE {
            unsafe {
                let _ = CloseHandle(self.0);
            }
        }
    }
}

struct LocalFreeOnDrop(HLOCAL);
impl Drop for LocalFreeOnDrop {
    fn drop(&mut self) {
        if !self.0.is_null() {
            unsafe {
                LocalFree(self.0);
            }
        }
    }
}

fn last_os_error(label: &str) -> std::io::Error {
    let err = unsafe { GetLastError() };
    std::io::Error::other(format!("{label} failed: rc={err}"))
}