//! Easy access to operating-system randomness via `/dev/urandom`. //! //! The crate exposes [`OsRandom`], a small wrapper around an open handle to //! `/dev/urandom` with helpers for reading primitive integers, sampling //! uniformly without modulo bias, and generating random ASCII strings from a //! caller-supplied alphabet. Common alphabets live in [`charset`]. //! //! # Project layout //! //! - [`OsRandom`] owns an open `/dev/urandom` handle. //! - Constructors open or duplicate that handle. //! - Primitive readers turn raw bytes into integer values. //! - Convenience byte readers fill caller-provided buffers. //! - Sampling helpers build unbiased higher-level choices from those primitive //! readers. //! - Range helpers sample integer spans. //! - Slice helpers choose caller-owned values by reference. //! - [`charset`] contains reusable ASCII alphabets for string generation. //! - String helpers compose those alphabets into common token formats. use std::{ fs::File, io::{self, Read}, mem::size_of, ops::{Bound, RangeBounds}, }; use ::paste::paste; const DEV_URANDOM: &str = "/dev/urandom"; /// Common character sets for use with [`OsRandom::pick`] and /// [`OsRandom::string_from`]. pub mod charset { const fn concat(parts: &[&[u8]]) -> [u8; N] { let mut out = [0u8; N]; let mut i = 0; let mut p = 0; while p < parts.len() { let part = parts[p]; let mut j = 0; while j < part.len() { out[i] = part[j]; i += 1; j += 1; } p += 1; } out } /// ASCII decimal digits `0`–`9`. pub const DIGITS: &[u8] = b"0123456789"; /// ASCII lowercase letters `a`–`z`. pub const LOWERCASE: &[u8] = b"abcdefghijklmnopqrstuvwxyz"; const UPPERCASE_ARR: [u8; 26] = { let mut out = [0u8; 26]; let mut i = 0; while i < 26 { out[i] = LOWERCASE[i].to_ascii_uppercase(); i += 1; } out }; /// ASCII uppercase letters `A`–`Z`. pub const UPPERCASE: &[u8] = &UPPERCASE_ARR; const ALPHABETIC_ARR: [u8; 52] = concat(&[LOWERCASE, UPPERCASE]); /// ASCII letters: [`LOWERCASE`] followed by [`UPPERCASE`]. pub const ALPHABETIC: &[u8] = &ALPHABETIC_ARR; const ALPHANUMERIC_ARR: [u8; 62] = concat(&[LOWERCASE, UPPERCASE, DIGITS]); /// ASCII letters and digits: [`LOWERCASE`], [`UPPERCASE`], then [`DIGITS`]. pub const ALPHANUMERIC: &[u8] = &ALPHANUMERIC_ARR; const HEX_LOWER_ARR: [u8; 16] = concat(&[DIGITS, LOWERCASE.split_at(6).0]); /// Lowercase hexadecimal digits `0`–`9`, `a`–`f`. pub const HEX_LOWER: &[u8] = &HEX_LOWER_ARR; const HEX_UPPER_ARR: [u8; 16] = concat(&[DIGITS, UPPERCASE.split_at(6).0]); /// Uppercase hexadecimal digits `0`–`9`, `A`–`F`. pub const HEX_UPPER: &[u8] = &HEX_UPPER_ARR; } /// Handle to the OS randomness source (`/dev/urandom`). /// /// Construct with [`OsRandom::try_new`], then call the typed `get_*` readers, /// the uniform-sampling helpers, or use the [`Read`] impl to fill an arbitrary /// buffer. pub struct OsRandom { devurandom: File, } macro_rules! os_random_get_integer_impls { ($($t:ty),*) => { $( paste! { /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. pub fn [](&mut self) -> std::io::Result<$t> { let mut buf = [0u8; size_of::<$t>()]; self.devurandom.read_exact(&mut buf)?; Ok($t::from_ne_bytes(buf)) } } )* }; } macro_rules! os_random_unsigned_range_impls { ($(($t:ty, $method:ident, $range_method:ident, $below_method:ident, $get_method:ident)),*) => { $( /// Returns a uniformly distributed integer in `0..n`. /// /// # Panics /// Panics if `n == 0`. /// /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; #[doc = concat!("let value = rng.", stringify!($method), "(10)?;")] /// assert!(value < 10); /// # Ok(()) /// # } /// ``` pub fn $method(&mut self, n: $t) -> io::Result<$t> { assert!(n > 0, "n must be greater than zero"); self.$below_method(n) } /// Returns a uniformly distributed integer from `range`. /// /// The range may use any [`RangeBounds`] form, including `a..b`, /// `a..=b`, `..b`, `..=b`, `a..`, and `..`. /// /// # Panics /// Panics if the range is empty or if an excluded bound overflows /// the integer type. /// /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; #[doc = concat!("let value = rng.", stringify!($range_method), "(4..=9)?;")] /// assert!((4..=9).contains(&value)); /// # Ok(()) /// # } /// ``` pub fn $range_method(&mut self, range: R) -> io::Result<$t> where R: RangeBounds<$t>, { let start = match range.start_bound() { Bound::Included(&start) => start, Bound::Excluded(&start) => start .checked_add(1) .expect("excluded range start must not overflow"), Bound::Unbounded => <$t>::MIN, }; let end = match range.end_bound() { Bound::Included(&end) => end, Bound::Excluded(&end) => end .checked_sub(1) .expect("excluded range end must not overflow"), Bound::Unbounded => <$t>::MAX, }; assert!(start <= end, "range must not be empty"); let span = end.wrapping_sub(start).wrapping_add(1); if span == 0 { return self.$get_method(); } Ok(start.wrapping_add(self.$below_method(span)?)) } fn $below_method(&mut self, n: $t) -> io::Result<$t> { let threshold = (0 as $t).wrapping_sub(n) % n; loop { let value = self.$get_method()?; if value >= threshold { return Ok(value % n); } } } )* }; } macro_rules! os_random_signed_range_impls { ($(($t:ty, $u:ty, $method:ident, $range_method:ident, $below_method:ident, $get_method:ident)),*) => { $( /// Returns a uniformly distributed integer in `0..n`. /// /// # Panics /// Panics if `n <= 0`. /// /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; #[doc = concat!("let value = rng.", stringify!($method), "(10)?;")] /// assert!((0..10).contains(&value)); /// # Ok(()) /// # } /// ``` pub fn $method(&mut self, n: $t) -> io::Result<$t> { assert!(n > 0, "n must be greater than zero"); self.$range_method(0..n) } /// Returns a uniformly distributed integer from `range`. /// /// The range may use any [`RangeBounds`] form, including `a..b`, /// `a..=b`, `..b`, `..=b`, `a..`, and `..`. /// /// # Panics /// Panics if the range is empty or if an excluded bound overflows /// the integer type. /// /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; #[doc = concat!("let value = rng.", stringify!($range_method), "(-4..=4)?;")] /// assert!((-4..=4).contains(&value)); /// # Ok(()) /// # } /// ``` pub fn $range_method(&mut self, range: R) -> io::Result<$t> where R: RangeBounds<$t>, { const SIGN_BIT: $u = (<$u>::MAX >> 1) + 1; let start = match range.start_bound() { Bound::Included(&start) => start, Bound::Excluded(&start) => start .checked_add(1) .expect("excluded range start must not overflow"), Bound::Unbounded => <$t>::MIN, }; let end = match range.end_bound() { Bound::Included(&end) => end, Bound::Excluded(&end) => end .checked_sub(1) .expect("excluded range end must not overflow"), Bound::Unbounded => <$t>::MAX, }; assert!(start <= end, "range must not be empty"); let start = start.cast_unsigned() ^ SIGN_BIT; let end = end.cast_unsigned() ^ SIGN_BIT; let span = end.wrapping_sub(start).wrapping_add(1); if span == 0 { return self.$get_method(); } let value = start.wrapping_add(self.$below_method(span)?) ^ SIGN_BIT; Ok(value.cast_signed()) } )* }; } /// Constructor and primitive integer readers. /// /// Each `get_` method reads exactly `size_of::()` bytes from /// `/dev/urandom` and interprets them in native-endian order, so every bit /// pattern is equally likely. impl OsRandom { /// # Errors /// Returns any I/O error produced while opening `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; /// let value = rng.get_u64()?; /// # let _ = value; /// # Ok(()) /// # } /// ``` pub fn try_new() -> Result { let devurandom = File::open(DEV_URANDOM)?; Ok(Self { devurandom }) } /// Fills `buf` with bytes from `/dev/urandom`. /// /// This is equivalent to calling [`Read::read_exact`] on `OsRandom`, but it /// keeps the common "fill this byte slice" use case available without /// importing the [`Read`] trait. /// /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; /// let mut bytes = [0u8; 32]; /// rng.fill_bytes(&mut bytes)?; /// # Ok(()) /// # } /// ``` pub fn fill_bytes(&mut self, buf: &mut [u8]) -> io::Result<()> { self.devurandom.read_exact(buf) } /// Duplicates this handle to `/dev/urandom`. /// /// The cloned value has its own [`File`] handle, so separate call sites can /// own independent `OsRandom` values while reading from the same /// operating-system randomness source. /// /// # Errors /// Returns any I/O error produced while duplicating the underlying file /// handle. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut first = ez_urandom::OsRandom::try_new()?; /// let mut second = first.try_clone()?; /// /// let a = first.get_u64()?; /// let b = second.get_u64()?; /// # let _ = (a, b); /// # Ok(()) /// # } /// ``` pub fn try_clone(&self) -> io::Result { Ok(Self { devurandom: self.devurandom.try_clone()?, }) } os_random_get_integer_impls!( u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize ); } /// Opens `/dev/urandom` and panics if the operating-system source is /// unavailable. /// /// Prefer [`OsRandom::try_new`] when the caller should decide how to handle an /// I/O failure. /// /// # Examples /// /// ``` /// let mut rng = ez_urandom::OsRandom::default(); /// let mut bytes = [0u8; 16]; /// rng.fill_bytes(&mut bytes).expect("/dev/urandom read failed"); /// ``` impl Default for OsRandom { fn default() -> Self { Self::try_new().expect("/dev/urandom should be available") } } /// Duplicates the underlying `/dev/urandom` handle. /// /// Prefer [`OsRandom::try_clone`] when the caller should decide how to handle an /// I/O failure while duplicating the file handle. /// /// # Examples /// /// ``` /// let rng = ez_urandom::OsRandom::default(); /// let mut cloned = rng.clone(); /// let value = cloned.get_u32().expect("/dev/urandom read failed"); /// # let _ = value; /// ``` impl Clone for OsRandom { fn clone(&self) -> Self { self.try_clone() .expect("/dev/urandom handle should be cloneable") } } /// Uniform sampling helpers. /// /// Range methods use rejection sampling on top of the matching primitive /// integer reader, so bounded results are exactly uniform — no modulo bias. impl OsRandom { os_random_unsigned_range_impls!( (u8, gen_range_u8, gen_range_u8_in, gen_below_u8, get_u8), (u16, gen_range_u16, gen_range_u16_in, gen_below_u16, get_u16), (u32, gen_range_u32, gen_range_u32_in, gen_below_u32, get_u32), (u64, gen_range_u64, gen_range_u64_in, gen_below_u64, get_u64), ( u128, gen_range_u128, gen_range_u128_in, gen_below_u128, get_u128 ), ( usize, gen_range_usize, gen_range_usize_in, gen_below_usize, get_usize ) ); os_random_signed_range_impls!( (i8, u8, gen_range_i8, gen_range_i8_in, gen_below_u8, get_i8), ( i16, u16, gen_range_i16, gen_range_i16_in, gen_below_u16, get_i16 ), ( i32, u32, gen_range_i32, gen_range_i32_in, gen_below_u32, get_i32 ), ( i64, u64, gen_range_i64, gen_range_i64_in, gen_below_u64, get_i64 ), ( i128, u128, gen_range_i128, gen_range_i128_in, gen_below_u128, get_i128 ), ( isize, usize, gen_range_isize, gen_range_isize_in, gen_below_usize, get_isize ) ); /// Returns a uniformly chosen byte from `set`. /// /// # Panics /// Panics if `set` is empty. /// /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; /// let digit = rng.pick(ez_urandom::charset::DIGITS)?; /// assert!(digit.is_ascii_digit()); /// # Ok(()) /// # } /// ``` pub fn pick(&mut self, set: &[u8]) -> io::Result { Ok(*self.choose(set)?) } /// Returns a uniformly chosen item from `items`. /// /// The returned reference points into the caller-provided slice; no item is /// cloned or moved. /// /// # Panics /// Panics if `items` is empty. /// /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; /// let colors = ["red", "green", "blue"]; /// let color = rng.choose(&colors)?; /// assert!(colors.contains(color)); /// # Ok(()) /// # } /// ``` pub fn choose<'a, T>(&mut self, items: &'a [T]) -> io::Result<&'a T> { assert!(!items.is_empty(), "items must not be empty"); let i = self.gen_range_usize(items.len())?; Ok(&items[i]) } /// Returns a `String` of `len` characters drawn uniformly from `set`. /// /// # Panics /// Panics if `set` is empty, longer than `u32::MAX`, or contains /// non-ASCII bytes. /// /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; /// let token = rng.string_from(ez_urandom::charset::ALPHANUMERIC, 24)?; /// assert_eq!(token.len(), 24); /// assert!(token.is_ascii()); /// # Ok(()) /// # } /// ``` pub fn string_from(&mut self, set: &[u8], len: usize) -> io::Result { assert!(set.is_ascii(), "set must contain only ASCII bytes"); let mut buf = vec![0u8; len]; for byte in &mut buf { *byte = self.pick(set)?; } Ok(String::from_utf8(buf).expect("ASCII bytes are valid UTF-8")) } /// Returns an alphanumeric ASCII token of `len` characters. /// /// Characters are drawn uniformly from [`charset::ALPHANUMERIC`]. /// /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; /// let token = rng.token(32)?; /// assert_eq!(token.len(), 32); /// assert!(token.bytes().all(|byte| byte.is_ascii_alphanumeric())); /// # Ok(()) /// # } /// ``` pub fn token(&mut self, len: usize) -> io::Result { self.string_from(charset::ALPHANUMERIC, len) } /// Returns a lowercase hexadecimal string of `len` characters. /// /// Characters are drawn uniformly from [`charset::HEX_LOWER`]. The `len` /// argument is the number of hex characters, not the number of source /// bytes. /// /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; /// let hex = rng.hex(16)?; /// assert_eq!(hex.len(), 16); /// assert!(hex.bytes().all(|byte| byte.is_ascii_hexdigit())); /// # Ok(()) /// # } /// ``` pub fn hex(&mut self, len: usize) -> io::Result { self.string_from(charset::HEX_LOWER, len) } /// Returns a decimal digit string of `len` characters. /// /// This is useful for PINs, short human-entered codes, and other cases /// where only ASCII digits are accepted. /// /// # Errors /// Returns any I/O error produced while reading from `/dev/urandom`. /// /// # Examples /// /// ``` /// # fn main() -> std::io::Result<()> { /// let mut rng = ez_urandom::OsRandom::try_new()?; /// let pin = rng.pin(6)?; /// assert_eq!(pin.len(), 6); /// assert!(pin.bytes().all(|byte| byte.is_ascii_digit())); /// # Ok(()) /// # } /// ``` pub fn pin(&mut self, len: usize) -> io::Result { self.string_from(charset::DIGITS, len) } } /// Forwards reads directly to the underlying `/dev/urandom` handle so an /// `OsRandom` can be used anywhere an [`io::Read`] source is expected. impl Read for OsRandom { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.devurandom.read(buf) } }