1use std::{2 fs,3 io::{self, stdout, Cursor, Read, Write},4 path::PathBuf,5 str::FromStr,6};78use age::Recipient;9use anyhow::{anyhow, bail, ensure, Context, Result};10use clap::Parser;11use ed25519_dalek::SigningKey;12use fleet_shared::SecretData;13use rand::{14 distributions::{Alphanumeric, DistString, Distribution, Uniform},15 rngs::OsRng,16 thread_rng, Rng,17};1819fn write_output(out: &str, data: impl AsRef<[u8]>, stdout_marker: &mut bool) -> Result<()> {20 let data = data.as_ref();21 if out == "-" {22 let mut stdout = stdout();23 if *stdout_marker {24 stdout.write_all(&[b'\n'])?;25 }26 *stdout_marker = true;27 stdout.write_all(data)?;28 } else {29 fs::write(out, data)?;30 };31 Ok(())32}3334#[derive(Parser)]35enum Generate {36 37 38 Ed25519 {39 public: String,40 private: String,41 42 #[arg(long)]43 no_embed_public: bool,44 },45 Password {46 output: String,47 size: usize,48 #[arg(long, short = 'n')]49 no_symbols: bool,50 },51}5253#[derive(Parser)]54enum Opts {55 56 Public {57 #[arg(long)]58 allow_empty: bool,59 },60 61 Private {62 #[arg(long)]63 allow_empty: bool,64 #[arg(short = 'r')]65 recipient: Vec<String>,66 },67 68 69 70 71 #[command(subcommand)]72 Generate(Generate),73 74 75 76 77 78 79}8081fn parse_stdin() -> Result<Option<Vec<u8>>> {82 let mut input = vec![];83 io::stdin().read_to_end(&mut input)?;84 if input.is_empty() {85 Ok(None)86 } else {87 Ok(Some(input))88 }89}90pub fn encrypt_secret_data(91 recipients: impl IntoIterator<Item = impl Recipient + Send + 'static>,92 data: Vec<u8>,93) -> Option<SecretData> {94 let mut encrypted = vec![];95 let recipients = recipients96 .into_iter()97 .map(|v| Box::new(v) as Box<dyn Recipient + Send>)98 .collect::<Vec<_>>();99 let mut encryptor = age::Encryptor::with_recipients(recipients)?100 .wrap_output(&mut encrypted)101 .expect("in memory write");102 io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");103 encryptor.finish().expect("in memory flush");104 Some(SecretData {105 data: encrypted,106 encrypted: true,107 })108}109110fn main() -> Result<()> {111 let opts = Opts::parse();112 113 let mut rng = thread_rng();114115 match opts {116 Opts::Public { allow_empty } => {117 let stdin = parse_stdin()?;118 if stdin.is_none() && !allow_empty {119 bail!("empty stdin input is not allowed unless --allow-empty is set");120 }121 let stdin = stdin.unwrap_or_default();122 io::stdout().write_all(123 SecretData {124 data: stdin,125 encrypted: false,126 }127 .to_string()128 .as_bytes(),129 )?;130 }131 Opts::Private {132 allow_empty,133 recipient,134 } => {135 let stdin = parse_stdin()?;136 if stdin.is_none() && !allow_empty {137 bail!("empty stdin input is not allowed unless --allow-empty is set");138 }139 let stdin = stdin.unwrap_or_default();140 if recipient.is_empty() {141 bail!("recipient list is empty");142 }143 let out = encrypt_secret_data(144 recipient145 .into_iter()146 .map(|r| age::ssh::Recipient::from_str(&r))147 .collect::<Result<Vec<age::ssh::Recipient>, age::ssh::ParseRecipientKeyError>>()148 .map_err(|e| anyhow!("parse recipients: {e:?}"))?,149 stdin,150 )151 .expect("got recipients");152 io::stdout().write_all(out.to_string().as_bytes())?;153 }154 Opts::Generate(gen) => {155 let mut stdout_marker: bool = false;156 match gen {157 Generate::Ed25519 {158 public,159 private,160 no_embed_public,161 } => {162 let key = SigningKey::generate(&mut rng).to_keypair_bytes();163164 write_output(&public, &key[32..], &mut stdout_marker).context("public")?;165 write_output(166 &private,167 &key[..{168 if no_embed_public {169 32170 } else {171 64172 }173 }],174 &mut stdout_marker,175 )176 .context("private")?;177 }178 Generate::Password {179 size,180 no_symbols,181 output,182 } => {183 ensure!(184 size >= 6,185 "misconfiguration? password is shorter than 6 chars"186 );187 let out = if no_symbols {188 Alphanumeric.sample_string(&mut rng, size)189 } else {190 191 const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";192 let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len());193 (0..size)194 .map(|_| uniform.sample(&mut rng))195 .map(|i| GEN_ASCII_SYMBOLS[i] as char)196 .collect::<String>()197 };198 write_output(&output, out, &mut stdout_marker)?;199 }200 }201 }202 }203 Ok(())204}