use crate::antihacker;
use crate::kdf::{self, KdfParams, SALT_LEN};
const HONEY_INFO: &[u8] = b"quipu-honey-v1/pad";
const STREAM_BYTES_PER_TOKEN: usize = 8;
const MAGIC: [u8; 4] = *b"QHNY";
const VERSION: u8 = 1;
const HEADER_LEN: usize = 4 + 1 + SALT_LEN + 4 + 4 + 4 + 2 + 4;
const MAX_TOKENS: usize = 1000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HoneyError {
TokenOutOfRange,
BadAlphabet,
BadLength,
Truncated,
BadMagic,
UnsupportedVersion(u8),
InsaneKdf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Alphabet {
size: u16,
}
impl Alphabet {
pub fn new(size: u16) -> Result<Self, HoneyError> {
if size < 2 {
return Err(HoneyError::BadAlphabet);
}
Ok(Self { size })
}
pub fn digits() -> Self {
Self { size: 10 }
}
pub fn size(&self) -> u16 {
self.size
}
}
#[derive(Debug, Clone)]
pub struct HoneyOptions<'a> {
pub pepper: &'a [u8],
pub kdf_params: KdfParams,
}
impl Default for HoneyOptions<'_> {
fn default() -> Self {
Self {
pepper: b"",
kdf_params: KdfParams::default(),
}
}
}
fn keystream_digits(
passphrase: &str,
salt: &[u8; SALT_LEN],
pepper: &[u8],
params: &KdfParams,
size: u16,
len: usize,
) -> Vec<u16> {
let mut master = kdf::derive_master_key(passphrase, salt, pepper, params);
let mut buf = vec![0u8; len * STREAM_BYTES_PER_TOKEN];
kdf::derive_stream(&master, HONEY_INFO, &mut buf);
antihacker::wipe(&mut master);
let a = size as u64;
let mut out = Vec::with_capacity(len);
for chunk in buf.chunks_exact(STREAM_BYTES_PER_TOKEN) {
let v = u64::from_be_bytes(chunk.try_into().expect("8 bytes"));
out.push((v % a) as u16);
}
antihacker::wipe(&mut buf);
out
}
pub fn encrypt(
tokens: &[u16],
alphabet: Alphabet,
passphrase: &str,
opts: &HoneyOptions,
) -> Result<Vec<u8>, HoneyError> {
if tokens.is_empty() || tokens.len() > MAX_TOKENS {
return Err(HoneyError::BadLength);
}
if tokens.iter().any(|&t| t >= alphabet.size) {
return Err(HoneyError::TokenOutOfRange);
}
let mut salt = [0u8; SALT_LEN];
getrandom::getrandom(&mut salt).expect("RNG del sistema");
let ks = keystream_digits(
passphrase,
&salt,
opts.pepper,
&opts.kdf_params,
alphabet.size,
tokens.len(),
);
let a = alphabet.size;
let mut out = Vec::with_capacity(HEADER_LEN + tokens.len() * 2);
out.extend_from_slice(&MAGIC);
out.push(VERSION);
out.extend_from_slice(&salt);
out.extend_from_slice(&opts.kdf_params.mem_kib.to_be_bytes());
out.extend_from_slice(&opts.kdf_params.iterations.to_be_bytes());
out.extend_from_slice(&opts.kdf_params.parallelism.to_be_bytes());
out.extend_from_slice(&a.to_be_bytes());
out.extend_from_slice(&(tokens.len() as u32).to_be_bytes());
for (i, &t) in tokens.iter().enumerate() {
let c = ((t as u32 + ks[i] as u32) % a as u32) as u16;
out.extend_from_slice(&c.to_be_bytes());
}
Ok(out)
}
pub fn decrypt(blob: &[u8], passphrase: &str, pepper: &[u8]) -> Result<Vec<u16>, HoneyError> {
if blob.len() < HEADER_LEN {
return Err(HoneyError::Truncated);
}
if blob[0..4] != MAGIC {
return Err(HoneyError::BadMagic);
}
let version = blob[4];
if version != VERSION {
return Err(HoneyError::UnsupportedVersion(version));
}
let mut salt = [0u8; SALT_LEN];
salt.copy_from_slice(&blob[5..5 + SALT_LEN]);
let mut p = 5 + SALT_LEN;
let mem_kib = u32::from_be_bytes(blob[p..p + 4].try_into().expect("4 bytes"));
p += 4;
let iterations = u32::from_be_bytes(blob[p..p + 4].try_into().expect("4 bytes"));
p += 4;
let parallelism = u32::from_be_bytes(blob[p..p + 4].try_into().expect("4 bytes"));
p += 4;
let size = u16::from_be_bytes(blob[p..p + 2].try_into().expect("2 bytes"));
p += 2;
let len = u32::from_be_bytes(blob[p..p + 4].try_into().expect("4 bytes")) as usize;
p += 4;
if size < 2 {
return Err(HoneyError::BadAlphabet);
}
if len == 0 || len > MAX_TOKENS {
return Err(HoneyError::BadLength);
}
let params = KdfParams {
mem_kib,
iterations,
parallelism,
};
if !params.is_sane() {
return Err(HoneyError::InsaneKdf);
}
if blob.len() < HEADER_LEN + len * 2 {
return Err(HoneyError::Truncated);
}
let ks = keystream_digits(passphrase, &salt, pepper, ¶ms, size, len);
let a = size as u32;
let mut out = Vec::with_capacity(len);
for i in 0..len {
let c = u16::from_be_bytes(blob[p + i * 2..p + i * 2 + 2].try_into().expect("2 bytes"));
let t = ((c as u32 + a - ks[i] as u32) % a) as u16;
out.push(t);
}
Ok(out)
}
pub fn encrypt_pin(pin: &str, passphrase: &str, opts: &HoneyOptions) -> Result<Vec<u8>, HoneyError> {
let tokens = pin_to_tokens(pin)?;
encrypt(&tokens, Alphabet::digits(), passphrase, opts)
}
pub fn decrypt_pin(blob: &[u8], passphrase: &str, pepper: &[u8]) -> Result<String, HoneyError> {
let tokens = decrypt(blob, passphrase, pepper)?;
Ok(tokens.iter().map(|&t| (b'0' + t as u8) as char).collect())
}
fn pin_to_tokens(pin: &str) -> Result<Vec<u16>, HoneyError> {
if pin.is_empty() || pin.len() > MAX_TOKENS {
return Err(HoneyError::BadLength);
}
pin.chars()
.map(|c| c.to_digit(10).map(|d| d as u16).ok_or(HoneyError::TokenOutOfRange))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn cheap() -> HoneyOptions<'static> {
HoneyOptions {
pepper: b"",
kdf_params: KdfParams {
mem_kib: 64,
iterations: 1,
parallelism: 1,
},
}
}
#[test]
fn pin_round_trips_with_correct_passphrase() {
let blob = encrypt_pin("4913", "correcta", &cheap()).unwrap();
assert_eq!(decrypt_pin(&blob, "correcta", b"").unwrap(), "4913");
}
#[test]
fn wrong_passphrase_yields_a_valid_decoy_not_an_error() {
let blob = encrypt_pin("4913", "correcta", &cheap()).unwrap();
let decoy = decrypt_pin(&blob, "incorrecta", b"").unwrap();
assert_eq!(decoy.len(), 4, "el señuelo tiene la misma longitud");
assert!(decoy.chars().all(|c| c.is_ascii_digit()), "señuelo válido: {decoy}");
}
#[test]
fn no_success_oracle_over_a_brute_force_batch() {
let blob = encrypt_pin("271828", "la-correcta", &cheap()).unwrap();
let mut seen = std::collections::HashSet::new();
for i in 0..200 {
let guess = format!("intento-{i}");
let pin = decrypt_pin(&blob, &guess, b"").expect("nunca hay error de clave");
assert_eq!(pin.len(), 6);
assert!(pin.chars().all(|c| c.is_ascii_digit()));
seen.insert(pin);
}
assert!(seen.len() > 100, "señuelos poco dispersos: {}", seen.len());
}
#[test]
fn decoys_are_roughly_uniform() {
let blob = encrypt_pin("0000", "real", &cheap()).unwrap();
let mut hist = [0u32; 10];
for i in 0..1000 {
let pin = decrypt_pin(&blob, &format!("g{i}"), b"").unwrap();
let d = pin.as_bytes()[0] - b'0';
hist[d as usize] += 1;
}
assert!(hist.iter().all(|&h| h > 40 && h < 180), "histograma sesgado: {hist:?}");
}
#[test]
fn pepper_changes_the_result() {
let blob = encrypt_pin("4913", "clave", &cheap()).unwrap();
assert_eq!(decrypt_pin(&blob, "clave", b"").unwrap(), "4913");
let with_pepper = decrypt_pin(&blob, "clave", b"pepper-distinto").unwrap();
assert_ne!(with_pepper, "4913");
assert_eq!(with_pepper.len(), 4);
}
#[test]
fn deterministic_given_the_same_salt() {
let blob = encrypt_pin("1234", "clave", &cheap()).unwrap();
assert_eq!(
decrypt_pin(&blob, "otra", b"").unwrap(),
decrypt_pin(&blob, "otra", b"").unwrap()
);
}
#[test]
fn generic_alphabet_round_trips() {
let ab = Alphabet::new(2048).unwrap();
let secret = vec![1337u16, 42, 2000, 0, 2047];
let blob = encrypt(&secret, ab, "frase-clave", &cheap()).unwrap();
assert_eq!(decrypt(&blob, "frase-clave", b"").unwrap(), secret);
let decoy = decrypt(&blob, "otra-frase", b"").unwrap();
assert_eq!(decoy.len(), 5);
assert!(decoy.iter().all(|&t| t < 2048));
}
#[test]
fn rejects_token_out_of_range() {
let ab = Alphabet::new(10).unwrap();
assert_eq!(encrypt(&[9, 10], ab, "k", &cheap()), Err(HoneyError::TokenOutOfRange));
}
#[test]
fn rejects_degenerate_alphabet() {
assert_eq!(Alphabet::new(1).err(), Some(HoneyError::BadAlphabet));
}
#[test]
fn decrypt_rejects_structural_corruption() {
let mut blob = encrypt_pin("4913", "clave", &cheap()).unwrap();
blob[0] ^= 0xFF; assert_eq!(decrypt_pin(&blob, "clave", b""), Err(HoneyError::BadMagic));
}
#[test]
fn decrypt_rejects_insane_kdf_params_before_deriving() {
let mut blob = encrypt_pin("4913", "clave", &cheap()).unwrap();
let off = 5 + SALT_LEN;
blob[off..off + 4].copy_from_slice(&0xFFFF_FFFFu32.to_be_bytes());
assert_eq!(decrypt_pin(&blob, "clave", b""), Err(HoneyError::InsaneKdf));
}
}