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 133 #[default]134 Raw,135 136 Base64,137 138 Hex,139}140141#[derive(Parser)]142enum Generate {143 144 145 Ed25519 {146 #[arg(long, short = 'p')]147 public: String,148 #[arg(long, short = 's')]149 private: String,150 151 #[arg(long)]152 no_embed_public: bool,153 #[arg(long, short = 'e', value_enum, default_value_t)]154 encoding: OutputEncoding,155 },156 157 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 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 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 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 206 207 208 209 Decode {210 #[arg(long, short = 'i')]211 input: String,212 },213 214 215 216 217 218 219 220 #[command(subcommand)]221 Generate(Generate),222}223224fn main() -> Result<()> {225 let opts = Opts::parse();226 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 291 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}