use crate::patterns::Pattern;
use crate::segment::{CharsetName, Segment};
use rand::rngs::OsRng;
use std::sync::Arc;
const ENTROPY_HARD_FAIL_BITS: f64 = 83.0;
const ENTROPY_WARN_BITS: f64 = 131.0;
fn calculate_entropy(variable_len: usize, charset_size: usize) -> f64 {
if charset_size <= 1 {
return 0.0;
}
variable_len as f64 * (charset_size as f64).log2()
}
#[derive(Debug, Clone)]
pub struct SecretOptions {
pub anchor_len: usize,
pub tail_anchor_len: usize,
pub restrict_charset: bool,
pub force: bool,
}
impl Default for SecretOptions {
fn default() -> Self {
Self {
anchor_len: 3,
tail_anchor_len: 0,
restrict_charset: false,
force: false,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SecretError {
#[error("secret too short for the given anchor_len")]
TooShort,
#[error(
"anchor_len {anchor_len} is too short (minimum 2, recommended 3+); a 0- or 1-byte anchor cannot pre-filter candidates reliably"
)]
AnchorTooShort {
anchor_len: usize,
},
#[error(
"anchor_len ({anchor_len}) + tail_anchor_len ({tail_anchor_len}) >= secret length ({secret_len}); no variable bytes remain"
)]
NoVariableBytes {
anchor_len: usize,
tail_anchor_len: usize,
secret_len: usize,
},
#[error("fake generation exhausted {attempts} attempts; charset too small for variable length")]
CollisionLimit {
attempts: u32,
},
#[error(
"insufficient entropy: {bits:.1} bits < {threshold:.1} bit minimum (use --force to override)"
)]
InsufficientEntropy {
bits: f64,
threshold: f64,
},
}
pub fn register(secret: &[u8]) -> Result<Pattern, SecretError> {
register_with_options_rng(secret, &SecretOptions::default(), &mut OsRng)
}
pub fn register_with_options(secret: &[u8], opts: &SecretOptions) -> Result<Pattern, SecretError> {
register_with_options_rng(secret, opts, &mut OsRng)
}
#[cfg(test)]
pub(crate) fn register_with_rng<R: rand::RngCore>(
secret: &[u8],
rng: &mut R,
) -> Result<Pattern, SecretError> {
register_with_options_rng(secret, &SecretOptions::default(), rng)
}
pub(crate) fn register_with_options_rng<R: rand::RngCore>(
secret: &[u8],
opts: &SecretOptions,
rng: &mut R,
) -> Result<Pattern, SecretError> {
if opts.anchor_len < 2 {
return Err(SecretError::AnchorTooShort {
anchor_len: opts.anchor_len,
});
}
if secret.is_empty() {
return Err(SecretError::TooShort);
}
if secret.len() < opts.anchor_len {
return Err(SecretError::TooShort);
}
if opts.anchor_len < 3 {
log::warn!(
"doppel: anchor_len {} is below the recommended minimum of 3; short anchors generate more false Aho-Corasick candidates",
opts.anchor_len
);
}
let anchor_len = opts.anchor_len;
let tail_anchor_len = opts
.tail_anchor_len
.min(secret.len().saturating_sub(anchor_len));
let middle_start = anchor_len;
let middle_end = secret.len().saturating_sub(tail_anchor_len);
let middle_len = middle_end.saturating_sub(middle_start);
if middle_len == 0 {
return Err(SecretError::NoVariableBytes {
anchor_len,
tail_anchor_len,
secret_len: secret.len(),
});
}
let middle_bytes = &secret[middle_start..middle_end];
let (charset, charset_size) = if opts.restrict_charset {
let detected_name = crate::segment::detect_charset_name(middle_bytes);
let size = detected_name.resolve().bytes.len();
(detected_name, size)
} else {
(CharsetName::Wide, 92)
};
let entropy = calculate_entropy(middle_len, charset_size);
if entropy < ENTROPY_HARD_FAIL_BITS && !opts.force {
return Err(SecretError::InsufficientEntropy {
bits: entropy,
threshold: ENTROPY_HARD_FAIL_BITS,
});
}
if entropy < ENTROPY_WARN_BITS {
log::warn!(
"doppel: effective entropy {:.1} bits < {:.1} bits recommended; \
consider a longer secret or --force",
entropy,
ENTROPY_WARN_BITS
);
}
if !opts.restrict_charset && middle_bytes.iter().all(|b| b.is_ascii_alphanumeric()) {
log::warn!(
"doppel: secret variable bytes are all alphanumeric but restrict-charset is false; \
fake bytes will be drawn from the wide charset (92 chars) which may be structurally \
implausible for the target system. Use --restrict-charset to match the secret's charset."
);
}
let anchor_bytes = &secret[..anchor_len];
let anchor_charset = crate::segment::detect_charset_name(anchor_bytes);
let mut segments: Vec<Segment> = vec![
Segment::Opaque {
value: anchor_bytes.to_vec(),
charset: anchor_charset,
},
Segment::Variable {
charset,
min: middle_len,
max: middle_len, },
];
if tail_anchor_len > 0 {
let tail_bytes = &secret[middle_end..];
let tail_charset = crate::segment::detect_charset_name(tail_bytes);
segments.push(Segment::Opaque {
value: tail_bytes.to_vec(),
charset: tail_charset,
});
}
for seg in &segments {
if let Segment::Variable { min, max, .. } = seg {
debug_assert_eq!(
min, max,
"INV-31: instance pattern variable segment min must equal max"
);
}
}
let mut salt = [0u8; 32];
rng.fill_bytes(&mut salt);
let digest = crate::crypto::hmac_sha256(&salt, secret);
let arc_segments: Arc<[Segment]> = segments.into();
let pattern = Pattern {
identifier: String::new(),
segments: arc_segments.clone(),
salt,
digests: vec![digest],
};
let variable_lengths = vec![middle_len];
crate::fake::derive_fake_structural_segments(&salt, &arc_segments, &variable_lengths, secret)
.map_err(|_| SecretError::CollisionLimit { attempts: 1_000 })?;
Ok(pattern)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{SeedableRng, rngs::StdRng};
#[test]
fn test_register_with_rng_deterministic() {
let secret = b"deterministic-registration-test-secret-01";
let mut rng_a = StdRng::seed_from_u64(42);
let mut rng_b = StdRng::seed_from_u64(42);
let pat_a = register_with_rng(secret, &mut rng_a).unwrap();
let pat_b = register_with_rng(secret, &mut rng_b).unwrap();
assert_eq!(pat_a.salt, pat_b.salt, "same seed must produce same salt");
assert_eq!(
pat_a.digests, pat_b.digests,
"same seed must produce same digest"
);
}
#[test]
fn test_anchor_too_short_rejects_zero() {
let secret = b"my-long-enough-secret-value";
let opts = SecretOptions {
anchor_len: 0,
..SecretOptions::default()
};
let mut rng = StdRng::seed_from_u64(1);
assert!(matches!(
register_with_options_rng(secret, &opts, &mut rng),
Err(SecretError::AnchorTooShort { anchor_len: 0 })
));
}
#[test]
fn test_anchor_too_short_rejects_one() {
let secret = b"my-long-enough-secret-value";
let opts = SecretOptions {
anchor_len: 1,
..SecretOptions::default()
};
let mut rng = StdRng::seed_from_u64(2);
assert!(matches!(
register_with_options_rng(secret, &opts, &mut rng),
Err(SecretError::AnchorTooShort { anchor_len: 1 })
));
}
#[test]
fn test_anchor_len_two_succeeds() {
let secret = b"my-long-enough-secret-value";
let opts = SecretOptions {
anchor_len: 2,
..SecretOptions::default()
};
let mut rng = StdRng::seed_from_u64(3);
assert!(register_with_options_rng(secret, &opts, &mut rng).is_ok());
}
}