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

difftreelog

source

cmds/generator-helper/src/main.rs6.7 KiBsourcehistory
1use std::{2	env,3	fs::{File, OpenOptions},4	io::{copy, Read, Write},5	str::FromStr,6};78use age::{9	ssh::{ParseRecipientKeyError, Recipient as SshRecipient},10	Encryptor, Recipient,11};12use anyhow::{anyhow, bail, ensure, Context, Result};13use clap::{Parser, ValueEnum};14use fleet_shared::SecretData;15use rand::{16	distributions::{Alphanumeric, DistString, Distribution, Uniform},17	thread_rng,18};1920fn write_output_file(out: &str) -> Result<File> {21	let file = OpenOptions::new()22		.create_new(true)23		.write(true)24		.open(out)25		.with_context(|| format!("failed to open output {out:?}"))?;26	Ok(file)27}28fn write_public(out: &str, mut input: impl Read, encoding: OutputEncoding) -> Result<()> {29	let mut output = write_output_file(out)?;3031	let mut data = Vec::new();32	copy(&mut input, &mut wrap_encoder(&mut data, encoding))?;3334	output.write_all(35		SecretData {36			data,37			encrypted: false,38		}39		.to_string()40		.as_bytes(),41	)?;42	Ok(())43}44fn write_private(45	identities: &Identities,46	out: &str,47	mut input: impl Read,48	encoding: OutputEncoding,49) -> Result<()> {50	let mut output = write_output_file(out)?;51	let encryptor = make_encryptor(identities)?;5253	let mut data = Vec::new();54	{55		let mut encrypted_writer = encryptor.wrap_output(&mut data)?;56		copy(57			&mut input,58			&mut wrap_encoder(&mut encrypted_writer, encoding),59		)?;60		encrypted_writer.finish()?;61	};6263	output.write_all(64		SecretData {65			data,66			encrypted: true,67		}68		.to_string()69		.as_bytes(),70	)?;71	Ok(())72}7374type Identities = Vec<SshRecipient>;75fn load_identities() -> Result<Identities> {76	let list = env::var("GENERATOR_HELPER_IDENTITIES");77	let list = match list {78		Ok(v) => v,79		Err(env::VarError::NotPresent) => {80			bail!("gh is only intended to be used from secret generator scripts, but if you really want to use it somewhere else - set GENERATOR_HELPER_IDENTITIES to list of newline-delimited ssh identities");81		}82		Err(e) => bail!("somehow, identities list is not utf-8: {e}"),83	};84	let list = list.trim();85	ensure!(!list.is_empty(), "no identities passed, can't encrypt data");86	list.lines()87		.map(age::ssh::Recipient::from_str)88		.collect::<Result<Identities, ParseRecipientKeyError>>()89		.map_err(|e| anyhow!("parse recipients: {e:?}"))90}91fn make_encryptor(r: &Identities) -> Result<Encryptor> {92	Ok(Encryptor::with_recipients(93		r.iter()94			.map(|v| {95				let coerced: Box<dyn Recipient + Send> = Box::new(v.clone());96				coerced97			})98			.collect(),99	)100	.expect("list is not empty"))101}102fn wrap_encoder<'t>(w: impl Write + 't, encoding: OutputEncoding) -> impl Write + 't {103	fn coerce<'t>(w: impl Write + 't) -> Box<dyn Write + 't> {104		Box::new(w)105	}106	match encoding {107		OutputEncoding::Raw => coerce(w),108		OutputEncoding::Base64 => {109			use base64::engine::general_purpose::STANDARD;110			let writer = base64::write::EncoderWriter::new(w, &STANDARD);111			coerce(writer)112		}113	}114}115116#[derive(Clone, Copy, ValueEnum, Default)]117enum OutputEncoding {118	/// Do not encode data, store as is.119	#[default]120	Raw,121	/// Encode as base64 (with padding).122	Base64,123}124125#[derive(Parser)]126enum Generate {127	/// Generate public, private keys without wrapping, in standard ed25519 schema128	/// (64 bytes private (due to merge with private), 32 bytes public)129	Ed25519 {130		#[arg(long, short = 'p')]131		public: String,132		#[arg(long, short = 's')]133		private: String,134		/// Private key should be just the private key (32 bytes), not standard private+public.135		#[arg(long)]136		no_embed_public: bool,137		#[arg(long, short = 'e', value_enum, default_value_t)]138		encoding: OutputEncoding,139	},140	/// Generate public, private keys without wrapping, in standard x25519 schema141	/// (32 bytes private, 32 bytes public)142	X25519 {143		#[arg(long, short = 'p')]144		public: String,145		#[arg(long, short = 's')]146		private: String,147		#[arg(long, short = 'e', value_enum, default_value_t)]148		encoding: OutputEncoding,149	},150	Password {151		#[arg(long, short = 'o')]152		output: String,153		#[arg(long)]154		size: usize,155		#[arg(long, short = 'n')]156		no_symbols: bool,157		#[arg(long, short = 'e', value_enum, default_value_t)]158		encoding: OutputEncoding,159	},160}161162#[derive(Parser)]163enum Opts {164	/// Encode public part from stdin.165	Public {166		#[arg(long, short = 'o')]167		output: String,168		#[arg(long, short = 'e', value_enum, default_value_t)]169		encoding: OutputEncoding,170	},171	/// Encrypt private part from stdin.172	Private {173		#[arg(long, short = 'o')]174		output: String,175		#[arg(long, short = 'e', value_enum, default_value_t)]176		encoding: OutputEncoding,177	},178	/// Generate keys in well-known schemas.179	///180	/// Note that this command is only intended to be used in fleet secret generator,181	/// otherwise you should ensure noone is able to read generated files, they don't have any mode set by default.182	#[command(subcommand)]183	Generate(Generate),184}185186fn main() -> Result<()> {187	let opts = Opts::parse();188	// Assumed to be secure, seeded from secure OsRng+reseeded.189	let mut rng = thread_rng();190191	match opts {192		Opts::Public { output, encoding } => {193			write_public(&output, std::io::stdin(), encoding)?;194		}195		Opts::Private { output, encoding } => {196			let recipients = load_identities()?;197			write_private(&recipients, &output, std::io::stdin(), encoding)?;198		}199		Opts::Generate(gen) => {200			match gen {201				Generate::Ed25519 {202					public,203					private,204					no_embed_public,205					encoding,206				} => {207					let recipients = load_identities()?;208					let key = ed25519_dalek::SigningKey::generate(&mut rng).to_keypair_bytes();209					write_public(&public, &key[32..], encoding)?;210					write_private(211						&recipients,212						&private,213						&key[..{214							if no_embed_public {215								32216							} else {217								64218							}219						}],220						encoding,221					)?;222				}223				Generate::X25519 {224					public,225					private,226					encoding,227				} => {228					let recipients = load_identities()?;229					let key = x25519_dalek::StaticSecret::random_from_rng(rng);230					let public_key: x25519_dalek::PublicKey = (&key).into();231					write_public(&public, public_key.as_bytes().as_slice(), encoding)?;232					write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?;233				}234				Generate::Password {235					size,236					no_symbols,237					output,238					encoding,239				} => {240					ensure!(241						size >= 6,242						"misconfiguration? password is shorter than 6 chars"243					);244					let recipients = load_identities()?;245					let out = if no_symbols {246						Alphanumeric.sample_string(&mut rng, size)247					} else {248						// Alphabet of Alphanumberic + symbols249						const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";250						let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len());251						(0..size)252							.map(|_| uniform.sample(&mut rng))253							.map(|i| GEN_ASCII_SYMBOLS[i] as char)254							.collect::<String>()255					};256					write_private(&recipients, &output, out.as_bytes(), encoding)?;257				}258			}259		}260	}261	Ok(())262}