git.delta.rocks / jrsonnet / refs/commits / ad7852dc047c

difftreelog

source

cmds/generator-helper/src/main.rs5.1 KiBsourcehistory
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	/// Generate public, private keys without wrapping, in standard ed25519 schema37	/// (64 bytes private (due to merge with private), 32 bytes public)38	Ed25519 {39		public: String,40		private: String,41		/// Private key should be just the private key (32 bytes), not standard private+public.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	/// Encode public part from stdin.56	Public {57		#[arg(long)]58		allow_empty: bool,59	},60	/// Encrypt private part from stdin.61	Private {62		#[arg(long)]63		allow_empty: bool,64		#[arg(short = 'r')]65		recipient: Vec<String>,66	},67	/// Generate keys in well-known schemas.68	///69	/// Note that this command is only intended to be used in fleet secret generator,70	/// otherwise you should ensure noone is able to read generated files, they don't have any mode set by default.71	#[command(subcommand)]72	Generate(Generate),73	// Generate {74	// 	kind: GenerateKind,75	// 	/// Different generators generate different number of files, you need to specify number of outputs corresponding to the generator.76	// 	#[arg(short = 'o')]77	// 	outputs: Vec<String>,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	// Assumed to be secure, seeded from secure OsRng+reseeded.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						// Alphabet of Alphanumberic + symbols191						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}