1use std::{2 env,3 fs::{File, OpenOptions},4 io::{self, Read, Write, copy, stdin, stdout},5 str::FromStr,6};78use age::{9 Encryptor, Recipient,10 ssh::{ParseRecipientKeyError, Recipient as SshRecipient},11};12use anyhow::{Context, Result, anyhow, bail, ensure};13use clap::{Parser, ValueEnum};14use ed25519_dalek::SecretKey;15use fleet_shared::SecretData;16use rand::{17 Rng as _,18 distr::{Alphanumeric, Distribution, SampleString, Uniform},19 rng,20};2122fn write_output_file(out: &str) -> Result<File> {23 let file = OpenOptions::new()24 .create_new(true)25 .write(true)26 .open(out)27 .with_context(|| format!("failed to open output {out:?}"))?;28 Ok(file)29}30fn write_public(out: &str, mut input: impl Read, encoding: OutputEncoding) -> Result<()> {31 let mut output = write_output_file(out)?;3233 let mut data = Vec::new();34 copy(&mut input, &mut wrap_encoder(&mut data, encoding))?;3536 output.write_all(37 SecretData {38 data,39 encrypted: false,40 }41 .to_string()42 .as_bytes(),43 )?;44 Ok(())45}46fn write_private(47 identities: &Identities,48 out: &str,49 mut input: impl Read,50 encoding: OutputEncoding,51) -> Result<()> {52 let mut output = write_output_file(out)?;53 let encryptor = make_encryptor(identities)?;5455 let mut data = Vec::new();56 {57 let mut encrypted_writer = encryptor.wrap_output(&mut data)?;58 copy(59 &mut input,60 &mut wrap_encoder(&mut encrypted_writer, encoding),61 )?;62 encrypted_writer.finish()?;63 };6465 output.write_all(66 SecretData {67 data,68 encrypted: true,69 }70 .to_string()71 .as_bytes(),72 )?;73 Ok(())74}7576type Identities = Vec<SshRecipient>;77fn load_identities() -> Result<Identities> {78 let list = env::var("GENERATOR_HELPER_IDENTITIES");79 let list = match list {80 Ok(v) => v,81 Err(env::VarError::NotPresent) => {82 bail!(83 "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"84 );85 }86 Err(e) => bail!("somehow, identities list is not utf-8: {e}"),87 };88 let list = list.trim();89 ensure!(!list.is_empty(), "no identities passed, can't encrypt data");90 list.lines()91 .map(age::ssh::Recipient::from_str)92 .collect::<Result<Identities, ParseRecipientKeyError>>()93 .map_err(|e| anyhow!("parse recipients: {e:?}"))94}95fn make_encryptor(r: &Identities) -> Result<Encryptor> {96 Ok(97 Encryptor::with_recipients(r.iter().map(|v| v as &dyn Recipient))98 .expect("list is not empty"),99 )100}101fn wrap_encoder<'t>(w: impl Write + 't, encoding: OutputEncoding) -> impl Write + 't {102 fn coerce<'t>(w: impl Write + 't) -> Box<dyn Write + 't> {103 Box::new(w)104 }105 match encoding {106 OutputEncoding::Raw => coerce(w),107 OutputEncoding::Base64 => {108 use base64::{engine::general_purpose::STANDARD, write::EncoderWriter};109110 let writer = EncoderWriter::new(w, &STANDARD);111 coerce(writer)112 }113 OutputEncoding::Hex => {114 struct HexWriter<W>(W);115 impl<W> Write for HexWriter<W>116 where117 W: Write,118 {119 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {120 let encoded = hex::encode(buf);121 self.0.write_all(encoded.as_bytes())?;122 Ok(buf.len())123 }124125 fn flush(&mut self) -> io::Result<()> {126 self.0.flush()127 }128 }129 coerce(HexWriter(w))130 }131 }132}133134#[derive(Clone, Copy, ValueEnum, Default)]135enum OutputEncoding {136 137 #[default]138 Raw,139 140 Base64,141 142 Hex,143}144145#[derive(Parser)]146enum Generate {147 148 149 Ed25519 {150 #[arg(long, short = 'p')]151 public: String,152 #[arg(long, short = 's')]153 private: String,154 155 #[arg(long)]156 no_embed_public: bool,157 #[arg(long, short = 'e', value_enum, default_value_t)]158 encoding: OutputEncoding,159 },160 161 162 X25519 {163 #[arg(long, short = 'p')]164 public: String,165 #[arg(long, short = 's')]166 private: String,167 #[arg(long, short = 'e', value_enum, default_value_t)]168 encoding: OutputEncoding,169 },170 Password {171 #[arg(long, short = 'o')]172 output: String,173 #[arg(long)]174 size: usize,175 #[arg(long, short = 'n')]176 no_symbols: bool,177 #[arg(long, short = 'e', value_enum, default_value_t)]178 encoding: OutputEncoding,179 },180 Bytes {181 #[arg(long, short = 'o')]182 output: String,183 #[arg(long, short = 'c')]184 count: usize,185 186 #[arg(long)]187 no_nuls: bool,188 #[arg(long, short = 'e', value_enum, default_value_t)]189 encoding: OutputEncoding,190 },191}192193#[derive(Parser)]194enum Opts {195 196 Public {197 #[arg(long, short = 'o')]198 output: String,199 #[arg(long, short = 'e', value_enum, default_value_t)]200 encoding: OutputEncoding,201 },202 203 Private {204 #[arg(long, short = 'o')]205 output: String,206 #[arg(long, short = 'e', value_enum, default_value_t)]207 encoding: OutputEncoding,208 },209 210 211 212 213 Decode {214 #[arg(long, short = 'i')]215 input: String,216 },217 218 219 220 221 222 223 224 #[command(subcommand)]225 Generate(Generate),226}227228fn main() -> Result<()> {229 let opts = Opts::parse();230 231 let mut rng = rng();232233 match opts {234 Opts::Public { output, encoding } => {235 write_public(&output, stdin(), encoding)?;236 }237 Opts::Private { output, encoding } => {238 let recipients = load_identities()?;239 write_private(&recipients, &output, stdin(), encoding)?;240 }241 Opts::Generate(generate) => {242 match generate {243 Generate::Ed25519 {244 public,245 private,246 no_embed_public,247 encoding,248 } => {249 use ed25519_dalek::SigningKey;250251 let recipients = load_identities()?;252 let mut secret = SecretKey::default();253 rng.fill_bytes(&mut secret);254 255 let key = SigningKey::from_bytes(&secret).to_keypair_bytes();256 write_public(&public, &key[32..], encoding)?;257 write_private(258 &recipients,259 &private,260 &key[..{ if no_embed_public { 32 } else { 64 } }],261 encoding,262 )?;263 }264 Generate::X25519 {265 public,266 private,267 encoding,268 } => {269 use x25519_dalek::{PublicKey, StaticSecret};270271 let recipients = load_identities()?;272 273 let key = StaticSecret::random();274 let public_key: PublicKey = (&key).into();275 write_public(&public, public_key.as_bytes().as_slice(), encoding)?;276 write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?;277 }278 Generate::Password {279 size,280 no_symbols,281 output,282 encoding,283 } => {284 ensure!(285 size >= 6,286 "misconfiguration? password is shorter than 6 chars"287 );288 let recipients = load_identities()?;289 let out = if no_symbols {290 Alphanumeric.sample_string(&mut rng, size)291 } else {292 293 const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";294 let uniform =295 Uniform::new(0, GEN_ASCII_SYMBOLS.len()).expect("range is valid");296 (0..size)297 .map(|_| uniform.sample(&mut rng))298 .map(|i| GEN_ASCII_SYMBOLS[i] as char)299 .collect::<String>()300 };301 write_private(&recipients, &output, out.as_bytes(), encoding)?;302 }303 Generate::Bytes {304 output,305 count,306 no_nuls,307 encoding,308 } => {309 ensure!(310 count >= 6,311 "misconfiguration? bytestring is shorter than 6 chars"312 );313 let recipients = load_identities()?;314 let mut bytes = vec![0u8; count];315 if no_nuls {316 let rand = Uniform::new_inclusive(0x1u8, 0xffu8)317 .expect("range is valid")318 .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}
1use std::{2 env,3 fs::{File, OpenOptions},4 io::{self, Read, Write, copy, stdin, stdout},5 str::FromStr,6};78use age::{9 Encryptor, Recipient,10 ssh::{ParseRecipientKeyError, Recipient as SshRecipient},11};12use anyhow::{Context, Result, anyhow, bail, ensure};13use base64::{Engine as _, engine::general_purpose::STANDARD, write::EncoderWriter};14use clap::{Parser, ValueEnum};15use ed25519_dalek::SecretKey;16use fleet_shared::SecretData;17use hmac::Mac as _;18use rand::{19 Rng as _,20 distr::{Alphanumeric, Distribution, SampleString, Uniform},21 rng,22};23use sha2::Digest as _;2425fn gen_password(rng: &mut impl rand::Rng, size: usize, no_symbols: bool) -> String {26 if no_symbols {27 Alphanumeric.sample_string(rng, size)28 } else {29 const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";30 let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len()).expect("range is valid");31 (0..size)32 .map(|_| uniform.sample(rng))33 .map(|i| GEN_ASCII_SYMBOLS[i] as char)34 .collect::<String>()35 }36}3738fn write_output_file(out: &str) -> Result<File> {39 let file = OpenOptions::new()40 .create_new(true)41 .write(true)42 .open(out)43 .with_context(|| format!("failed to open output {out:?}"))?;44 Ok(file)45}46fn write_public(out: &str, mut input: impl Read, encoding: OutputEncoding) -> Result<()> {47 let mut output = write_output_file(out)?;4849 let mut data = Vec::new();50 copy(&mut input, &mut wrap_encoder(&mut data, encoding))?;5152 output.write_all(53 SecretData {54 data,55 encrypted: false,56 }57 .to_string()58 .as_bytes(),59 )?;60 Ok(())61}62fn write_private(63 identities: &Identities,64 out: &str,65 mut input: impl Read,66 encoding: OutputEncoding,67) -> Result<()> {68 let mut output = write_output_file(out)?;69 let encryptor = make_encryptor(identities)?;7071 let mut data = Vec::new();72 {73 let mut encrypted_writer = encryptor.wrap_output(&mut data)?;74 copy(75 &mut input,76 &mut wrap_encoder(&mut encrypted_writer, encoding),77 )?;78 encrypted_writer.finish()?;79 };8081 output.write_all(82 SecretData {83 data,84 encrypted: true,85 }86 .to_string()87 .as_bytes(),88 )?;89 Ok(())90}9192type Identities = Vec<SshRecipient>;93fn load_identities() -> Result<Identities> {94 let list = env::var("GENERATOR_HELPER_IDENTITIES");95 let list = match list {96 Ok(v) => v,97 Err(env::VarError::NotPresent) => {98 bail!(99 "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"100 );101 }102 Err(e) => bail!("somehow, identities list is not utf-8: {e}"),103 };104 let list = list.trim();105 ensure!(!list.is_empty(), "no identities passed, can't encrypt data");106 list.lines()107 .map(age::ssh::Recipient::from_str)108 .collect::<Result<Identities, ParseRecipientKeyError>>()109 .map_err(|e| anyhow!("parse recipients: {e:?}"))110}111fn make_encryptor(r: &Identities) -> Result<Encryptor> {112 Ok(113 Encryptor::with_recipients(r.iter().map(|v| v as &dyn Recipient))114 .expect("list is not empty"),115 )116}117fn wrap_encoder<'t>(w: impl Write + 't, encoding: OutputEncoding) -> impl Write + 't {118 fn coerce<'t>(w: impl Write + 't) -> Box<dyn Write + 't> {119 Box::new(w)120 }121 match encoding {122 OutputEncoding::Raw => coerce(w),123 OutputEncoding::Base64 => {124 let writer = EncoderWriter::new(w, &STANDARD);125 coerce(writer)126 }127 OutputEncoding::Hex => {128 struct HexWriter<W>(W);129 impl<W> Write for HexWriter<W>130 where131 W: Write,132 {133 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {134 let encoded = hex::encode(buf);135 self.0.write_all(encoded.as_bytes())?;136 Ok(buf.len())137 }138139 fn flush(&mut self) -> io::Result<()> {140 self.0.flush()141 }142 }143 coerce(HexWriter(w))144 }145 }146}147148#[derive(Clone, Copy, ValueEnum, Default)]149enum OutputEncoding {150 151 #[default]152 Raw,153 154 Base64,155 156 Hex,157}158159#[derive(Parser)]160enum Generate {161 162 163 Ed25519 {164 #[arg(long, short = 'p')]165 public: String,166 #[arg(long, short = 's')]167 private: String,168 169 #[arg(long)]170 no_embed_public: bool,171 #[arg(long, short = 'e', value_enum, default_value_t)]172 encoding: OutputEncoding,173 },174 175 176 X25519 {177 #[arg(long, short = 'p')]178 public: String,179 #[arg(long, short = 's')]180 private: String,181 #[arg(long, short = 'e', value_enum, default_value_t)]182 encoding: OutputEncoding,183 },184 Password {185 #[arg(long, short = 'o')]186 output: String,187 #[arg(long)]188 size: usize,189 #[arg(long, short = 'n')]190 no_symbols: bool,191 #[arg(long, short = 'e', value_enum, default_value_t)]192 encoding: OutputEncoding,193 },194 PostgresPassword {195 #[arg(long, short = 's')]196 secret: String,197 #[arg(long, short = 'H')]198 hash: String,199 #[arg(long, default_value_t = 24)]200 size: usize,201 #[arg(long, default_value_t = 4096)]202 iterations: u32,203 #[arg(long, short = 'n')]204 no_symbols: bool,205 },206 Bytes {207 #[arg(long, short = 'o')]208 output: String,209 #[arg(long, short = 'c')]210 count: usize,211 212 #[arg(long)]213 no_nuls: bool,214 #[arg(long, short = 'e', value_enum, default_value_t)]215 encoding: OutputEncoding,216 },217}218219#[derive(Parser)]220enum Opts {221 222 Public {223 #[arg(long, short = 'o')]224 output: String,225 #[arg(long, short = 'e', value_enum, default_value_t)]226 encoding: OutputEncoding,227 },228 229 Private {230 #[arg(long, short = 'o')]231 output: String,232 #[arg(long, short = 'e', value_enum, default_value_t)]233 encoding: OutputEncoding,234 },235 236 237 238 239 Decode {240 #[arg(long, short = 'i')]241 input: String,242 },243 244 245 246 247 248 249 250 #[command(subcommand)]251 Generate(Generate),252}253254fn main() -> Result<()> {255 let opts = Opts::parse();256 257 let mut rng = rng();258259 match opts {260 Opts::Public { output, encoding } => {261 write_public(&output, stdin(), encoding)?;262 }263 Opts::Private { output, encoding } => {264 let recipients = load_identities()?;265 write_private(&recipients, &output, stdin(), encoding)?;266 }267 Opts::Generate(generate) => {268 match generate {269 Generate::Ed25519 {270 public,271 private,272 no_embed_public,273 encoding,274 } => {275 use ed25519_dalek::SigningKey;276277 let recipients = load_identities()?;278 let mut secret = SecretKey::default();279 rng.fill_bytes(&mut secret);280 281 let key = SigningKey::from_bytes(&secret).to_keypair_bytes();282 write_public(&public, &key[32..], encoding)?;283 write_private(284 &recipients,285 &private,286 &key[..{ if no_embed_public { 32 } else { 64 } }],287 encoding,288 )?;289 }290 Generate::X25519 {291 public,292 private,293 encoding,294 } => {295 use x25519_dalek::{PublicKey, StaticSecret};296297 let recipients = load_identities()?;298 299 let key = StaticSecret::random();300 let public_key: PublicKey = (&key).into();301 write_public(&public, public_key.as_bytes().as_slice(), encoding)?;302 write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?;303 }304 Generate::Password {305 size,306 no_symbols,307 output,308 encoding,309 } => {310 ensure!(311 size >= 6,312 "misconfiguration? password is shorter than 6 chars"313 );314 let recipients = load_identities()?;315 let out = gen_password(&mut rng, size, no_symbols);316 write_private(&recipients, &output, out.as_bytes(), encoding)?;317 }318 Generate::PostgresPassword {319 secret,320 hash,321 size,322 iterations,323 no_symbols,324 } => {325 ensure!(326 size >= 6,327 "misconfiguration? password is shorter than 6 chars"328 );329 let recipients = load_identities()?;330 let password = gen_password(&mut rng, size, no_symbols);331332 let mut salt = [0u8; 16];333 rng.fill_bytes(&mut salt);334 let salted = pbkdf2::pbkdf2_hmac_array::<sha2::Sha256, 32>(335 password.as_bytes(),336 &salt,337 iterations,338 );339340 type HmacSha256 = hmac::Hmac<sha2::Sha256>;341 let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(&salted)342 .expect("HMAC accepts any key length");343 mac.update(b"Client Key");344 let client_key = mac.finalize().into_bytes();345346 let mut hasher = sha2::Sha256::new();347 hasher.update(client_key);348 let stored_key = hasher.finalize();349350 let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(&salted)351 .expect("HMAC accepts any key length");352 mac.update(b"Server Key");353 let server_key = mac.finalize().into_bytes();354355 let hash_str = format!(356 "SCRAM-SHA-256${}:{}${}:{}",357 iterations,358 STANDARD.encode(salt),359 STANDARD.encode(stored_key),360 STANDARD.encode(server_key),361 );362363 write_private(364 &recipients,365 &secret,366 password.as_bytes(),367 OutputEncoding::Raw,368 )?;369 write_public(&hash, hash_str.as_bytes(), OutputEncoding::Raw)?;370 }371 Generate::Bytes {372 output,373 count,374 no_nuls,375 encoding,376 } => {377 ensure!(378 count >= 6,379 "misconfiguration? bytestring is shorter than 6 chars"380 );381 let recipients = load_identities()?;382 let mut bytes = vec![0u8; count];383 if no_nuls {384 let rand = Uniform::new_inclusive(0x1u8, 0xffu8)385 .expect("range is valid")386 .sample_iter(&mut rng);387 for (byte, rand) in bytes.iter_mut().zip(rand) {388 *byte = rand;389 }390 } else {391 rng.fill_bytes(&mut bytes);392 };393 write_private(&recipients, &output, bytes.as_slice(), encoding)?;394 }395 }396 }397 Opts::Decode { input } => {398 let mut data = Vec::new();399 File::open(input)?.read_to_end(&mut data)?;400 let data = String::from_utf8(data).context(401 "encoded data is always utf-8, you are trying to use decode the wrong way.",402 )?;403 let data =404 SecretData::from_str(&data).map_err(|e| anyhow!("failed to decode data: {e}"))?;405 ensure!(406 !data.encrypted,407 "you can not decrypt secret data, only decode public."408 );409 stdout().write_all(&data.data)?;410 }411 }412 Ok(())413}