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

difftreelog

source

cmds/generator-helper/src/main.rs9.0 KiBsourcehistory
1use std::{2	env,3	fs::{File, OpenOptions},4	io::{self, copy, stdin, stdout, 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, RngCore,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(93		Encryptor::with_recipients(r.iter().map(|v| v as &dyn Recipient))94			.expect("list is not empty"),95	)96}97fn wrap_encoder<'t>(w: impl Write + 't, encoding: OutputEncoding) -> impl Write + 't {98	fn coerce<'t>(w: impl Write + 't) -> Box<dyn Write + 't> {99		Box::new(w)100	}101	match encoding {102		OutputEncoding::Raw => coerce(w),103		OutputEncoding::Base64 => {104			use base64::{engine::general_purpose::STANDARD, write::EncoderWriter};105106			let writer = EncoderWriter::new(w, &STANDARD);107			coerce(writer)108		}109		OutputEncoding::Hex => {110			struct HexWriter<W>(W);111			impl<W> Write for HexWriter<W>112			where113				W: Write,114			{115				fn write(&mut self, buf: &[u8]) -> io::Result<usize> {116					let encoded = hex::encode(buf);117					self.0.write_all(encoded.as_bytes())?;118					Ok(buf.len())119				}120121				fn flush(&mut self) -> io::Result<()> {122					self.0.flush()123				}124			}125			coerce(HexWriter(w))126		}127	}128}129130#[derive(Clone, Copy, ValueEnum, Default)]131enum OutputEncoding {132	/// Do not encode data, store as is.133	#[default]134	Raw,135	/// Encode as base64 (with padding).136	Base64,137	/// Encode as hex (without leading 0x)138	Hex,139}140141#[derive(Parser)]142enum Generate {143	/// Generate public, private keys without wrapping, in standard ed25519 schema144	/// (64 bytes private (due to merge with private), 32 bytes public)145	Ed25519 {146		#[arg(long, short = 'p')]147		public: String,148		#[arg(long, short = 's')]149		private: String,150		/// Private key should be just the private key (32 bytes), not standard private+public.151		#[arg(long)]152		no_embed_public: bool,153		#[arg(long, short = 'e', value_enum, default_value_t)]154		encoding: OutputEncoding,155	},156	/// Generate public, private keys without wrapping, in standard x25519 schema157	/// (32 bytes private, 32 bytes public)158	X25519 {159		#[arg(long, short = 'p')]160		public: String,161		#[arg(long, short = 's')]162		private: String,163		#[arg(long, short = 'e', value_enum, default_value_t)]164		encoding: OutputEncoding,165	},166	Password {167		#[arg(long, short = 'o')]168		output: String,169		#[arg(long)]170		size: usize,171		#[arg(long, short = 'n')]172		no_symbols: bool,173		#[arg(long, short = 'e', value_enum, default_value_t)]174		encoding: OutputEncoding,175	},176	Bytes {177		#[arg(long, short = 'o')]178		output: String,179		#[arg(long, short = 'c')]180		count: usize,181		/// Ensure there is no NULs in bytestring.182		#[arg(long)]183		no_nuls: bool,184		#[arg(long, short = 'e', value_enum, default_value_t)]185		encoding: OutputEncoding,186	},187}188189#[derive(Parser)]190enum Opts {191	/// Encode public part from stdin.192	Public {193		#[arg(long, short = 'o')]194		output: String,195		#[arg(long, short = 'e', value_enum, default_value_t)]196		encoding: OutputEncoding,197	},198	/// Encrypt private part from stdin.199	Private {200		#[arg(long, short = 'o')]201		output: String,202		#[arg(long, short = 'e', value_enum, default_value_t)]203		encoding: OutputEncoding,204	},205	/// Sometimes you also need to reencode secret, this command allows you to get raw data from206	/// secret encoded using `gh public`... I would like if I knew a better design for some sort of207	/// such thing. Ideally there should be no need to decode secrets back, but garage wants both208	/// raw pubkey and raw secret key, yet also requires node id which is hex-reencoded public key.209	Decode {210		#[arg(long, short = 'i')]211		input: String,212	},213	/// Generate keys in well-known schemas.214	///215	/// Note that this command is only intended to be used in fleet secret generator,216	/// otherwise you should ensure noone is able to read generated files, they don't have any mode set by default.217	///218	/// Fleet also doesn't zeroize memory/assumes good OsRng/makes other assumptions, which makes it only suitable to219	/// be used in nix sandbox.220	#[command(subcommand)]221	Generate(Generate),222}223224fn main() -> Result<()> {225	let opts = Opts::parse();226	// Assumed to be secure, seeded from secure OsRng+reseeded.227	let mut rng = thread_rng();228229	match opts {230		Opts::Public { output, encoding } => {231			write_public(&output, stdin(), encoding)?;232		}233		Opts::Private { output, encoding } => {234			let recipients = load_identities()?;235			write_private(&recipients, &output, stdin(), encoding)?;236		}237		Opts::Generate(gen) => {238			match gen {239				Generate::Ed25519 {240					public,241					private,242					no_embed_public,243					encoding,244				} => {245					use ed25519_dalek::SigningKey;246247					let recipients = load_identities()?;248					let key = SigningKey::generate(&mut rng).to_keypair_bytes();249					write_public(&public, &key[32..], encoding)?;250					write_private(251						&recipients,252						&private,253						&key[..{254							if no_embed_public {255								32256							} else {257								64258							}259						}],260						encoding,261					)?;262				}263				Generate::X25519 {264					public,265					private,266					encoding,267				} => {268					use x25519_dalek::{PublicKey, StaticSecret};269270					let recipients = load_identities()?;271					let key = StaticSecret::random_from_rng(rng);272					let public_key: PublicKey = (&key).into();273					write_public(&public, public_key.as_bytes().as_slice(), encoding)?;274					write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?;275				}276				Generate::Password {277					size,278					no_symbols,279					output,280					encoding,281				} => {282					ensure!(283						size >= 6,284						"misconfiguration? password is shorter than 6 chars"285					);286					let recipients = load_identities()?;287					let out = if no_symbols {288						Alphanumeric.sample_string(&mut rng, size)289					} else {290						// Alphabet of Alphanumberic + symbols291						const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";292						let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len());293						(0..size)294							.map(|_| uniform.sample(&mut rng))295							.map(|i| GEN_ASCII_SYMBOLS[i] as char)296							.collect::<String>()297					};298					write_private(&recipients, &output, out.as_bytes(), encoding)?;299				}300				Generate::Bytes {301					output,302					count,303					no_nuls,304					encoding,305				} => {306					ensure!(307						count >= 6,308						"misconfiguration? bytestring is shorter than 6 chars"309					);310					let recipients = load_identities()?;311					let mut bytes = vec![0u8; count];312					if no_nuls {313						let rand = Uniform::new_inclusive(0x1u8, 0xffu8).sample_iter(&mut rng);314						for (byte, rand) in bytes.iter_mut().zip(rand) {315							*byte = rand;316						}317					} else {318						rng.fill_bytes(&mut bytes);319					};320					write_private(&recipients, &output, bytes.as_slice(), encoding)?;321				}322			}323		}324		Opts::Decode { input } => {325			let mut data = Vec::new();326			File::open(input)?.read_to_end(&mut data)?;327			let data = String::from_utf8(data).context(328				"encoded data is always utf-8, you are trying to use decode the wrong way.",329			)?;330			let data =331				SecretData::from_str(&data).map_err(|e| anyhow!("failed to decode data: {e}"))?;332			ensure!(333				!data.encrypted,334				"you can not decrypt secret data, only decode public."335			);336			stdout().write_all(&data.data)?;337		}338	}339	Ok(())340}