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(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, write::EncoderWriter};110111 let writer = EncoderWriter::new(w, &STANDARD);112 coerce(writer)113 }114 OutputEncoding::Hex => {115 struct HexWriter<W>(W);116 impl<W> Write for HexWriter<W>117 where118 W: Write,119 {120 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {121 let encoded = hex::encode(buf);122 self.0.write_all(encoded.as_bytes())?;123 Ok(buf.len())124 }125126 fn flush(&mut self) -> io::Result<()> {127 self.0.flush()128 }129 }130 coerce(HexWriter(w))131 }132 }133}134135#[derive(Clone, Copy, ValueEnum, Default)]136enum OutputEncoding {137 138 #[default]139 Raw,140 141 Base64,142 143 Hex,144}145146#[derive(Parser)]147enum Generate {148 149 150 Ed25519 {151 #[arg(long, short = 'p')]152 public: String,153 #[arg(long, short = 's')]154 private: String,155 156 #[arg(long)]157 no_embed_public: bool,158 #[arg(long, short = 'e', value_enum, default_value_t)]159 encoding: OutputEncoding,160 },161 162 163 X25519 {164 #[arg(long, short = 'p')]165 public: String,166 #[arg(long, short = 's')]167 private: String,168 #[arg(long, short = 'e', value_enum, default_value_t)]169 encoding: OutputEncoding,170 },171 Password {172 #[arg(long, short = 'o')]173 output: String,174 #[arg(long)]175 size: usize,176 #[arg(long, short = 'n')]177 no_symbols: bool,178 #[arg(long, short = 'e', value_enum, default_value_t)]179 encoding: OutputEncoding,180 },181 Bytes {182 #[arg(long, short = 'o')]183 output: String,184 #[arg(long, short = 'c')]185 count: usize,186 187 #[arg(long)]188 no_nuls: bool,189 #[arg(long, short = 'e', value_enum, default_value_t)]190 encoding: OutputEncoding,191 },192}193194#[derive(Parser)]195enum Opts {196 197 Public {198 #[arg(long, short = 'o')]199 output: String,200 #[arg(long, short = 'e', value_enum, default_value_t)]201 encoding: OutputEncoding,202 },203 204 Private {205 #[arg(long, short = 'o')]206 output: String,207 #[arg(long, short = 'e', value_enum, default_value_t)]208 encoding: OutputEncoding,209 },210 211 212 213 214 Decode {215 #[arg(long, short = 'i')]216 input: String,217 },218 219 220 221 222 223 224 225 #[command(subcommand)]226 Generate(Generate),227}228229fn main() -> Result<()> {230 let opts = Opts::parse();231 232 let mut rng = thread_rng();233234 match opts {235 Opts::Public { output, encoding } => {236 write_public(&output, stdin(), encoding)?;237 }238 Opts::Private { output, encoding } => {239 let recipients = load_identities()?;240 write_private(&recipients, &output, stdin(), encoding)?;241 }242 Opts::Generate(gen) => {243 match gen {244 Generate::Ed25519 {245 public,246 private,247 no_embed_public,248 encoding,249 } => {250 use ed25519_dalek::SigningKey;251252 let recipients = load_identities()?;253 let key = SigningKey::generate(&mut rng).to_keypair_bytes();254 write_public(&public, &key[32..], encoding)?;255 write_private(256 &recipients,257 &private,258 &key[..{259 if no_embed_public {260 32261 } else {262 64263 }264 }],265 encoding,266 )?;267 }268 Generate::X25519 {269 public,270 private,271 encoding,272 } => {273 use x25519_dalek::{PublicKey, StaticSecret};274275 let recipients = load_identities()?;276 let key = StaticSecret::random_from_rng(rng);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 296 const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";297 let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len());298 (0..size)299 .map(|_| uniform.sample(&mut rng))300 .map(|i| GEN_ASCII_SYMBOLS[i] as char)301 .collect::<String>()302 };303 write_private(&recipients, &output, out.as_bytes(), encoding)?;304 }305 Generate::Bytes {306 output,307 count,308 no_nuls,309 encoding,310 } => {311 ensure!(312 count >= 6,313 "misconfiguration? bytestring is shorter than 6 chars"314 );315 let recipients = load_identities()?;316 let mut bytes = vec![0u8; count];317 if no_nuls {318 let rand = Uniform::new_inclusive(0x1u8, 0xffu8).sample_iter(&mut rng);319 for (byte, rand) in bytes.iter_mut().zip(rand) {320 *byte = rand;321 }322 } else {323 rng.fill_bytes(&mut bytes);324 };325 write_private(&recipients, &output, bytes.as_slice(), encoding)?;326 }327 }328 }329 Opts::Decode { input } => {330 let mut data = Vec::new();331 File::open(input)?.read_to_end(&mut data)?;332 let data = String::from_utf8(data).context(333 "encoded data is always utf-8, you are trying to use decode the wrong way.",334 )?;335 let data =336 SecretData::from_str(&data).map_err(|e| anyhow!("failed to decode data: {e}"))?;337 ensure!(338 !data.encrypted,339 "you can not decrypt secret data, only decode public."340 );341 stdout().write_all(&data.data)?;342 }343 }344 Ok(())345}