kernal-api 0.1.14

Async OS HAL, profiling, symbolization, and allocator instrumentation
//! Secure OS entropy with bounded admission, output and caller wait.
//!
//! There is no pseudorandom fallback. Applications own token length, encoding,
//! storage and authorization policy. Returned bytes are ordinary caller-owned
//! memory, not a protected or automatically zeroized secret container.

use std::{
    fmt,
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
    time::Duration,
};

/// Maximum allocation/output of one entropy request (64 KiB).
pub const MAX_RANDOM_BYTES: usize = 64 * 1024;

/// Semantic failures. Diagnostics never include generated bytes or backend types.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RandomError {
    InvalidLimits,
    TooLarge,
    AtCapacity,
    RuntimeUnavailable,
    Unavailable,
    TimedOut,
    Cancelled,
    WorkerFailed,
}

impl fmt::Display for RandomError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Self::InvalidLimits => "invalid secure entropy limits",
            Self::TooLarge => "secure entropy request exceeds byte limit",
            Self::AtCapacity => "secure entropy capacity exhausted",
            Self::RuntimeUnavailable => "secure entropy requires an active async runtime",
            Self::Unavailable => "operating-system secure entropy unavailable",
            Self::TimedOut => "secure entropy deadline expired",
            Self::Cancelled => "secure entropy request cancelled",
            Self::WorkerFailed => "secure entropy worker failed",
        })
    }
}
impl std::error::Error for RandomError {}

/// One shared native-work budget. Clones share capacity; callers should reuse
/// an instance rather than create a pool per request. Admission fails immediately
/// at capacity, with no additional waiter queue or output allocation.
#[derive(Clone, Debug)]
pub struct SecureRandom {
    permits: Arc<tokio::sync::Semaphore>,
    timeout: Duration,
}

impl SecureRandom {
    /// Configure 1..=64 outstanding requests and a positive timeout <= one day.
    ///
    /// # Errors
    /// Returns `InvalidLimits` for values outside those bounds.
    pub fn new(max_operations: usize, timeout: Duration) -> Result<Self, RandomError> {
        if !(1..=64).contains(&max_operations)
            || timeout.is_zero()
            || timeout > Duration::from_secs(86400)
        {
            return Err(RandomError::InvalidLimits);
        }
        Ok(Self {
            permits: Arc::new(tokio::sync::Semaphore::new(max_operations)),
            timeout,
        })
    }

    /// Obtain up to [`MAX_RANDOM_BYTES`] bytes from the private OS-random backend.
    /// An empty request returns immediately without invoking the OS or runtime.
    /// Requires a runtime with timers enabled for nonempty requests.
    ///
    /// The OS can block (for example, waiting for entropy initialization).
    /// Cancellation or timeout stops the caller waiting, but cannot interrupt
    /// an OS call already running. The worker retains admission until it really
    /// ends, including after timeout or dropped futures. Queued cancelled work
    /// skips the OS call. No partial output is returned on any failure.
    ///
    /// # Errors
    /// Reports oversize requests, exhausted capacity, missing runtime, entropy
    /// failure, timeout or worker failure without exposing secret bytes.
    pub async fn bytes(&self, length: usize) -> Result<Vec<u8>, RandomError> {
        self.generate_with(length, |bytes| getrandom::fill(bytes).map_err(|_| ()))
            .await
    }

    async fn generate_with<F>(&self, length: usize, generate: F) -> Result<Vec<u8>, RandomError>
    where
        F: FnOnce(&mut [u8]) -> Result<(), ()> + Send + 'static,
    {
        if length > MAX_RANDOM_BYTES {
            return Err(RandomError::TooLarge);
        }
        if length == 0 {
            return Ok(Vec::new());
        }
        crate::async_engine::RuntimeHandle::current()
            .map_err(|_| RandomError::RuntimeUnavailable)?;
        let permit = self
            .permits
            .clone()
            .try_acquire_owned()
            .map_err(|_| RandomError::AtCapacity)?;
        let stopped = Arc::new(AtomicBool::new(false));
        let _cancel = StopOnDrop(stopped.clone());
        let worker = tokio::task::spawn_blocking(move || {
            let _permit = permit;
            if stopped.load(Ordering::Acquire) {
                return Err(RandomError::Cancelled);
            }
            let mut bytes = vec![0; length];
            generate(&mut bytes).map_err(|()| RandomError::Unavailable)?;
            if stopped.load(Ordering::Acquire) {
                return Err(RandomError::Cancelled);
            }
            Ok(bytes)
        });
        tokio::time::timeout(self.timeout, worker)
            .await
            .map_err(|_| RandomError::TimedOut)?
            .map_err(|_| RandomError::WorkerFailed)?
    }
}

struct StopOnDrop(Arc<AtomicBool>);
impl Drop for StopOnDrop {
    fn drop(&mut self) {
        self.0.store(true, Ordering::Release);
    }
}

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

    #[tokio::test]
    async fn success_and_partial_failure_never_confuse_output() {
        let random = SecureRandom::new(1, Duration::from_secs(2)).unwrap();
        assert_eq!(
            random
                .generate_with(4, |bytes| {
                    bytes.copy_from_slice(&[1, 2, 3, 4]);
                    Ok(())
                })
                .await
                .unwrap(),
            [1, 2, 3, 4]
        );
        assert_eq!(
            random
                .generate_with(4, |bytes| {
                    bytes[0] = 42;
                    Err(())
                })
                .await,
            Err(RandomError::Unavailable)
        );
        assert_eq!(
            random.bytes(MAX_RANDOM_BYTES + 1).await,
            Err(RandomError::TooLarge)
        );
        assert!(random
            .generate_with(0, |_| panic!("empty request reached backend"))
            .await
            .unwrap()
            .is_empty());
        assert_eq!(
            random
                .generate_with(MAX_RANDOM_BYTES + 1, |_| panic!(
                    "oversize request reached backend"
                ))
                .await,
            Err(RandomError::TooLarge)
        );
    }

    #[tokio::test]
    async fn timed_out_native_work_keeps_capacity_until_completion() {
        let random = SecureRandom::new(1, Duration::from_millis(100)).unwrap();
        let (release, waiting) = std::sync::mpsc::channel();
        let (started, began) = tokio::sync::oneshot::channel();
        let clone = random.clone();
        let task = tokio::spawn(async move {
            clone
                .generate_with(1, move |_| {
                    let _ = started.send(());
                    let _ = waiting.recv();
                    Ok(())
                })
                .await
        });
        began.await.unwrap();
        assert_eq!(task.await.unwrap(), Err(RandomError::TimedOut));
        assert_eq!(random.bytes(1).await, Err(RandomError::AtCapacity));
        release.send(()).unwrap();
        tokio::time::timeout(Duration::from_secs(2), async {
            while random.permits.available_permits() == 0 {
                tokio::task::yield_now().await;
            }
        })
        .await
        .unwrap();
        assert_eq!(random.generate_with(1, |_| Ok(())).await.unwrap().len(), 1);
    }

    #[tokio::test]
    async fn dropped_request_keeps_capacity_until_completion() {
        let random = SecureRandom::new(1, Duration::from_secs(2)).unwrap();
        let (release, waiting) = std::sync::mpsc::channel();
        let (started, began) = tokio::sync::oneshot::channel();
        let clone = random.clone();
        let task = tokio::spawn(async move {
            clone
                .generate_with(1, move |_| {
                    let _ = started.send(());
                    let _ = waiting.recv();
                    Ok(())
                })
                .await
        });
        began.await.unwrap();
        task.abort();
        assert!(task.await.unwrap_err().is_cancelled());
        assert_eq!(random.bytes(1).await, Err(RandomError::AtCapacity));
        release.send(()).unwrap();
        tokio::time::timeout(Duration::from_secs(2), async {
            while random.permits.available_permits() == 0 {
                tokio::task::yield_now().await;
            }
        })
        .await
        .unwrap();
    }
}