use crate::api::{decode, encode, Options};
use crate::dictionaries;
use crate::kdf::KdfParams;
use crate::lab::engine::Rng;
use std::time::{Duration, Instant};
pub struct GuessReport {
pub attempts: usize,
pub cracked: usize,
pub total: Duration,
pub per_guess: Duration,
}
impl GuessReport {
pub fn cost_years(&self, keyspace_bits: u32) -> f64 {
let secs_per = self.per_guess.as_secs_f64();
let guesses = 2f64.powi(keyspace_bits as i32);
secs_per * guesses / (365.25 * 24.0 * 3600.0)
}
}
pub fn guessing_cost(guesses: usize, seed: u64) -> GuessReport {
let dict = dictionaries::ascii94();
let opts = Options {
pepper: b"",
kdf_params: KdfParams {
mem_kib: 16 * 1024,
iterations: 2,
parallelism: 1,
},
codebook_id: 0,
};
let real_pass = "correcta-y-fuera-del-ranking-2026";
let secret = b"tesoro";
let sym = encode(secret, real_pass, &dict, &opts);
let mut rng = Rng::seeded(seed);
let mut cracked = 0usize;
let start = Instant::now();
for i in 0..guesses {
let guess = format!("guess-{}-{}", i, rng.next_u64());
if decode(&sym, &guess, &dict, b"").is_ok() {
cracked += 1;
}
}
let total = start.elapsed();
let per_guess = if guesses == 0 {
Duration::ZERO
} else {
total / guesses as u32
};
GuessReport {
attempts: guesses,
cracked,
total,
per_guess,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ranked_guesses_never_crack_and_cost_holds() {
let report = guessing_cost(64, 2026);
assert_eq!(report.attempts, 64);
assert_eq!(report.cracked, 0, "ningún intento del ranking debe descifrar");
assert!(
report.per_guess > Duration::ZERO,
"cada intento debe costar una derivación real"
);
}
}