use crate::store::{self, Store};
use crate::{agent, broker, brokered, read_secret};
use crate::{held_token, without_credentials};
use anyhow::{Context, Result};
use std::io::Write;
const EXEC_GRANT_SECS: u64 = 30;
fn batch_token(store: &Store, names: &[&str]) -> Option<String> {
let mut names: Vec<String> = names.iter().copied().map(String::from).collect();
names.sort();
names.dedup();
if names.len() < 2 || !brokered(store) || held_token().is_some() {
return None;
}
let answer = broker::grant(store, &names, &agent(), EXEC_GRANT_SECS).ok()?;
answer.lines().next().map(str::to_string)
}
struct SecretFiles {
dir: std::path::PathBuf,
}
impl SecretFiles {
fn new() -> Result<Self> {
use std::os::unix::fs::DirBuilderExt;
let dir = std::env::temp_dir().join(format!("passbox-{}", store::new_id()));
std::fs::DirBuilder::new().mode(0o700).create(&dir)?;
Ok(SecretFiles { dir })
}
fn is_empty(&self) -> bool {
std::fs::read_dir(&self.dir).is_ok_and(|mut d| d.next().is_none())
}
fn write(&self, index: usize, value: &str) -> Result<std::path::PathBuf> {
let path = self.dir.join(index.to_string());
let mut bytes = value.as_bytes().to_vec();
if !bytes.ends_with(b"\n") {
bytes.push(b'\n');
}
store::write_private(&path, &bytes)?;
zeroize::Zeroize::zeroize(&mut bytes);
Ok(path)
}
}
impl Drop for SecretFiles {
fn drop(&mut self) {
if let Ok(entries) = std::fs::read_dir(&self.dir) {
for entry in entries.flatten() {
let len = entry
.metadata()
.ok()
.and_then(|m| usize::try_from(m.len()).ok())
.unwrap_or(0);
let _ = std::fs::write(entry.path(), vec![0u8; len]);
}
}
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn split_pairs(pairs: &[String]) -> Result<Vec<(&str, &str)>> {
pairs
.iter()
.map(|pair| {
pair.split_once('=')
.with_context(|| format!("expected VAR=secret, got {pair}"))
})
.collect()
}
pub(crate) fn exec(
store: &Store,
envs: &[String],
files: &[String],
stdin: Option<&str>,
command: &[String],
) -> Result<()> {
let (program, args) = command.split_first().context("no command to run")?;
let mut child = std::process::Command::new(program);
without_credentials(&mut child).args(args);
let pairs = split_pairs(envs)?;
let file_pairs = split_pairs(files)?;
let names: Vec<&str> = pairs
.iter()
.chain(&file_pairs)
.map(|(_, name)| *name)
.chain(stdin)
.collect();
let token = batch_token(store, &names);
let read = |name: &str| match &token {
Some(token) => broker::request_token(store, name, &agent(), token),
None => read_secret(store, name),
};
for (var, name) in pairs {
child.env(var, read(name)?);
}
let secret_files = SecretFiles::new()?;
for (index, (var, name)) in file_pairs.into_iter().enumerate() {
let mut value = read(name)?;
let path = secret_files.write(index, &value);
zeroize::Zeroize::zeroize(&mut value);
child.env(var, path?);
}
let piped = match stdin {
Some(name) => Some(read(name)?),
None => None,
};
child.stdin(if piped.is_some() {
std::process::Stdio::piped()
} else {
std::process::Stdio::inherit()
});
if !secret_files.is_empty() {
extern "C" fn nothing(_: libc::c_int) {}
#[expect(
clippy::as_conversions,
reason = "signal(2) takes the handler as an address, which only `as` produces"
)]
let handler = nothing as extern "C" fn(libc::c_int) as libc::sighandler_t;
for signal in [libc::SIGINT, libc::SIGHUP, libc::SIGTERM] {
unsafe { libc::signal(signal, handler) };
}
}
let mut running = child
.spawn()
.with_context(|| format!("could not run {program}"))?;
if let Some(value) = piped {
running
.stdin
.as_mut()
.expect("piped")
.write_all(value.as_bytes())?;
drop(running.stdin.take());
}
let status = running.wait()?;
drop(secret_files);
std::process::exit(status.code().unwrap_or(1));
}
pub(crate) fn run_child(
command: &[String],
env_var: Option<&str>,
value: &str,
) -> Result<std::process::Output> {
let (program, args) = command.split_first().context("no command to run")?;
let mut child = std::process::Command::new(program);
without_credentials(&mut child)
.args(args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
match env_var {
Some(var) => {
child.env(var, value);
child.stdin(std::process::Stdio::null());
}
None => {
child.stdin(std::process::Stdio::piped());
}
}
let mut running = child
.spawn()
.with_context(|| format!("could not run {program}"))?;
if env_var.is_none() {
running
.stdin
.as_mut()
.expect("piped")
.write_all(value.as_bytes())?;
drop(running.stdin.take());
}
Ok(running.wait_with_output()?)
}