use crate::seed::Seed;
use zeroize::Zeroizing;
pub fn raw(seed: &Seed, realm: &str, path: &str, length: usize) -> Zeroizing<Vec<u8>> {
let key_type = format!("raw/{path}");
seed.derive(realm, &key_type, length)
}
pub fn mnemonic(
seed: &Seed,
realm: &str,
words: usize,
) -> Result<Zeroizing<String>, Box<dyn std::error::Error>> {
let entropy_bytes = match words {
12 => 16, 15 => 20, 18 => 24, 21 => 28, 24 => 32, _ => return Err("word count must be 12, 15, 18, 21, or 24".into()),
};
let entropy = seed.derive(realm, "mnemonic", entropy_bytes);
let mnemonic = bip39::Mnemonic::from_entropy(&entropy)?;
Ok(Zeroizing::new(mnemonic.to_string()))
}
pub fn integer(
seed: &Seed,
realm: &str,
path: &str,
min: i64,
max: i64,
) -> Result<i64, Box<dyn std::error::Error>> {
if min > max {
return Err("min must be less than or equal to max".into());
}
let range = (max as i128 - min as i128 + 1) as u128;
let key_type = format!("int/{path}");
let raw = seed.derive(realm, &key_type, 256);
let chunk_size = 8;
let limit = u64::MAX as u128 - (u64::MAX as u128 % range);
for chunk in raw.chunks(chunk_size) {
if chunk.len() < chunk_size {
break;
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(chunk);
let val = u64::from_be_bytes(bytes) as u128;
if val < limit {
return Ok((min as i128 + (val % range) as i128) as i64);
}
}
Err("failed to derive uniform integer (astronomically unlikely)".into())
}
pub fn uuid(seed: &Seed, realm: &str, path: &str) -> String {
let key_type = format!("uuid/{path}");
let mut bytes = *seed.derive_32(realm, &key_type);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
let b = &bytes[0..16];
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
b[0], b[1], b[2], b[3],
b[4], b[5],
b[6], b[7],
b[8], b[9],
b[10], b[11], b[12], b[13], b[14], b[15]
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mnemonic_deterministic() {
let seed = Seed::from_passphrase("test").unwrap();
let m1 = mnemonic(&seed, "realm", 24).unwrap();
let m2 = mnemonic(&seed, "realm", 24).unwrap();
assert_eq!(m1.as_str(), m2.as_str());
}
#[test]
fn mnemonic_valid_words() {
let seed = Seed::from_passphrase("test").unwrap();
for words in [12, 15, 18, 21, 24] {
let m = mnemonic(&seed, "realm", words).unwrap();
assert_eq!(m.split_whitespace().count(), words);
}
}
#[test]
fn integer_deterministic() {
let seed = Seed::from_passphrase("test").unwrap();
let i1 = integer(&seed, "realm", "count", 0, 100).unwrap();
let i2 = integer(&seed, "realm", "count", 0, 100).unwrap();
assert_eq!(i1, i2);
}
#[test]
fn integer_in_range() {
let seed = Seed::from_passphrase("test").unwrap();
for i in 0..100 {
let val = integer(&seed, "realm", &format!("count{i}"), 10, 20).unwrap();
assert!(val >= 10 && val <= 20);
}
}
#[test]
fn uuid_format() {
let seed = Seed::from_passphrase("test").unwrap();
let u = uuid(&seed, "realm", "id");
assert_eq!(u.len(), 36);
assert_eq!(u.chars().nth(14), Some('4')); let variant = u.chars().nth(19).unwrap();
assert!(variant == '8' || variant == '9' || variant == 'a' || variant == 'b');
let parts: Vec<&str> = u.split('-').collect();
assert_eq!(parts.len(), 5);
assert_eq!(parts[0].len(), 8);
assert_eq!(parts[1].len(), 4);
assert_eq!(parts[2].len(), 4);
assert_eq!(parts[3].len(), 4);
assert_eq!(parts[4].len(), 12);
}
}