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