sealwd 0.5.0

Secure password and token management library for Rust, featuring hashing, encryption, and random generation.
Documentation
// Copyright 2026 Thomas Zuyev

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

//     http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Random utilities backed by the operating system entropy source.
//!
//! Errors returned by this module indicate failure to access OS entropy
//! and are typically unrecoverable in production environments.

use rand::{TryRngCore, rngs::OsRng};
use std::num::NonZeroU32;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("OS entropy source is unavailable: {0}")]
    OsRngFailure(#[from] rand::rand_core::OsError),
}

pub type Result<T> = std::result::Result<T, Error>;

pub(crate) fn next_u32() -> Result<u32> {
    let v = OsRng.try_next_u32()?;
    Ok(v)
}

#[expect(dead_code)]
pub(crate) fn next_u64() -> Result<u64> {
    let v = OsRng.try_next_u64()?;
    Ok(v)
}

pub(crate) fn fill_bytes(dst: &mut [u8]) -> Result<()> {
    OsRng.try_fill_bytes(dst)?;
    Ok(())
}

/// Using `random % passw_len` to pick a symbol introduces **modulo bias**.
/// This occurs when the range of `random` is not evenly divisible by `passw_len`.
/// For example, if `random` is 0..255 and `passw_len = 12`:
///   - 12 fits into 256 exactly 21 times (21*12 = 252).
///   - The first 4 indices (0..3) occur one extra time compared to the others,
///     so these symbols are slightly more likely to be chosen.
///
/// To avoid this bias, we use **rejection sampling** below:
///   - Generate a random value.
///   - Only accept it if it falls within the largest multiple of `passw_len`
///     that fits in the random range.
///   - Otherwise, discard and retry.
///
/// This ensures each symbol is chosen with equal probability.
pub(crate) fn secure_idx(alph_len: NonZeroU32) -> Result<usize> {
    let upper_bound = u32::MAX - (u32::MAX % alph_len);
    loop {
        let val = next_u32()?;
        if val < upper_bound {
            return Ok((val % alph_len) as usize);
        }
    }
}