use std::collections::HashMap;
use std::process::{Command, Stdio};
use anyhow::{bail, Result};
use clap::Args;
use crate::cli::input::PayloadFormat;
use crate::config::Manifest;
use crate::crypto::envelope::Envelope;
use crate::crypto::signing::SignedEnvelope;
use crate::env;
use crate::keys;
use crate::transfer;
use crate::ui::display;
#[derive(Args)]
pub struct InjectArgs {
pub code: Option<String>,
#[arg(long)]
pub listen: bool,
#[arg(
last = true,
required = true,
value_name = "CMD",
num_args = 1..,
)]
pub command: Vec<String>,
#[arg(long, env = "ENSEAL_RELAY")]
pub relay: Option<String>,
#[arg(long, short)]
pub quiet: bool,
}
pub async fn run(args: InjectArgs) -> Result<()> {
if args.command.is_empty() {
bail!("no command specified. Usage: enseal inject <code> -- <command>");
}
if !args.listen && args.code.is_none() {
bail!("provide a wormhole code or use --listen. Usage: enseal inject <code> -- <command>");
}
if args.listen && args.code.is_some() {
bail!("--listen and a wormhole code are mutually exclusive");
}
let manifest = crate::config::Manifest::load(None)?;
let effective_relay: Option<String> = args.relay.clone().or(manifest.defaults.relay.clone());
let envelope = if args.listen {
listen_mode(&args, effective_relay.as_deref()).await?
} else {
receive_envelope(&args, effective_relay.as_deref()).await?
};
if !args.quiet {
if let PayloadFormat::Env | PayloadFormat::Kv = envelope.format {
validate_against_schema(&envelope.payload, manifest);
}
}
let secrets = extract_secrets(&envelope)?;
if !args.quiet {
display::info("Secrets:", &format!("{} variables", secrets.len()));
display::ok("injecting into process environment");
}
run_child(&args.command, &secrets)
}
async fn receive_envelope(args: &InjectArgs, relay_url: Option<&str>) -> Result<Envelope> {
let code = args
.code
.as_deref()
.expect("code required in non-listen mode");
let is_file = std::path::Path::new(code).exists() && code.ends_with(".age");
if is_file {
let store = keys::store::KeyStore::open()?;
let own_identity = keys::identity::EnsealIdentity::load(&store)?;
let path = std::path::Path::new(code);
let metadata = std::fs::metadata(path)?;
if metadata.len() > 16 * 1024 * 1024 {
bail!(
"file too large ({} bytes, max 16 MiB): {}",
metadata.len(),
path.display()
);
}
let data = std::fs::read(path)?;
let signed = SignedEnvelope::from_bytes(&data)?;
let trusted_sender = keys::find_trusted_sender(&store, &signed);
let (envelope, sender_pubkey) =
transfer::filedrop::read_from_bytes(&data, &own_identity, trusted_sender.as_ref())?;
if !args.quiet {
if let Some(ref trusted) = trusted_sender {
display::info("From:", &trusted.identity);
} else {
display::warning(&format!(
"received from unknown sender (signing key: {}...)",
&sender_pubkey[..20.min(sender_pubkey.len())]
));
}
display::ok("signature verified, file decrypted");
}
Ok(envelope)
} else if let Some(relay_url) = relay_url {
let data = transfer::relay::receive(relay_url, code).await?;
let store = keys::store::KeyStore::open()?;
if store.is_initialized() {
if let Ok(signed) = SignedEnvelope::from_bytes(&data) {
let own_identity = keys::identity::EnsealIdentity::load(&store)?;
let sender_pubkey = signed.sender_sign_pubkey.clone();
let trusted_sender = keys::find_trusted_sender(&store, &signed);
let inner_bytes = signed.open(&own_identity, trusted_sender.as_ref())?;
let envelope = Envelope::from_bytes(&inner_bytes)?;
envelope.check_age(300)?;
if !args.quiet {
if let Some(ref trusted) = trusted_sender {
display::info("From:", &trusted.identity);
} else {
display::warning(&format!(
"received from unknown sender (signing key: {}...)",
&sender_pubkey[..20.min(sender_pubkey.len())]
));
}
display::ok("signature verified");
}
return Ok(envelope);
}
}
if !args.quiet {
display::warning(
"received unsigned (anonymous) payload -- sender identity not verified",
);
}
let envelope = Envelope::from_bytes(&data)?;
envelope.check_age(300)?;
Ok(envelope)
} else {
let data = transfer::wormhole::receive_raw(code, None).await?;
let store = keys::store::KeyStore::open()?;
if store.is_initialized() {
if let Ok(signed) = SignedEnvelope::from_bytes(&data) {
let own_identity = keys::identity::EnsealIdentity::load(&store)?;
let sender_pubkey = signed.sender_sign_pubkey.clone();
let trusted_sender = keys::find_trusted_sender(&store, &signed);
let inner_bytes = signed.open(&own_identity, trusted_sender.as_ref())?;
let envelope = Envelope::from_bytes(&inner_bytes)?;
envelope.check_age(300)?;
if !args.quiet {
if let Some(ref trusted) = trusted_sender {
display::info("From:", &trusted.identity);
} else {
display::warning(&format!(
"received from unknown sender (signing key: {}...)",
&sender_pubkey[..20.min(sender_pubkey.len())]
));
}
display::ok("signature verified");
}
return Ok(envelope);
}
}
if !args.quiet {
display::warning(
"received unsigned (anonymous) payload -- sender identity not verified",
);
}
let envelope = Envelope::from_bytes(&data)?;
envelope.check_age(300)?;
Ok(envelope)
}
}
async fn listen_mode(args: &InjectArgs, relay_url: Option<&str>) -> Result<Envelope> {
let relay_url =
relay_url.ok_or_else(|| anyhow::anyhow!("--listen requires --relay or ENSEAL_RELAY"))?;
let store = keys::store::KeyStore::open()?;
let own_identity = keys::identity::EnsealIdentity::load(&store)?;
let channel_id = own_identity.channel_id();
if !args.quiet {
display::info("Listening on:", relay_url);
display::info("Channel:", &channel_id[..12]);
display::ok("waiting for incoming transfer...");
}
let data = transfer::relay::listen(relay_url, &channel_id).await?;
let signed = SignedEnvelope::from_bytes(&data)?;
let sender_pubkey = signed.sender_sign_pubkey.clone();
let trusted_sender = keys::find_trusted_sender(&store, &signed);
let inner_bytes = signed.open(&own_identity, trusted_sender.as_ref())?;
let envelope = Envelope::from_bytes(&inner_bytes)?;
envelope.check_age(300)?;
if !args.quiet {
if let Some(ref trusted) = trusted_sender {
display::info("From:", &trusted.identity);
} else {
display::warning(&format!(
"received from unknown sender (signing key: {}...)",
&sender_pubkey[..20.min(sender_pubkey.len())]
));
}
display::ok("signature verified");
}
Ok(envelope)
}
fn extract_secrets(envelope: &Envelope) -> Result<HashMap<String, String>> {
let mut secrets = HashMap::new();
match envelope.format {
PayloadFormat::Env | PayloadFormat::Kv => {
let env_file = crate::env::parser::parse(&envelope.payload)?;
for (key, value) in env_file.vars() {
secrets.insert(key.to_string(), value.to_string());
}
}
PayloadFormat::Raw => {
if let Some(ref label) = envelope.metadata.label {
if label.is_empty()
|| label.starts_with(|c: char| c.is_ascii_digit())
|| !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
{
bail!(
"label '{}' is not a valid env var name (use A-Z, 0-9, _). \
Sender should use --as KEY instead",
label
);
}
secrets.insert(label.clone(), envelope.payload.clone());
} else {
bail!(
"cannot inject raw payload without a key name. \
Sender should use --as KEY or --label KEY"
);
}
}
}
if secrets.is_empty() {
bail!("no secrets found in received payload");
}
Ok(secrets)
}
fn run_child(command: &[String], secrets: &HashMap<String, String>) -> Result<()> {
let mut child = Command::new(&command[0])
.args(&command[1..])
.envs(secrets)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| anyhow::anyhow!("failed to start '{}': {}", command[0], e))?;
#[cfg(unix)]
{
setup_signal_forwarding(child.id());
}
let status = child.wait()?;
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(sig) = status.signal() {
unsafe {
libc::signal(sig, libc::SIG_DFL);
libc::raise(sig);
}
}
}
let _ = std::io::Write::flush(&mut std::io::stderr());
let _ = std::io::Write::flush(&mut std::io::stdout());
std::process::exit(status.code().unwrap_or(1));
}
#[cfg(unix)]
fn setup_signal_forwarding(child_pid: u32) {
use std::sync::atomic::{AtomicU32, Ordering};
static CHILD_PID: AtomicU32 = AtomicU32::new(0);
CHILD_PID.store(child_pid, Ordering::SeqCst);
unsafe {
let mut sa: libc::sigaction = std::mem::zeroed();
sa.sa_sigaction = forward_signal as *const () as libc::sighandler_t;
sa.sa_flags = libc::SA_RESTART;
libc::sigemptyset(&mut sa.sa_mask);
libc::sigaction(libc::SIGINT, &sa, std::ptr::null_mut());
libc::sigaction(libc::SIGTERM, &sa, std::ptr::null_mut());
}
extern "C" fn forward_signal(sig: libc::c_int) {
let pid = CHILD_PID.load(std::sync::atomic::Ordering::SeqCst);
if pid != 0 {
unsafe {
libc::kill(pid as libc::pid_t, sig);
}
}
}
}
fn validate_against_schema(payload: &str, manifest: Manifest) {
let schema = match env::schema::load_schema(None) {
Ok(Some(s)) => s,
_ => {
match manifest.schema {
Some(s) => s,
None => return,
}
}
};
let env_file = match env::parser::parse(payload) {
Ok(f) => f,
Err(_) => return,
};
let errors = env::schema::validate(&env_file, &schema);
if !errors.is_empty() {
display::warning("payload has schema validation issues:");
for err in &errors {
display::warning(&format!(" {}", err));
}
}
}