use crate::cli::AgentArgs;
use crate::session;
use anyhow::{Context, Result, anyhow, bail};
use sefy_core::agent::{self, Client, Keeper, LockState, Seat, Taken};
use std::io::{BufRead, BufReader, IsTerminal, Read, Write};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::Duration;
use zeroize::Zeroizing;
const LONGEST_PASSWORD: usize = 64 * 1024;
const READY: &str = "ready";
const FAILED: &str = "failed";
pub fn run(args: AgentArgs, vault: Option<PathBuf>, password_env: Option<&str>) -> Result<()> {
if args.serve {
return serve(args.ttl);
}
let path = session::vault_path(vault)?;
if let Some((_, master)) = session::looking_through_agent(&path)? {
println!("already {}", session::lock_line(&path, Some(&master))?);
return Ok(());
}
let running = Client::of(Seat::Agent).and_then(|client| client.state());
if matches!(running, Some(LockState::Unlocked { .. })) {
bail!(
"an agent is running with another vault's password\n\
`sefy lock` stops it; an agent holds one password at a time"
);
}
let password = Zeroizing::new(session::password(password_env)?);
session::open(&path, &password)?;
let pid = start(&password, args.ttl, password_env)?;
println!(
"unlocked for {} of nobody using it, {} at most (agent pid {pid})",
crate::when::span(args.ttl),
crate::when::span(agent::CAP)
);
println!("`sefy lock` locks it now");
Ok(())
}
pub fn lock() -> Result<()> {
let locked = session::lock_everywhere();
if locked.is_empty() {
println!("nothing was unlocked");
} else {
println!("locked the {}", locked.join(" and the "));
}
Ok(())
}
fn start(password: &str, idle: Duration, password_env: Option<&str>) -> Result<u32> {
let program = std::env::current_exe().context("cannot find this program to start it")?;
let mut command = Command::new(program);
command
.args(["agent", "--serve", "--ttl"])
.arg(format!("{}s", idle.as_secs()))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.current_dir(neutral_directory())
.env_remove("SEFY_VAULT");
if let Some(variable) = password_env {
command.env_remove(variable);
}
detach(&mut command);
let mut child = command.spawn().context("cannot start the agent")?;
{
let mut stdin = child.stdin.take().expect("stdin was piped");
stdin
.write_all(password.as_bytes())
.context("cannot hand the password to the agent")?;
}
let stdout = child.stdout.take().expect("stdout was piped");
let mut line = String::new();
BufReader::new(stdout)
.read_line(&mut line)
.context("the agent did not answer")?;
let (word, rest) = line.trim().split_once(' ').unwrap_or((line.trim(), ""));
match word {
READY => rest.parse().context("the agent did not say who it is"),
FAILED => bail!("{rest}"),
_ => bail!("the agent stopped before it could listen"),
}
}
fn serve(idle: Duration) -> Result<()> {
if std::io::stdin().is_terminal() {
bail!("`sefy agent --serve` is what `sefy agent` starts; use that");
}
harden();
let mut password = Zeroizing::new(Vec::with_capacity(LONGEST_PASSWORD));
std::io::stdin()
.take(LONGEST_PASSWORD as u64)
.read_to_end(&mut password)
.context("cannot read the password")?;
if password.is_empty() {
println!("{FAILED} it was given no password");
bail!("given no password");
}
let keeper = Keeper::new();
keeper.give(password, idle);
keeper.when_locked(|| {
agent::leave(Seat::Agent);
std::process::exit(0);
});
if let Err(error) = agent::serve(Seat::Agent, keeper.clone()) {
println!("{FAILED} {}", describe(&error));
return Err(anyhow!("{error}"));
}
agent::start_ticking(keeper);
println!("{READY} {}", std::process::id());
std::io::stdout().flush()?;
loop {
std::thread::park();
}
}
fn describe(taken: &Taken) -> String {
match taken {
Taken::Occupied => "an agent is already running; `sefy lock` stops it".to_owned(),
other => other.to_string(),
}
}
fn harden() {
#[cfg(target_os = "linux")]
{
const PR_SET_DUMPABLE: std::ffi::c_int = 4;
unsafe extern "C" {
fn prctl(option: std::ffi::c_int, ...) -> std::ffi::c_int;
}
unsafe {
prctl(PR_SET_DUMPABLE, 0 as std::ffi::c_ulong);
}
}
}
fn neutral_directory() -> PathBuf {
#[cfg(windows)]
{
std::env::var_os("SystemRoot")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir)
}
#[cfg(not(windows))]
{
PathBuf::from("/")
}
}
#[cfg(windows)]
fn detach(command: &mut Command) {
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x0000_0008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
const STD_HANDLES: [u32; 3] = [-10i32 as u32, -11i32 as u32, -12i32 as u32];
const HANDLE_FLAG_INHERIT: u32 = 1;
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetStdHandle(which: u32) -> *mut std::ffi::c_void;
fn SetHandleInformation(handle: *mut std::ffi::c_void, mask: u32, flags: u32) -> i32;
}
command.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
unsafe {
for which in STD_HANDLES {
SetHandleInformation(GetStdHandle(which), HANDLE_FLAG_INHERIT, 0);
}
}
}
#[cfg(unix)]
fn detach(command: &mut Command) {
use std::os::unix::process::CommandExt;
unsafe extern "C" {
fn setsid() -> std::ffi::c_int;
}
unsafe {
command.pre_exec(|| {
setsid();
Ok(())
});
}
}