use anyhow::{bail, Context, Result};
use clap::Args;
use crate::cli::input::PayloadFormat;
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 ReceiveArgs {
pub code: String,
#[arg(long)]
pub output: Option<String>,
#[arg(long)]
pub clipboard: bool,
#[arg(long)]
pub no_write: bool,
#[arg(long, env = "ENSEAL_RELAY")]
pub relay: Option<String>,
#[arg(long)]
pub force: bool,
#[arg(long, short)]
pub quiet: bool,
}
pub async fn run(args: ReceiveArgs) -> Result<()> {
let manifest = crate::config::Manifest::load(None)?;
let effective_relay: Option<String> = args.relay.clone().or(manifest.defaults.relay.clone());
let is_file = std::path::Path::new(&args.code).exists() && args.code.ends_with(".age");
let envelope = if is_file {
receive_filedrop(&args)?
} else {
receive_wormhole(&args, effective_relay.as_deref()).await?
};
output_envelope(&args, &envelope)
}
async fn receive_wormhole(args: &ReceiveArgs, relay_url: Option<&str>) -> Result<Envelope> {
if let Some(relay_url) = relay_url {
let data = transfer::relay::receive(relay_url, &args.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_sign_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_sign_pubkey[..20.min(sender_sign_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)?;
return Ok(envelope);
}
let data = transfer::wormhole::receive_raw(&args.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_sign_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_sign_pubkey[..20.min(sender_sign_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)
}
fn receive_filedrop(args: &ReceiveArgs) -> Result<Envelope> {
let store = keys::store::KeyStore::open()?;
let own_identity = keys::identity::EnsealIdentity::load(&store)?;
let path = std::path::Path::new(&args.code);
let metadata = std::fs::metadata(path)
.with_context(|| format!("failed to read file: {}", path.display()))?;
if metadata.len() > 16 * 1024 * 1024 {
bail!(
"file too large ({} bytes, max 16 MiB): {}",
metadata.len(),
path.display()
);
}
let data =
std::fs::read(path).with_context(|| format!("failed to read file: {}", path.display()))?;
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)
}
fn output_envelope(args: &ReceiveArgs, envelope: &Envelope) -> Result<()> {
let payload = &envelope.payload;
if !args.quiet {
if let Some(count) = envelope.metadata.var_count {
display::info("Secrets:", &format!("{} variables", count));
}
if let Some(ref label) = envelope.metadata.label {
display::info("Label:", label);
}
}
if args.clipboard {
let mut clipboard = arboard::Clipboard::new()
.context("clipboard not available (are you in a graphical environment?)")?;
clipboard.set_text(payload)?;
if let Some(ref label) = envelope.metadata.label {
display::ok(&format!("copied to clipboard (label: \"{}\")", label));
} else {
display::ok("copied to clipboard");
}
return Ok(());
}
if matches!(envelope.format, PayloadFormat::Env) {
validate_against_schema(payload, args.quiet);
}
match envelope.format {
PayloadFormat::Env => {
if args.no_write {
print!("{}", payload);
} else {
let path = args.output.as_deref().unwrap_or(".env");
check_overwrite(path, args.force)?;
write_secret_file(path, payload)?;
let count = envelope.metadata.var_count.unwrap_or(0);
display::ok(&format!("{} secrets written to {}", count, path));
}
}
PayloadFormat::Raw => {
if let Some(ref path) = args.output {
check_overwrite(path, args.force)?;
write_secret_file(path, payload)?;
display::ok(&format!("written to {}", path));
} else {
print!("{}", payload);
}
}
PayloadFormat::Kv => {
if let Some(ref path) = args.output {
check_overwrite(path, args.force)?;
write_secret_file(path, payload)?;
display::ok(&format!("written to {}", path));
} else {
println!("{}", payload);
}
}
}
Ok(())
}
fn write_secret_file(path: &str, content: &str) -> Result<()> {
#[cfg(unix)]
{
use std::fs::OpenOptions;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)?;
file.write_all(content.as_bytes())?;
}
#[cfg(not(unix))]
{
std::fs::write(path, content)?;
}
Ok(())
}
fn check_overwrite(path: &str, force: bool) -> Result<()> {
if !std::path::Path::new(path).exists() {
return Ok(());
}
if force {
return Ok(());
}
if !is_terminal::is_terminal(std::io::stdin()) {
bail!(
"'{}' already exists. Use --force to overwrite in non-interactive mode",
path
);
}
let confirm = dialoguer::Confirm::new()
.with_prompt(format!("'{}' already exists. Overwrite?", path))
.default(false)
.interact()?;
if !confirm {
bail!("aborted: not overwriting '{}'", path);
}
Ok(())
}
fn validate_against_schema(payload: &str, quiet: bool) {
if quiet {
return;
}
let schema = match env::schema::load_schema(None) {
Ok(Some(s)) => s,
_ => 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("received .env has schema validation issues:");
for err in &errors {
display::warning(&format!(" {}", err));
}
}
}