use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use std::collections::HashMap;
pub mod crypticli;
pub mod word_list;
pub struct Generator {
pub rng: ChaCha8Rng,
depth: usize,
jump_table: HashMap<String, Distribution>,
}
impl Default for Generator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
struct Distribution {
tokens: Vec<String>,
entropies: Vec<f64>,
counts: Vec<usize>,
total: usize,
}
impl Generator {
pub fn new_custom(tokens: Vec<String>, depth: usize) -> Option<Generator> {
let depth = depth.max(1);
let rng = ChaCha8Rng::from_entropy();
let transition_matrix = transition_matrix_from_tokens(tokens, depth);
if transition_matrix.len() == 0 {
return None;
}
let jump_table = jump_table_from_transition_matrix(transition_matrix);
if jump_table.len() == 0 {
return None;
}
Some(Generator {
rng: rng,
depth: max_depth(&jump_table),
jump_table: jump_table,
})
}
pub fn new() -> Generator {
Generator::new_custom(word_list::eff::list(), 3).unwrap()
}
pub fn new_he() -> Generator {
Generator::new_custom(word_list::eff::list(), 2).unwrap()
}
pub fn gen_from_pattern(&mut self, pattern: &str) -> (String, f64) {
let mut passphrase = String::new();
let mut entropy = 0.0;
let mut iter = pattern.chars().peekable();
loop {
let cs = iter.next();
if let Some(c) = cs {
if c == '\\' {
if let Some(cn) = iter.next() {
passphrase.push(cn);
}
continue;
}
match c {
'w' | 'W' => {
let mut nlen = 0;
while nlen < 8 {
let (mut tok, h) = self.gen_next_token(&passphrase).unwrap();
if c == 'W' && nlen == 0 {
tok = uppercase_first_letter(&tok);
}
passphrase.push_str(&tok);
entropy += h;
nlen += self.depth;
}
}
's' | 'd' => {
let symbols = if c == 's' {
"@#!$%&=?^+-*\""
} else {
"0987654321"
};
let d = self.rng.gen_range(0..symbols.len());
passphrase.push(symbols.chars().nth(d).unwrap());
entropy += (symbols.len() as f64).log2();
}
'c' | 'C' => {
let (mut tok, h) = self.gen_next_token(&passphrase).unwrap();
if c == 'C' {
tok = uppercase_first_letter(&tok);
}
passphrase.push_str(&tok);
entropy += h;
}
_ => {
passphrase.push(c);
}
}
} else {
break;
}
}
(passphrase, entropy)
}
pub fn gen_next_token(&mut self, seed: &str) -> Option<(String, f64)> {
let sl = seed[seed.len().saturating_sub(self.depth)..].to_lowercase();
let mut tok = sl.as_str();
loop {
if let Some(tr) = self.jump_table.get(tok) {
let n = self.rng.gen_range(0..tr.total);
for (i, v) in tr.counts.iter().enumerate() {
if n < *v {
return Some((tr.tokens[i].clone(), tr.entropies[i]));
}
}
return None;
}
if tok.len() == 0 {
return None;
}
tok = &tok[1..];
}
}
}
fn max_depth(jump_table: &HashMap<String, Distribution>) -> usize {
let mut t_depth = 0;
for (k, v) in jump_table.iter() {
t_depth = t_depth.max(k.len());
for c in v.tokens.iter() {
t_depth = t_depth.max(c.len());
}
}
t_depth
}
fn transition_matrix_from_tokens(
tokens: Vec<String>,
depth: usize,
) -> HashMap<String, HashMap<String, usize>> {
let mut transition_matrix: HashMap<String, HashMap<String, usize>> = HashMap::new();
let mut put = |str: String, r: String| {
transition_matrix
.entry(str)
.or_default()
.entry(r)
.and_modify(|count| *count += 1)
.or_insert(1);
};
for raw_w in tokens.iter() {
let sl = raw_w.trim().to_lowercase();
if sl.len() == 0 {
continue;
}
let sb: Vec<char> = sl.chars().collect::<Vec<char>>();
for i in 0..sb.len() {
let from = sb[i.saturating_sub(depth)..i].into_iter().collect();
let to = sb[i..(i + depth).min(sb.len())].into_iter().collect();
if to == "" || from == to {
continue;
}
put(from, to);
}
}
transition_matrix
}
fn jump_table_from_transition_matrix(
transition_matrix: HashMap<String, HashMap<String, usize>>,
) -> HashMap<String, Distribution> {
let mut dist_trans_matrix = HashMap::new();
for (k, rfreq) in transition_matrix.into_iter() {
let total: usize = rfreq.values().sum();
let mut counts = Vec::new();
let mut tokens = Vec::new();
let mut entropies = Vec::new();
let mut cum = 0;
for (token, &freq) in rfreq.iter() {
let p = freq as f64 / total as f64;
cum += freq;
entropies.push(-p.log2());
counts.push(cum);
tokens.push(token.clone());
}
dist_trans_matrix.insert(
k,
Distribution {
tokens,
entropies,
counts,
total,
},
);
}
dist_trans_matrix
}
fn uppercase_first_letter(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
#[cfg(test)]
mod tests {
use word_list::debug;
use super::*;
fn certify(pattern: &str) -> bool {
let mut gen = Generator::new_custom(debug::list(), 2).unwrap();
gen.rng = ChaCha8Rng::seed_from_u64(0x5792CBF); let mut hist = HashMap::<String, usize>::new();
let mut tot_h: f64 = 0.0;
let mut tot_c: f64 = 1e-16;
let mut q = 128;
let mut old_entropy = 0.0;
println!("testing pattern {}", pattern);
loop {
for _ in 0..q {
let (pw, h) = gen.gen_from_pattern(pattern);
tot_h += h;
tot_c += 1.0;
let v = hist.get(&pw).or(Some(&0)).unwrap();
hist.insert(pw, v + 1);
}
q += q / 16;
let avg_h = tot_h / tot_c;
let mut entropy = 0.0 as f64;
for (_, &v) in hist.iter() {
let p = v as f64 / tot_c;
entropy += -p * p.log2();
}
entropy += (hist.len() as f64 - 1.0) / (2.0 * tot_c);
if (entropy - avg_h).abs() < 1e-2 {
println!("- PASSED! unique words {}", hist.len());
return true;
}
if (entropy - old_entropy).abs() < 1e-5 {
println!("- WARNING, entropies {} {}.", entropy, avg_h);
if (entropy - old_entropy).abs() < 1e-6 {
println!("- FAILED, entropies {} {}.", entropy, avg_h);
return false;
}
}
old_entropy = entropy;
}
}
#[test]
fn test_word() {
assert!(certify(""));
assert!(certify("d"));
assert!(certify("s"));
assert!(certify("c"));
assert!(certify("cc"));
assert!(certify("w"));
assert!(certify("ww"));
assert!(certify("w.w"));
assert!(certify("c.c"));
assert!(certify("ccc"));
assert!(certify("sdc"));
assert!(certify("literal"));
}
}