#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use crate::prelude::error::Result;
use crate::types::SecretVec;
pub fn secure_zeroize(data: &mut [u8]) {
use zeroize::Zeroize;
data.zeroize();
}
pub fn allocate_secure_buffer(size: usize) -> Result<SecretVec> {
if size == 0 {
return Err(crate::prelude::error::LatticeArcError::MemoryError(
"Cannot allocate zero-sized secure memory".to_string(),
));
}
const MAX_SECURE_ALLOCATION_SIZE: usize = 1024 * 1024;
if size > MAX_SECURE_ALLOCATION_SIZE {
return Err(crate::prelude::error::LatticeArcError::MemoryError(format!(
"Secure memory allocation size {} exceeds maximum allowed size {}",
size, MAX_SECURE_ALLOCATION_SIZE
)));
}
Ok(SecretVec::zero(size))
}
use rand::rngs::OsRng;
use rand_core::UnwrapErr;
pub type SecureRng = UnwrapErr<OsRng>;
use rand::{RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng;
use std::sync::{Mutex, OnceLock};
thread_local! {
static FALLBACK_RNG: Mutex<ChaCha20Rng> = Mutex::new(ChaCha20Rng::from_os_rng());
}
#[non_exhaustive]
pub enum RngHandle<'a> {
Global(&'a Mutex<SecureRng>),
#[cfg(not(feature = "fips"))]
ThreadLocal,
}
impl<'a> RngHandle<'a> {
pub fn secure() -> Result<RngHandle<'a>> {
match get_global_secure_rng() {
Ok(global) => Ok(RngHandle::Global(global)),
Err(_err) => {
#[cfg(feature = "fips")]
{
Err(crate::prelude::error::LatticeArcError::RandomError)
}
#[cfg(not(feature = "fips"))]
{
tracing::warn!("Global OsRng unavailable; falling back to thread-local RNG");
Ok(RngHandle::ThreadLocal)
}
}
}
}
pub fn fill_bytes(&self, dest: &mut [u8]) -> Result<()> {
match self {
RngHandle::Global(mutex) => match mutex.lock() {
Ok(mut rng) => {
rng.fill_bytes(dest);
Ok(())
}
Err(_) => fallback_fill_bytes(dest),
},
#[cfg(not(feature = "fips"))]
RngHandle::ThreadLocal => fallback_fill_bytes(dest),
}
}
pub fn next_u64(&self) -> Result<u64> {
match self {
RngHandle::Global(mutex) => match mutex.lock() {
Ok(mut rng) => Ok(rng.next_u64()),
Err(_) => fallback_next_u64(),
},
#[cfg(not(feature = "fips"))]
RngHandle::ThreadLocal => fallback_next_u64(),
}
}
pub fn next_u32(&self) -> Result<u32> {
match self {
RngHandle::Global(mutex) => match mutex.lock() {
Ok(mut rng) => Ok(rng.next_u32()),
Err(_) => fallback_next_u32(),
},
#[cfg(not(feature = "fips"))]
RngHandle::ThreadLocal => fallback_next_u32(),
}
}
}
#[inline]
fn fallback_fill_bytes(dest: &mut [u8]) -> Result<()> {
#[cfg(feature = "fips")]
{
let _ = dest;
Err(crate::prelude::error::LatticeArcError::RandomError)
}
#[cfg(not(feature = "fips"))]
FALLBACK_RNG.with(|rng| match rng.lock() {
Ok(mut rng) => {
rng.fill_bytes(dest);
Ok(())
}
Err(_) => Err(crate::prelude::error::LatticeArcError::RandomError),
})
}
#[inline]
fn fallback_next_u64() -> Result<u64> {
#[cfg(feature = "fips")]
{
Err(crate::prelude::error::LatticeArcError::RandomError)
}
#[cfg(not(feature = "fips"))]
FALLBACK_RNG.with(|rng| match rng.lock() {
Ok(mut rng) => Ok(rng.next_u64()),
Err(_) => Err(crate::prelude::error::LatticeArcError::RandomError),
})
}
#[inline]
fn fallback_next_u32() -> Result<u32> {
#[cfg(feature = "fips")]
{
Err(crate::prelude::error::LatticeArcError::RandomError)
}
#[cfg(not(feature = "fips"))]
FALLBACK_RNG.with(|rng| match rng.lock() {
Ok(mut rng) => Ok(rng.next_u32()),
Err(_) => Err(crate::prelude::error::LatticeArcError::RandomError),
})
}
static GLOBAL_SECURE_RNG: OnceLock<Mutex<SecureRng>> = OnceLock::new();
pub fn get_global_secure_rng() -> Result<&'static Mutex<SecureRng>> {
Ok(GLOBAL_SECURE_RNG.get_or_init(|| Mutex::new(UnwrapErr(OsRng))))
}
pub fn initialize_global_secure_rng() -> Result<()> {
let _ = get_global_secure_rng()?;
Ok(())
}
pub fn generate_secure_random_bytes(len: usize) -> Result<zeroize::Zeroizing<Vec<u8>>> {
if len == 0 {
return Err(crate::prelude::LatticeArcError::InvalidParameter(
"generate_secure_random_bytes: zero-length requests are rejected".to_string(),
));
}
const MAX_RANDOM_BYTES: usize = 1024 * 1024;
if len > MAX_RANDOM_BYTES {
return Err(crate::prelude::LatticeArcError::InvalidParameter(format!(
"generate_secure_random_bytes: requested {len} bytes exceeds {MAX_RANDOM_BYTES} cap"
)));
}
let mut bytes = zeroize::Zeroizing::new(vec![0u8; len]);
RngHandle::secure()?.fill_bytes(&mut bytes)?;
Ok(bytes)
}
pub fn generate_secure_random_u64() -> Result<u64> {
RngHandle::secure()?.next_u64()
}
pub fn generate_secure_random_u32() -> Result<u32> {
RngHandle::secure()?.next_u32()
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
reason = "test/bench code: unwrap is acceptable when inputs are statically known"
)]
mod tests {
use super::*;
#[test]
fn test_secure_zeroize_clears_bytes_succeeds() {
let mut data = vec![0xFF; 32];
secure_zeroize(&mut data);
assert!(data.iter().all(|&b| b == 0));
}
#[test]
fn test_allocate_secure_buffer_basic_succeeds() {
let mem = allocate_secure_buffer(32).unwrap();
assert_eq!(mem.len(), 32);
assert!(mem.expose_secret().iter().all(|&b| b == 0));
}
#[test]
fn test_allocate_secure_buffer_zero_size_fails() {
let result = allocate_secure_buffer(0);
assert!(result.is_err());
}
#[test]
fn test_allocate_secure_buffer_too_large_fails() {
let result = allocate_secure_buffer(2 * 1024 * 1024);
assert!(result.is_err());
}
#[test]
fn test_rng_handle_secure_is_secure_succeeds() {
let handle = RngHandle::secure().unwrap();
let mut buf = [0u8; 32];
handle.fill_bytes(&mut buf).unwrap();
assert!(buf.iter().any(|&b| b != 0));
}
#[test]
fn test_rng_handle_fill_bytes_global_succeeds() {
let handle = RngHandle::secure().unwrap();
let mut buf1 = [0u8; 16];
let mut buf2 = [0u8; 16];
handle.fill_bytes(&mut buf1).unwrap();
handle.fill_bytes(&mut buf2).unwrap();
assert_ne!(buf1, buf2);
}
#[test]
fn test_rng_handle_next_u64_succeeds() {
let handle = RngHandle::secure().unwrap();
let v1 = handle.next_u64().unwrap();
let v2 = handle.next_u64().unwrap();
assert_ne!(v1, v2);
}
#[test]
fn test_rng_handle_next_u32_succeeds() {
let handle = RngHandle::secure().unwrap();
let v = handle.next_u32().unwrap();
let _ = v;
}
#[cfg(not(feature = "fips"))]
#[test]
fn test_rng_handle_thread_local_fill_succeeds() {
let handle = RngHandle::ThreadLocal;
let mut buf = [0u8; 32];
handle.fill_bytes(&mut buf).unwrap();
assert!(buf.iter().any(|&b| b != 0));
}
#[cfg(not(feature = "fips"))]
#[test]
fn test_rng_handle_thread_local_next_u64_succeeds() {
let handle = RngHandle::ThreadLocal;
let v = handle.next_u64().unwrap();
let _ = v;
}
#[cfg(not(feature = "fips"))]
#[test]
fn test_rng_handle_thread_local_next_u32_succeeds() {
let handle = RngHandle::ThreadLocal;
let v = handle.next_u32().unwrap();
let _ = v;
}
#[test]
fn test_get_global_secure_rng_succeeds() {
let rng = get_global_secure_rng().unwrap();
let _ = rng; }
#[test]
fn test_initialize_global_secure_rng_succeeds() {
assert!(initialize_global_secure_rng().is_ok());
}
#[test]
fn test_generate_secure_random_bytes_is_secure_succeeds() {
let bytes = generate_secure_random_bytes(32).unwrap();
assert_eq!(bytes.len(), 32);
assert!(bytes.iter().any(|&b| b != 0));
}
#[test]
fn test_generate_secure_random_bytes_zero_len_fails() {
let err = generate_secure_random_bytes(0).unwrap_err();
assert!(format!("{err}").contains("zero-length"), "expected zero-length rejection: {err}");
}
#[test]
fn test_generate_secure_random_u64_is_secure_succeeds() {
let v = generate_secure_random_u64().unwrap();
let _ = v;
}
#[test]
fn test_generate_secure_random_u32_is_secure_succeeds() {
let v = generate_secure_random_u32().unwrap();
let _ = v;
}
#[test]
fn test_allocate_secure_buffer_boundary_succeeds() {
let mem = allocate_secure_buffer(1024 * 1024).unwrap();
assert_eq!(mem.len(), 1024 * 1024);
let result = allocate_secure_buffer(1024 * 1024 + 1);
assert!(result.is_err());
}
#[test]
fn test_generate_secure_random_bytes_various_lengths_is_secure_has_correct_size() {
for len in [1, 16, 32, 64, 128, 256] {
let bytes = generate_secure_random_bytes(len).unwrap();
assert_eq!(bytes.len(), len);
}
}
}