mb_rand 0.2.0

Safe Rust bindings to OpenBSD's arc4random functions
Documentation
//
// Copyright (c) 2025 murilo ijanc' <mbsd@m0x.ru>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
//! # mb_rand
//!
//! A Rust library crate providing safe bindings to OpenBSD's arc4random
//! functions.
//!
//! This crate provides two main functions:
//!
//! - `fill_bytes` - Fills a byte array with random data
//! - `random_string` - Generates a random string of a specified length from a
//!   character set

use std::ffi::c_void;

/// FFI bindings to the OpenBSD arc4random functions
mod ffi {
    use std::ffi::c_void;

    unsafe extern "C" {
        /// Fill a buffer with random bytes.
        ///
        /// This function directly calls OpenBSD's arc4random_buf function.
        ///
        /// # Safety
        ///
        /// This function is unsafe because it interacts with C code and requires
        /// that the buffer pointer is valid for the specified length.
        pub fn arc4random_buf(buf: *mut c_void, nbytes: usize);

        /// Get a random 32-bit unsigned integer.
        ///
        /// This function directly calls OpenBSD's arc4random function.
        #[allow(dead_code)]
        pub fn arc4random() -> u32;

        /// Get a random 32-bit unsigned integer within a range.
        ///
        /// This function directly calls OpenBSD's arc4random_uniform function.
        pub fn arc4random_uniform(upper_bound: u32) -> u32;
    }
}

/// Fill a byte array with random data.
///
/// This function uses arc4random_buf to fill the provided mutable byte slice with
/// cryptographically secure random data.
///
/// # Examples
///
/// ```
/// use mb_rand::fill_bytes;
///
/// let mut buffer = [0u8; 16];
/// fill_bytes(&mut buffer);
/// // buffer now contains random data
/// ```
pub fn fill_bytes(buffer: &mut [u8]) {
    let buf_ptr = buffer.as_mut_ptr() as *mut c_void;
    let buf_len = buffer.len();

    // SAFETY:
    // - The buffer pointer is valid for the specified length
    // - The C function does not retain the pointer after it returns
    // - The buffer memory is properly aligned as required by the FFI function
    unsafe {
        ffi::arc4random_buf(buf_ptr, buf_len);
    }
}

/// Generate random data of specified length and return it as a `Vec<u8>`.
///
/// This function allocates a new vector of the specified length and fills it
/// with random data using arc4random_buf.
///
/// # Examples
///
/// ```
/// use mb_rand::random_bytes;
///
/// let random_data = random_bytes(32);
/// assert_eq!(random_data.len(), 32);
/// ```
pub fn random_bytes(length: usize) -> Vec<u8> {
    let mut buffer = vec![0u8; length];
    fill_bytes(&mut buffer);
    buffer
}

/// Generate a random string of specified length from a set of characters.
///
/// This function uses arc4random_uniform to select random characters from
/// the provided character set to create a string of the specified length.
///
/// # Examples
///
/// ```
/// use mb_rand::random_string;
///
/// // Generate a random alphanumeric string of length 10
/// let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
/// let random_str = random_string(charset, 10);
/// assert_eq!(random_str.len(), 10);
/// ```
pub fn random_string(charset: &str, length: usize) -> String {
    if charset.is_empty() {
        return String::new();
    }

    let charset_bytes = charset.as_bytes();
    let charset_len = charset_bytes.len() as u32;

    let mut result = String::with_capacity(length);

    for _ in 0..length {
        // SAFETY:
        // - The arc4random_uniform function is safe to call
        // - We check that charset is not empty before calling
        let index = unsafe { ffi::arc4random_uniform(charset_len) } as usize;
        result.push(charset_bytes[index] as char);
    }

    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_fill_bytes() {
        let mut buffer = [0u8; 16];

        // Before filling, all bytes should be zero
        assert!(buffer.iter().all(|&b| b == 0));

        fill_bytes(&mut buffer);

        // After filling, it's statistically impossible for all bytes to still be zero
        // This is not a perfect test, but the probability of failure is negligible
        assert!(!buffer.iter().all(|&b| b == 0));
    }

    #[test]
    fn test_random_bytes() {
        let length = 32;
        let random_data = random_bytes(length);

        assert_eq!(random_data.len(), length);

        // Generate another one and verify they're different (statistically, they should be)
        let random_data2 = random_bytes(length);
        assert_ne!(random_data, random_data2);
    }

    #[test]
    fn test_random_string() {
        let charset = "abcdefghijklmnopqrstuvwxyz";
        let length = 20;

        let random_str = random_string(charset, length);

        assert_eq!(random_str.len(), length);
        assert!(random_str.chars().all(|c| charset.contains(c)));

        // Generate another one and verify they're different
        let random_str2 = random_string(charset, length);
        assert_ne!(random_str, random_str2);
    }

    #[test]
    fn test_random_string_empty_charset() {
        let empty_charset = "";
        let random_str = random_string(empty_charset, 10);
        assert_eq!(random_str, "");
    }

    #[test]
    fn test_random_string_zero_length() {
        let charset = "abcdefghijklmnopqrstuvwxyz";
        let random_str = random_string(charset, 0);
        assert_eq!(random_str, "");
    }
}