#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
use crate::error::Error;
use crate::vault::Vault;
use super::context::prepare_execution;
pub fn validate_env_var_name(name: &str) -> Result<(), Error> {
use crate::guard::env::{INJECT_ENV_VARS, SUSPICIOUS_ENV_VARS};
if INJECT_ENV_VARS.contains(&name) || SUSPICIOUS_ENV_VARS.contains(&name) {
return Err(Error::BinaryResolution(format!(
"env var '{name}' is on the injection blocklist; using it as a target would \
let the secret value redirect the loader."
)));
}
let mut chars = name.chars();
let Some(first) = chars.next() else {
return Err(Error::BinaryResolution(
"invalid env var name: empty string".to_string(),
));
};
if !(first.is_ascii_alphabetic() || first == '_') {
return Err(Error::BinaryResolution(format!(
"invalid env var name '{name}': must start with an ASCII letter or underscore"
)));
}
for c in chars {
if !(c.is_ascii_alphanumeric() || c == '_') {
return Err(Error::BinaryResolution(format!(
"invalid env var name '{name}': only ASCII alphanumerics and underscore allowed"
)));
}
}
Ok(())
}
#[must_use]
pub fn command_fingerprint(command: &[String]) -> String {
if command.len() <= 1 {
return String::new();
}
let canonical: String = command[1..]
.iter()
.map(|s| s.replace('\x1f', "\x1f\x1f"))
.collect::<Vec<_>>()
.join("\x1f");
hash_canonical_argv(&canonical)
}
#[allow(clippy::expect_used)] fn hash_canonical_argv(canonical: &str) -> String {
use hkdf::Hkdf;
use sha2::Sha256;
use std::fmt::Write;
const DOMAIN: &[u8] = b"envseal-argv-fingerprint-v1";
let hk = Hkdf::<Sha256>::new(Some(DOMAIN), canonical.as_bytes());
let mut out = [0u8; 32];
hk.expand(b"argv-fingerprint", &mut out)
.expect("HKDF expand of 32 bytes is mathematically infallible per RFC 5869");
let mut hex = String::with_capacity(out.len() * 2);
for b in &out {
let _ = write!(hex, "{b:02x}");
}
hex
}
pub struct InjectRequest<'a> {
pub secret_name: &'a str,
pub env_var: &'a str,
pub command: &'a [String],
}
#[derive(Debug, Clone, Copy, Default)]
pub struct InjectExecOptions {
pub isolate_stdio: bool,
}
pub fn execute(vault: &Vault, request: &InjectRequest) -> Result<(), Error> {
execute_with_options(vault, request, InjectExecOptions::default())
}
pub fn execute_with_options(
vault: &Vault,
request: &InjectRequest,
options: InjectExecOptions,
) -> Result<(), Error> {
execute_multi_with_options(
vault,
&[(request.secret_name, request.env_var)],
request.command,
options,
)
}
pub fn execute_multi(
vault: &Vault,
mappings: &[(&str, &str)],
command: &[String],
) -> Result<(), Error> {
execute_multi_with_options(vault, mappings, command, InjectExecOptions::default())
}
pub fn execute_multi_with_options(
vault: &Vault,
mappings: &[(&str, &str)],
command: &[String],
options: InjectExecOptions,
) -> Result<(), Error> {
crate::guard::check_self_preload()?;
crate::guard::harden_process();
let prepared = prepare_execution(vault, mappings, command)?;
let mut cmd = Command::new(&prepared.exec_path);
#[cfg(unix)]
cmd.arg0(&prepared.binary_path);
cmd.args(&prepared.args);
cmd.env_clear();
for (k, v) in &prepared.clean_env {
cmd.env(k, v);
}
for (var, val) in &prepared.env_pairs {
cmd.env(var, val.as_str());
}
if options.isolate_stdio {
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
}
let status = cmd.status().map_err(Error::ExecFailed)?;
if status.success() {
Ok(())
} else {
Err(Error::ExecFailed(std::io::Error::other(format!(
"child process exited with status {status}"
))))
}
}
pub fn execute_multi_capture_status(
vault: &Vault,
mappings: &[(&str, &str)],
command: &[String],
options: InjectExecOptions,
) -> Result<i32, Error> {
crate::guard::check_self_preload()?;
crate::guard::harden_process();
let prepared = prepare_execution(vault, mappings, command)?;
let mut cmd = Command::new(&prepared.exec_path);
#[cfg(unix)]
cmd.arg0(&prepared.binary_path);
cmd.args(&prepared.args);
cmd.env_clear();
for (k, v) in &prepared.clean_env {
cmd.env(k, v);
}
for (var, val) in &prepared.env_pairs {
cmd.env(var, val.as_str());
}
if options.isolate_stdio {
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
}
let status = cmd.status().map_err(Error::ExecFailed)?;
Ok(status.code().unwrap_or(-1))
}