feat(controller/generator): introduce bytes/pwd/ssh generators

This commit is contained in:
spushk 2026-05-15 18:07:40 +05:00 • committed by spushk
commit 2020b32021
Signed by: spushk
GPG key ID: 7797B761E105E8AA
2 changed files with 315 additions and 20 deletions

View file

@ -0,0 +1,291 @@
use itertools::Itertools;
use rand::prelude::{
CryptoRng,
RngExt,
};
use rand::{
TryCryptoRng,
TryRng,
};
use zeroize::{
Zeroize,
ZeroizeOnDrop,
Zeroizing,
};
#[derive(Debug, thiserror::Error)]
pub enum GeneratorErr {
#[error("Distribution to sample from was empty")]
EmptyDistribution,
#[error("RNG stuck during generation")]
RngStuck { reason: String },
#[error("Failed to generate SSH pair")]
SshPair(#[from] ssh_key::Error),
}
pub fn bytes<G: CryptoRng>(rng: &mut G, size: usize) -> Vec<u8> {
let mut buf = vec![0u8; size];
rng.fill_bytes(&mut buf);
buf
}
pub fn str_bytes<G: CryptoRng>(rng: &mut G, length: usize, dictionary: &[char]) -> Result<Vec<u8>, GeneratorErr> {
let mut buf = String::with_capacity(length);
let dictionary = Zeroizing::new(dictionary.iter().unique().cloned().collect::<Vec<char>>());
let distribution = rand::distr::slice::Choose::new(&dictionary).map_err(|_| GeneratorErr::EmptyDistribution)?;
let dict_size = dictionary.len();
if dict_size > 3 {
let mut last_chars = Zeroizing::new(['\0'; 3]);
for l in 0..length {
// est for repetitions
let max_attempts = (dict_size * 4).max(64);
for _ in 0..max_attempts {
let sample = rng.sample(distribution);
if !last_chars.contains(sample) {
(last_chars[0], last_chars[1], last_chars[2]) = (last_chars[1], last_chars[2], *sample);
buf.push(*sample);
break;
}
}
if buf.len() != l + 1 {
return Err(GeneratorErr::RngStuck {
reason: format!(
"Unable to sample {length} characters from dictionary without 3-n repetitions for {max_attempts} attempts"
),
});
}
}
} else {
for _ in 0..length {
buf.push(*rng.sample(distribution));
}
}
if buf.len() != length {
Err(GeneratorErr::RngStuck {
reason: format!("Unable to sample {length} characters"),
})
} else {
Ok(buf.into_bytes())
}
}
/// Generate SSH keypair, encoded for OpenSSH
pub fn ssh_pair<G: CryptoRng>(rng: &mut G, algorithm: SshAlgorithm) -> Result<SshPair, GeneratorErr> {
let privkey = ssh_key::PrivateKey::random(
rng,
match algorithm {
SshAlgorithm::Ed25519 => ssh_key::Algorithm::Ed25519,
},
)?;
let pubkey = privkey.public_key();
let privkey_encoded = privkey.to_openssh(ssh_key::LineEnding::LF)?;
let pubkey_encoded = pubkey.to_openssh()?;
Ok(SshPair {
pubkey: pubkey_encoded.into_bytes(),
privkey: privkey_encoded.as_bytes().to_vec(),
})
}
pub enum SshAlgorithm {
Ed25519,
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct SshPair {
pub pubkey: Vec<u8>,
pub privkey: Vec<u8>,
}
pub struct AwsRng(aws_lc_rs::rand::SystemRandom);
impl AwsRng {
pub fn new() -> Self {
Self(aws_lc_rs::rand::SystemRandom::new())
}
}
impl Default for AwsRng {
fn default() -> Self {
Self::new()
}
}
impl TryRng for AwsRng {
type Error = core::convert::Infallible;
fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
let mut buf = [0u8; 4];
self.try_fill_bytes(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
let mut buf = [0u8; 8];
self.try_fill_bytes(&mut buf)?;
Ok(u64::from_le_bytes(buf))
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
aws_lc_rs::rand::fill(dest).expect("aws-lc-rs fill failed");
Ok(())
}
}
impl TryCryptoRng for AwsRng {}
#[cfg(test)]
mod test {
use rand::prelude::SeedableRng;
use rand_hc::Hc128Rng;
use super::*;
fn assert_no_3_peat(bytes: &[u8]) {
let s = core::str::from_utf8(bytes).expect("not valid utf8");
let chars: Vec<char> = s.chars().collect();
for win in chars.windows(3) {
assert!(win.iter().all_unique(), "3-peat found: {:?}", win);
}
}
fn assert_dict_chars(bytes: &[u8], dict: &[char]) {
let s = core::str::from_utf8(bytes).expect("not valid utf8");
for c in s.chars() {
assert!(dict.contains(&c), "char {c} not in dict");
}
}
fn verify_ssh_pair(pair: &SshPair) {
let pubkey = ssh_key::PublicKey::from_openssh(core::str::from_utf8(&pair.pubkey).unwrap()).unwrap();
let privkey = ssh_key::PrivateKey::from_openssh(core::str::from_utf8(&pair.privkey).unwrap()).unwrap();
assert_eq!(privkey.public_key(), &pubkey);
}
fn dict_alnum(size: usize) -> Vec<char> {
"a1b2c3d4e5f6g7h8i9j0klmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
.chars()
.take(size)
.collect()
}
#[test]
fn random_bytes() {
let mut rng = AwsRng::new();
// length
assert_eq!(bytes(&mut rng, 0).len(), 0);
assert_eq!(bytes(&mut rng, 1).len(), 1);
assert_eq!(bytes(&mut rng, 32).len(), 32);
assert_eq!(bytes(&mut rng, 128).len(), 128);
assert_eq!(bytes(&mut rng, 1024).len(), 1024);
// rand
assert_ne!(bytes(&mut rng, 1024), bytes(&mut rng, 1024));
}
#[test]
fn random_str_bytes() {
let mut rng = AwsRng::new();
let dict = dict_alnum(32);
let gen0 = str_bytes(&mut rng, 0, &dict).unwrap();
let gen1 = str_bytes(&mut rng, 1, &dict).unwrap();
let gen27 = str_bytes(&mut rng, 27, &dict).unwrap();
let gen256 = str_bytes(&mut rng, 256, &dict).unwrap();
assert_ne!(str_bytes(&mut rng, 256, &dict).unwrap(), gen256);
// length
assert_eq!(gen0.len(), 0);
assert_eq!(gen1.len(), 1);
assert_eq!(gen27.len(), 27);
assert_eq!(gen256.len(), 256);
// dict
assert_dict_chars(&gen0, &dict);
assert_dict_chars(&gen1, &dict);
assert_dict_chars(&gen27, &dict);
assert_dict_chars(&gen256, &dict);
// reps
assert_no_3_peat(&gen27);
assert_no_3_peat(&gen256);
}
#[test]
fn random_str_bytes_dict_3() {
let mut rng = AwsRng::new();
let dict: Vec<char> = dict_alnum(3);
let gen16 = str_bytes(&mut rng, 16, &dict).unwrap();
assert_eq!(gen16.len(), 16);
assert_dict_chars(&gen16, &dict);
}
#[test]
fn random_ssh_pair_ed25519() {
let mut rng = AwsRng::new();
let pair = ssh_pair(&mut rng, SshAlgorithm::Ed25519).unwrap();
verify_ssh_pair(&pair);
}
// ====== Deterministic (Hc128Rng) ======
#[test]
fn deterministic_bytes() {
// we believe that generation is the same as for random, so our concern here is only value stability
assert_eq!(
bytes(&mut Hc128Rng::seed_from_u64(42), 256),
bytes(&mut Hc128Rng::seed_from_u64(42), 256)
);
assert_ne!(
bytes(&mut Hc128Rng::seed_from_u64(42), 256),
bytes(&mut Hc128Rng::seed_from_u64(43), 256)
);
}
#[test]
fn deterministic_str_bytes() {
// we believe that generation is the same as for random, so our concern here is only value stability
let dict = dict_alnum(32);
assert_eq!(
str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, &dict).unwrap(),
str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, &dict).unwrap()
);
assert_ne!(
str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, &dict).unwrap(),
str_bytes(&mut Hc128Rng::seed_from_u64(43), 256, &dict).unwrap()
);
}
#[test]
fn deterministic_ssh_pair_ed25519() {
// we believe that generation is the same as for random, so our concern here is only value stability
assert_eq!(
ssh_pair(&mut Hc128Rng::seed_from_u64(42), SshAlgorithm::Ed25519)
.unwrap()
.privkey,
ssh_pair(&mut Hc128Rng::seed_from_u64(42), SshAlgorithm::Ed25519)
.unwrap()
.privkey
);
assert_ne!(
ssh_pair(&mut Hc128Rng::seed_from_u64(42), SshAlgorithm::Ed25519)
.unwrap()
.privkey,
ssh_pair(&mut Hc128Rng::seed_from_u64(43), SshAlgorithm::Ed25519)
.unwrap()
.privkey
);
}
}

View file

@ -1,4 +1,5 @@
#![feature(sync_nonpoison, nonpoison_rwlock)]
pub mod generator;
pub mod reporting;
pub mod utils;