use std::path::PathBuf;
use zeroize::Zeroizing;
use crate::error::Error;
use crate::guard;
use crate::gui::{self, Approval};
use crate::policy::{self, Policy};
use crate::vault::Vault;
pub struct PreparedExecution {
pub binary_path: String,
pub exec_path: String,
pub args: Vec<String>,
pub clean_env: std::collections::HashMap<String, String>,
pub env_pairs: Vec<(String, Zeroizing<String>)>,
#[allow(dead_code)]
pinned_file: Option<std::fs::File>,
}
impl PreparedExecution {
#[cfg(target_os = "linux")]
#[must_use]
pub fn pinned_target_fd(&self) -> std::os::fd::RawFd {
use std::os::fd::AsRawFd;
self.pinned_file.as_ref().map_or(-1, AsRawFd::as_raw_fd)
}
}
#[allow(clippy::too_many_lines)]
pub fn prepare_execution(
vault: &Vault,
mappings: &[(&str, &str)],
command: &[String],
) -> Result<PreparedExecution, Error> {
let mut sec_config =
crate::security_config::load_config(vault.root(), vault.master_key_bytes())?;
guard::enforce_env_policy(&sec_config)?;
let gui_signals = guard::assess_gui_signals(&guard::DetectorContext::ambient());
guard::emit_signals_inline(gui_signals, &sec_config)?;
if command.is_empty() {
return Err(Error::BinaryResolution(
"no command specified after --".to_string(),
));
}
for &(_, env_var) in mappings {
super::inject::validate_env_var_name(env_var)?;
}
for &(secret_name, _) in mappings {
if secret_name.eq_ignore_ascii_case("ctf-flag") {
let _ = crate::audit::log_required(&crate::audit::AuditEvent::SignalRecorded {
tier: format!("{:?}", sec_config.tier),
classification: format!(
"critical [ctf.flag.injection_attempt] refused to inject 'ctf-flag' \
into '{}' — flag is structurally non-injectable",
command.first().map_or("<no-command>", String::as_str)
),
});
return Err(Error::EnvironmentCompromised(
"ctf-flag is non-injectable by design. The CTF flag exists only \
for `envseal ctf verify` hash comparison; envseal refuses to \
hand its plaintext to any child process. Run `envseal ctf reset` \
to clear the challenge."
.to_string(),
));
}
}
let missing: Vec<String> = mappings
.iter()
.filter(|(name, _)| !vault.has_secret(name))
.map(|(name, env_var)| format!("{env_var}={name}"))
.collect();
if !missing.is_empty() {
return Err(Error::SecretNotFound(format!(
"the following .envseal mapping(s) reference secrets that aren't in the vault: \
{} — store them with `envseal store <name>` or remove the line(s) from .envseal",
missing.join(", ")
)));
}
let binary_path = policy::resolve_binary(&command[0])?;
let policy_path = vault.policy_path();
let mut policy = Policy::load_sealed(&policy_path, vault.master_key_bytes())?;
if policy.generation < sec_config.policy_generation {
return Err(Error::EnvironmentCompromised(format!(
"policy rollback detected: loaded generation {} is older than expected {}",
policy.generation, sec_config.policy_generation
)));
}
let binary_path_buf = PathBuf::from(&binary_path);
#[allow(unused_mut, unused_assignments)]
let mut exec_path = binary_path.clone();
#[allow(unused_mut, unused_assignments)]
let mut kept_alive_file: Option<std::fs::File> = None;
#[cfg(target_os = "linux")]
{
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(&binary_path_buf)
.map_err(|e| {
Error::BinaryResolution(format!("failed to open binary for TOCTOU prevention: {e}"))
})?;
exec_path = format!("/proc/self/fd/{}", file.as_raw_fd());
kept_alive_file = Some(file);
}
#[cfg(all(unix, not(target_os = "linux")))]
{
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(&binary_path_buf)
.map_err(|e| {
Error::BinaryResolution(format!("failed to open binary for TOCTOU prevention: {e}"))
})?;
exec_path = format!("/dev/fd/{}", file.as_raw_fd());
kept_alive_file = Some(file);
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
use std::os::windows::io::FromRawHandle;
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, OPEN_EXISTING,
};
let wide: Vec<u16> = binary_path_buf
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let handle = unsafe {
CreateFileW(
wide.as_ptr(),
0x8000_0000, FILE_SHARE_READ, std::ptr::null_mut(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
return Err(Error::BinaryResolution(format!(
"failed to open binary for TOCTOU prevention: {}",
std::io::Error::last_os_error()
)));
}
let file = unsafe { std::fs::File::from_raw_handle(handle.cast::<std::ffi::c_void>()) };
exec_path.clone_from(&binary_path);
kept_alive_file = Some(file);
}
if let Some(stored_hash) = policy.binary_hash(&binary_path) {
if let Some(ref mut file) = kept_alive_file {
guard::verify_file_hash(file, &stored_hash, &binary_path_buf)?;
} else {
guard::verify_binary_hash(&binary_path_buf, &stored_hash)?;
}
}
let fingerprint_str = super::inject::command_fingerprint(command);
let fingerprint_for_lookup = if fingerprint_str.is_empty() {
None
} else {
Some(fingerprint_str.as_str())
};
let mut current_hash: Option<String> = None;
let mut policy_dirty = false;
for &(secret_name, env_var) in mappings {
if current_hash.is_none() {
let hash = if let Some(ref mut file) = kept_alive_file {
guard::hash_open_file(file)?
} else {
guard::hash_binary(std::path::Path::new(&binary_path))?
};
current_hash = Some(hash);
}
if policy.is_authorized_with_hash_and_args(
&binary_path,
secret_name,
fingerprint_for_lookup,
current_hash.as_deref(),
) {
continue;
}
let approval =
gui::request_approval(&binary_path, command, secret_name, env_var, &sec_config)?;
match approval {
Approval::AllowOnce => {}
Approval::AllowAlways => {
let hash = current_hash.as_deref().unwrap_or("");
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let default_expiry = sec_config.default_rule_expiry_secs;
let max_expiry = sec_config.max_rule_expiry_secs;
let effective_expiry = match (default_expiry, max_expiry) {
(None, None) => None,
(Some(d), None) => Some(d),
(None, Some(m)) => Some(m),
(Some(d), Some(m)) => Some(d.min(m)),
};
if let Some(secs) = effective_expiry {
policy.allow_key_with_expiry(
&binary_path,
secret_name,
hash,
fingerprint_for_lookup.map(str::to_string),
now.saturating_add(secs),
);
} else {
policy.allow_key_with_hash_and_args(
&binary_path,
secret_name,
hash,
fingerprint_for_lookup.map(str::to_string),
);
}
policy_dirty = true;
}
Approval::Deny => {
if policy_dirty {
policy.save_sealed(&policy_path, vault.master_key_bytes())?;
sec_config.policy_generation = policy.generation;
crate::security_config::save_config(
vault.root(),
&sec_config,
vault.master_key_bytes(),
)?;
}
return Err(Error::UserDenied);
}
}
}
if policy_dirty {
policy.save_sealed(&policy_path, vault.master_key_bytes())?;
sec_config.policy_generation = policy.generation;
crate::security_config::save_config(vault.root(), &sec_config, vault.master_key_bytes())?;
}
let mut env_pairs: Vec<(String, Zeroizing<String>)> = Vec::with_capacity(mappings.len());
for &(secret_name, env_var) in mappings {
let plaintext = vault.decrypt(secret_name)?;
let value = Zeroizing::new(String::from_utf8(plaintext.to_vec()).map_err(|e| {
Error::CryptoFailure(format!("secret '{secret_name}' is not valid UTF-8: {e}"))
})?);
env_pairs.push((env_var.to_string(), value));
}
crate::audit::log_required(&crate::audit::AuditEvent::SecretAccessed {
binary: binary_path.clone(),
secret: mappings
.iter()
.map(|(s, _)| (*s).to_string())
.collect::<Vec<_>>()
.join(","),
})?;
let clean_env = guard::sanitized_env();
Ok(PreparedExecution {
binary_path,
exec_path,
args: command[1..].to_vec(),
clean_env,
env_pairs,
pinned_file: kept_alive_file,
})
}
#[cfg(target_os = "linux")]
pub fn harden_child_process_inner() -> std::io::Result<()> {
unsafe {
if libc::prctl(libc::PR_SET_DUMPABLE, 0) < 0 {
return Err(std::io::Error::last_os_error());
}
let rlimit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if libc::setrlimit(libc::RLIMIT_CORE, &rlimit) < 0 {
return Err(std::io::Error::last_os_error());
}
if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn harden_child_process_inner() -> std::io::Result<()> {
Ok(())
}