nula_core/util/rng.rs
1//! Random byte generation helpers.
2//!
3//! The Nostr protocol needs cryptographically random bytes for fresh secret
4//! keys and unique subscription IDs. This module wraps [`getrandom`] and
5//! exposes a tiny stable API: it returns errors instead of panicking when the
6//! operating system fails to provide entropy, which is critical for relays
7//! and signers running in constrained environments (containers, jails, kernel
8//! lockdown, …).
9
10use thiserror::Error;
11
12/// Error returned when the operating system fails to provide entropy.
13#[derive(Debug, Clone, Copy, Error)]
14#[error("operating system failed to provide entropy: {0}")]
15#[non_exhaustive]
16pub struct RngError(#[from] pub(crate) getrandom::Error);
17
18/// Fill `buf` with cryptographically secure random bytes from the OS.
19///
20/// # Errors
21///
22/// Propagates errors from the OS entropy source.
23pub fn fill_bytes(buf: &mut [u8]) -> Result<(), RngError> {
24 getrandom::fill(buf)?;
25 Ok(())
26}
27
28/// Return `N` cryptographically secure random bytes from the OS RNG.
29///
30/// # Errors
31///
32/// Propagates errors from the OS entropy source.
33pub fn random_bytes<const N: usize>() -> Result<[u8; N], RngError> {
34 let mut out = [0_u8; N];
35 fill_bytes(&mut out)?;
36 Ok(out)
37}
38
39/// Generate a lowercase hex string built from `N` random bytes.
40///
41/// The output is always `2 * N` characters long. This is the canonical format
42/// used by Nostr subscription IDs and other opaque identifiers carried over
43/// the wire.
44///
45/// # Errors
46///
47/// Propagates errors from the OS entropy source.
48pub fn random_hex_string<const N: usize>() -> Result<String, RngError> {
49 let bytes = random_bytes::<N>()?;
50 Ok(crate::util::hex::encode(bytes))
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn random_bytes_distinct() {
59 let lhs: [u8; 32] = random_bytes().unwrap();
60 let rhs: [u8; 32] = random_bytes().unwrap();
61 assert_ne!(lhs, rhs);
62 }
63
64 #[test]
65 fn random_hex_string_length() {
66 let value = random_hex_string::<16>().unwrap();
67 assert_eq!(value.len(), 32);
68 assert!(value.chars().all(|c| c.is_ascii_hexdigit()));
69 }
70}