feat(controller/generator): introduce bytes/pwd/ssh generators
This commit is contained in:
parent
683869982f
commit
2020b32021
2 changed files with 315 additions and 20 deletions
291
kuyaseg-controller/src/generator.rs
Normal file
291
kuyaseg-controller/src/generator.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
#![feature(sync_nonpoison, nonpoison_rwlock)]
|
||||
|
||||
pub mod generator;
|
||||
pub mod reporting;
|
||||
pub mod utils;
|
||||
|
|
|
|||
Loading…
Reference in a new issue