git.delta.rocks / jrsonnet / refs/commits / 1470de8a447c

difftreelog

source

cmds/generator-helper/src/main.rs9.4 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 ed25519_dalek::SecretKey;15use fleet_shared::SecretData;16use rand::{17	distr::{Alphanumeric, Distribution, SampleString, Uniform},18	rng, RngCore,19};2021fn write_output_file(out: &str) -> Result<File> {22	let file = OpenOptions::new()23		.create_new(true)24		.write(true)25		.open(out)26		.with_context(|| format!("failed to open output {out:?}"))?;27	Ok(file)28}29fn write_public(out: &str, mut input: impl Read, encoding: OutputEncoding) -> Result<()> {30	let mut output = write_output_file(out)?;3132	let mut data = Vec::new();33	copy(&mut input, &mut wrap_encoder(&mut data, encoding))?;3435	output.write_all(36		SecretData {37			data,38			encrypted: false,39		}40		.to_string()41		.as_bytes(),42	)?;43	Ok(())44}45fn write_private(46	identities: &Identities,47	out: &str,48	mut input: impl Read,49	encoding: OutputEncoding,50) -> Result<()> {51	let mut output = write_output_file(out)?;52	let encryptor = make_encryptor(identities)?;5354	let mut data = Vec::new();55	{56		let mut encrypted_writer = encryptor.wrap_output(&mut data)?;57		copy(58			&mut input,59			&mut wrap_encoder(&mut encrypted_writer, encoding),60		)?;61		encrypted_writer.finish()?;62	};6364	output.write_all(65		SecretData {66			data,67			encrypted: true,68		}69		.to_string()70		.as_bytes(),71	)?;72	Ok(())73}7475type Identities = Vec<SshRecipient>;76fn load_identities() -> Result<Identities> {77	let list = env::var("GENERATOR_HELPER_IDENTITIES");78	let list = match list {79		Ok(v) => v,80		Err(env::VarError::NotPresent) => {81			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");82		}83		Err(e) => bail!("somehow, identities list is not utf-8: {e}"),84	};85	let list = list.trim();86	ensure!(!list.is_empty(), "no identities passed, can't encrypt data");87	list.lines()88		.map(age::ssh::Recipient::from_str)89		.collect::<Result<Identities, ParseRecipientKeyError>>()90		.map_err(|e| anyhow!("parse recipients: {e:?}"))91}92fn make_encryptor(r: &Identities) -> Result<Encryptor> {93	Ok(94		Encryptor::with_recipients(r.iter().map(|v| v as &dyn Recipient))95			.expect("list is not empty"),96	)97}98fn wrap_encoder<'t>(w: impl Write + 't, encoding: OutputEncoding) -> impl Write + 't {99	fn coerce<'t>(w: impl Write + 't) -> Box<dyn Write + 't> {100		Box::new(w)101	}102	match encoding {103		OutputEncoding::Raw => coerce(w),104		OutputEncoding::Base64 => {105			use base64::{engine::general_purpose::STANDARD, write::EncoderWriter};106107			let writer = EncoderWriter::new(w, &STANDARD);108			coerce(writer)109		}110		OutputEncoding::Hex => {111			struct HexWriter<W>(W);112			impl<W> Write for HexWriter<W>113			where114				W: Write,115			{116				fn write(&mut self, buf: &[u8]) -> io::Result<usize> {117					let encoded = hex::encode(buf);118					self.0.write_all(encoded.as_bytes())?;119					Ok(buf.len())120				}121122				fn flush(&mut self) -> io::Result<()> {123					self.0.flush()124				}125			}126			coerce(HexWriter(w))127		}128	}129}130131#[derive(Clone, Copy, ValueEnum, Default)]132enum OutputEncoding {133	/// Do not encode data, store as is.134	#[default]135	Raw,136	/// Encode as base64 (with padding).137	Base64,138	/// Encode as hex (without leading 0x)139	Hex,140}141142#[derive(Parser)]143enum Generate {144	/// Generate public, private keys without wrapping, in standard ed25519 schema145	/// (64 bytes private (due to merge with private), 32 bytes public)146	Ed25519 {147		#[arg(long, short = 'p')]148		public: String,149		#[arg(long, short = 's')]150		private: String,151		/// Private key should be just the private key (32 bytes), not standard private+public.152		#[arg(long)]153		no_embed_public: bool,154		#[arg(long, short = 'e', value_enum, default_value_t)]155		encoding: OutputEncoding,156	},157	/// Generate public, private keys without wrapping, in standard x25519 schema158	/// (32 bytes private, 32 bytes public)159	X25519 {160		#[arg(long, short = 'p')]161		public: String,162		#[arg(long, short = 's')]163		private: String,164		#[arg(long, short = 'e', value_enum, default_value_t)]165		encoding: OutputEncoding,166	},167	Password {168		#[arg(long, short = 'o')]169		output: String,170		#[arg(long)]171		size: usize,172		#[arg(long, short = 'n')]173		no_symbols: bool,174		#[arg(long, short = 'e', value_enum, default_value_t)]175		encoding: OutputEncoding,176	},177	Bytes {178		#[arg(long, short = 'o')]179		output: String,180		#[arg(long, short = 'c')]181		count: usize,182		/// Ensure there is no NULs in bytestring.183		#[arg(long)]184		no_nuls: bool,185		#[arg(long, short = 'e', value_enum, default_value_t)]186		encoding: OutputEncoding,187	},188}189190#[derive(Parser)]191enum Opts {192	/// Encode public part from stdin.193	Public {194		#[arg(long, short = 'o')]195		output: String,196		#[arg(long, short = 'e', value_enum, default_value_t)]197		encoding: OutputEncoding,198	},199	/// Encrypt private part from stdin.200	Private {201		#[arg(long, short = 'o')]202		output: String,203		#[arg(long, short = 'e', value_enum, default_value_t)]204		encoding: OutputEncoding,205	},206	/// Sometimes you also need to reencode secret, this command allows you to get raw data from207	/// secret encoded using `gh public`... I would like if I knew a better design for some sort of208	/// such thing. Ideally there should be no need to decode secrets back, but garage wants both209	/// raw pubkey and raw secret key, yet also requires node id which is hex-reencoded public key.210	Decode {211		#[arg(long, short = 'i')]212		input: String,213	},214	/// Generate keys in well-known schemas.215	///216	/// Note that this command is only intended to be used in fleet secret generator,217	/// otherwise you should ensure noone is able to read generated files, they don't have any mode set by default.218	///219	/// Fleet also doesn't zeroize memory/assumes good OsRng/makes other assumptions, which makes it only suitable to220	/// be used in nix sandbox.221	#[command(subcommand)]222	Generate(Generate),223}224225fn main() -> Result<()> {226	let opts = Opts::parse();227	// Assumed to be secure, seeded from secure OsRng+reseeded.228	let mut rng = rng();229230	match opts {231		Opts::Public { output, encoding } => {232			write_public(&output, stdin(), encoding)?;233		}234		Opts::Private { output, encoding } => {235			let recipients = load_identities()?;236			write_private(&recipients, &output, stdin(), encoding)?;237		}238		Opts::Generate(generate) => {239			match generate {240				Generate::Ed25519 {241					public,242					private,243					no_embed_public,244					encoding,245				} => {246					use ed25519_dalek::SigningKey;247248					let recipients = load_identities()?;249					let mut secret = SecretKey::default();250					rng.fill_bytes(&mut secret);251					// TODO: Use SigningKey::generate after https://github.com/dalek-cryptography/curve25519-dalek/pull/762252					let key = SigningKey::from_bytes(&secret).to_keypair_bytes();253					write_public(&public, &key[32..], encoding)?;254					write_private(255						&recipients,256						&private,257						&key[..{258							if no_embed_public {259								32260							} else {261								64262							}263						}],264						encoding,265					)?;266				}267				Generate::X25519 {268					public,269					private,270					encoding,271				} => {272					use x25519_dalek::{PublicKey, StaticSecret};273274					let recipients = load_identities()?;275					// TODO: Use random_from_rng after https://github.com/dalek-cryptography/curve25519-dalek/pull/762276					let key = StaticSecret::random();277					let public_key: PublicKey = (&key).into();278					write_public(&public, public_key.as_bytes().as_slice(), encoding)?;279					write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?;280				}281				Generate::Password {282					size,283					no_symbols,284					output,285					encoding,286				} => {287					ensure!(288						size >= 6,289						"misconfiguration? password is shorter than 6 chars"290					);291					let recipients = load_identities()?;292					let out = if no_symbols {293						Alphanumeric.sample_string(&mut rng, size)294					} else {295						// Alphabet of Alphanumberic + symbols296						const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";297						let uniform =298							Uniform::new(0, GEN_ASCII_SYMBOLS.len()).expect("range is valid");299						(0..size)300							.map(|_| uniform.sample(&mut rng))301							.map(|i| GEN_ASCII_SYMBOLS[i] as char)302							.collect::<String>()303					};304					write_private(&recipients, &output, out.as_bytes(), encoding)?;305				}306				Generate::Bytes {307					output,308					count,309					no_nuls,310					encoding,311				} => {312					ensure!(313						count >= 6,314						"misconfiguration? bytestring is shorter than 6 chars"315					);316					let recipients = load_identities()?;317					let mut bytes = vec![0u8; count];318					if no_nuls {319						let rand = Uniform::new_inclusive(0x1u8, 0xffu8)320							.expect("range is valid")321							.sample_iter(&mut rng);322						for (byte, rand) in bytes.iter_mut().zip(rand) {323							*byte = rand;324						}325					} else {326						rng.fill_bytes(&mut bytes);327					};328					write_private(&recipients, &output, bytes.as_slice(), encoding)?;329				}330			}331		}332		Opts::Decode { input } => {333			let mut data = Vec::new();334			File::open(input)?.read_to_end(&mut data)?;335			let data = String::from_utf8(data).context(336				"encoded data is always utf-8, you are trying to use decode the wrong way.",337			)?;338			let data =339				SecretData::from_str(&data).map_err(|e| anyhow!("failed to decode data: {e}"))?;340			ensure!(341				!data.encrypted,342				"you can not decrypt secret data, only decode public."343			);344			stdout().write_all(&data.data)?;345		}346	}347	Ok(())348}