use miniscript::bitcoin::hashes::{sha256, Hash, HashEngine}; use std::{ collections::hash_map, error, fmt, hash::{BuildHasher, Hasher}, time::{SystemTime, UNIX_EPOCH}, }; #[derive(Debug)] pub enum RandomnessError { Hardware(String), Os(String), ContextualInfo(String), } impl fmt::Display for RandomnessError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::Hardware(s) => write!(f, "Error when getting randomness from hardware: {}", s), Self::Os(s) => write!(f, "Error when getting randomness from the OS: {}", s), Self::ContextualInfo(s) => write!(f, "Error when getting contextual info: {}", s), } } } impl error::Error for RandomnessError {} // Get some entrop from RDRAND when available. #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn cpu_randomness() -> Result, RandomnessError> { if let Ok(mut rand_gen) = rdrand::RdRand::new() { let mut buf = [0; 32]; rand_gen .try_fill_bytes(&mut buf) .map_err(|e| RandomnessError::Hardware(e.to_string()))?; assert_ne!(buf, [0; 32]); Ok(Some(buf)) } else { // Not available. Ok(None) } } // OS-generated randomness. See https://docs.rs/getrandom/latest/getrandom/#supported-targets // (basically this calls `getrandom()` or polls `/dev/urandom` on Linux, `BCryptGenRandom` on // Windows, and `getentropy()` / `/dev/random` on Mac. fn system_randomness() -> Result<[u8; 32], RandomnessError> { let mut buf = [0; 32]; getrandom::getrandom(&mut buf).map_err(|e| RandomnessError::Os(e.to_string()))?; assert_ne!(buf, [0; 32]); Ok(buf) } // Some more contextual data to try to get at least a slight bit of additional entropy. fn additional_data() -> Result<[u8; 32], RandomnessError> { let mut engine = sha256::HashEngine::default(); let timestamp: u16 = (SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|e| RandomnessError::ContextualInfo(e.to_string()))? .as_secs() % u16::MAX as u64) as u16; engine.input(×tamp.to_be_bytes()); let hasher_number = hash_map::RandomState::new().build_hasher().finish(); engine.input(&hasher_number.to_be_bytes()); let pid = std::process::id(); engine.input(&pid.to_be_bytes()); // TODO: get some more contextual information Ok(*sha256::Hash::from_engine(engine).as_inner()) } /// Get 32 random bytes. This is mainly based on OS-provided randomness (`getrandom` or /// `/dev/urandom` on Linux, `getentropy` / `/dev/random` on MacOS, and `BCryptGenRandom` on /// Windows. In addition some randomness may be taken directly from the CPU if it is /// available, and some contextual information are added to the mix as well. pub fn random_bytes() -> Result<[u8; 32], RandomnessError> { let mut engine = sha256::HashEngine::default(); #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] if let Some(bytes) = cpu_randomness()? { engine.input(&bytes); } engine.input(&system_randomness()?); engine.input(&additional_data()?); // TODO: add more sources of randomness Ok(*sha256::Hash::from_engine(engine).as_inner()) } #[cfg(test)] mod tests { use super::*; use std::collections::HashSet; // This does not test the quality of the randomness but at least sanity checks it's // not obviously broken. #[test] fn randomness_sanity_check() { let mut set = HashSet::with_capacity(100); for _ in 0..100 { let rand = random_bytes().unwrap(); assert!(!set.contains(&rand)); set.insert(rand); } } // I used this to perform statistical tests of the random generation function using ENT // (https://fourmilab.ch/random/). //#[test] //fn write_to_file() { //use std::io::Write; //let mut f = std::fs::OpenOptions::new() //.write(true) //.create(true) //.append(true) //.open("random_out") //.unwrap(); //for _ in 0..10_000_000 { //f.write(&random_bytes().unwrap()).unwrap(); //} //} }