secure_gate/random/
fixed_rng.rs

1// secure-gate/src/random/fixed_rng.rs
2use crate::Fixed;
3use rand::rand_core::OsError;
4use rand::rngs::OsRng;
5use rand::TryRngCore;
6
7#[cfg(any(feature = "encoding-hex", feature = "encoding-base64"))]
8use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
9#[cfg(any(
10    feature = "encoding-hex",
11    feature = "encoding-base64",
12    feature = "encoding-bech32"
13))]
14use bech32::{Bech32, Bech32m, Hrp};
15#[cfg(feature = "encoding-hex")]
16use hex;
17
18/// Fixed-length cryptographically secure random value with encoding methods.
19///
20/// This is a newtype over `Fixed<[u8; N]>` that enforces construction only via secure RNG.
21/// Guarantees freshness — cannot be created from arbitrary bytes.
22///
23/// Requires the "rand" feature.
24///
25/// Supports direct encoding to Hex, Base64, Bech32, and Bech32m via convenience methods.
26///
27/// # Examples
28///
29/// Basic usage:
30/// ```
31/// # #[cfg(feature = "rand")]
32/// # {
33/// use secure_gate::random::FixedRng;
34/// let random: FixedRng<32> = FixedRng::generate();
35/// assert_eq!(random.len(), 32);
36/// # }
37/// ```
38///
39/// With alias:
40/// ```
41/// # #[cfg(feature = "rand")]
42/// # {
43/// use secure_gate::fixed_alias_rng;
44/// fixed_alias_rng!(Nonce, 24);
45/// let nonce = Nonce::generate();
46/// # }
47/// ```
48pub struct FixedRng<const N: usize>(Fixed<[u8; N]>);
49
50impl<const N: usize> FixedRng<N> {
51    /// Generate fresh random bytes using the OS RNG.
52    ///
53    /// Uses `rand::rngs::OsRng` directly for maximum throughput.
54    /// Panics if the RNG fails (rare, but correct for crypto code).
55    ///
56    /// # Example
57    ///
58    /// ```
59    /// # #[cfg(feature = "rand")]
60    /// # {
61    /// use secure_gate::random::FixedRng;
62    /// let random = FixedRng::<16>::generate();
63    /// assert!(!random.is_empty());
64    /// # }
65    /// ```
66    pub fn generate() -> Self {
67        let mut bytes = [0u8; N];
68        OsRng
69            .try_fill_bytes(&mut bytes)
70            .expect("OsRng failed — this should never happen on supported platforms");
71        Self(Fixed::new(bytes))
72    }
73
74    /// Try to generate fresh random bytes using the OS RNG.
75    ///
76    /// Returns an error if the RNG fails.
77    ///
78    /// # Example
79    ///
80    /// ```
81    /// # #[cfg(feature = "rand")]
82    /// # {
83    /// use secure_gate::random::FixedRng;
84    /// let random: Result<FixedRng<32>, rand::rand_core::OsError> = FixedRng::try_generate();
85    /// assert!(random.is_ok());
86    /// # }
87    /// ```
88    pub fn try_generate() -> Result<Self, OsError> {
89        let mut bytes = [0u8; N];
90        OsRng
91            .try_fill_bytes(&mut bytes)
92            .map(|_| Self(Fixed::new(bytes)))
93    }
94
95    /// Expose the random bytes for read-only access.
96    ///
97    /// # Example
98    ///
99    /// ```
100    /// # #[cfg(feature = "rand")]
101    /// # {
102    /// use secure_gate::random::FixedRng;
103    /// let random = FixedRng::<4>::generate();
104    /// let bytes = random.expose_secret();
105    /// # }
106    /// ```
107    #[inline(always)]
108    pub fn expose_secret(&self) -> &[u8; N] {
109        self.0.expose_secret()
110    }
111
112    /// Returns the fixed length in bytes.
113    #[inline(always)]
114    pub const fn len(&self) -> usize {
115        N
116    }
117
118    /// Returns `true` if the length is zero.
119    #[inline(always)]
120    pub const fn is_empty(&self) -> bool {
121        N == 0
122    }
123
124    /// Consume the wrapper and return the inner `Fixed<[u8; N]>`.
125    ///
126    /// This transfers ownership without exposing the secret bytes.
127    /// The returned `Fixed` retains all security guarantees (zeroize, etc.).
128    ///
129    /// # Example
130    ///
131    /// ```
132    /// # #[cfg(feature = "rand")]
133    /// # {
134    /// use secure_gate::{Fixed, random::FixedRng};
135    /// let random = FixedRng::<32>::generate();
136    /// let fixed: Fixed<[u8; 32]> = random.into_inner();
137    /// // Can now use fixed.expose_secret() as needed
138    /// # }
139    /// ```
140    #[inline(always)]
141    pub fn into_inner(self) -> Fixed<[u8; N]> {
142        self.0
143    }
144}
145
146impl<const N: usize> core::fmt::Debug for FixedRng<N> {
147    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
148        f.write_str("[REDACTED]")
149    }
150}
151
152#[cfg(all(feature = "rand", feature = "encoding-bech32"))]
153impl<const N: usize> FixedRng<N> {
154    /// Consume self and return the random bytes as a validated Bech32 string with the specified HRP.
155    ///
156    /// The raw bytes are zeroized immediately after encoding (via drop of `self`).
157    ///
158    /// # Panics
159    ///
160    /// Panics if the HRP is invalid or encoding fails (should never happen for valid random bytes).
161    pub fn into_bech32(self, hrp: &str) -> crate::encoding::bech32::Bech32String {
162        let hrp = Hrp::parse(hrp).expect("invalid HRP");
163        let encoded = bech32::encode::<Bech32>(hrp, self.expose_secret())
164            .expect("encoding valid random bytes cannot fail");
165
166        crate::encoding::bech32::Bech32String::new_unchecked(
167            encoded,
168            crate::encoding::bech32::EncodingVariant::Bech32,
169        )
170    }
171
172    /// Borrow and encode to Bech32 (raw bytes remain available).
173    pub fn to_bech32(&self, hrp: &str) -> crate::encoding::bech32::Bech32String {
174        let hrp = Hrp::parse(hrp).expect("invalid HRP");
175        let encoded = bech32::encode::<Bech32>(hrp, self.expose_secret())
176            .expect("encoding valid random bytes cannot fail");
177
178        crate::encoding::bech32::Bech32String::new_unchecked(
179            encoded,
180            crate::encoding::bech32::EncodingVariant::Bech32,
181        )
182    }
183
184    /// Consume self and return the random bytes as a validated Bech32m string with the specified HRP.
185    ///
186    /// The raw bytes are zeroized immediately after encoding (via drop of `self`).
187    ///
188    /// # Panics
189    ///
190    /// Panics if the HRP is invalid or encoding fails (should never happen for valid random bytes).
191    pub fn into_bech32m(self, hrp: &str) -> crate::encoding::bech32::Bech32String {
192        let hrp = Hrp::parse(hrp).expect("invalid HRP");
193        let encoded = bech32::encode::<Bech32m>(hrp, self.expose_secret())
194            .expect("encoding valid random bytes cannot fail");
195
196        crate::encoding::bech32::Bech32String::new_unchecked(
197            encoded,
198            crate::encoding::bech32::EncodingVariant::Bech32m,
199        )
200    }
201
202    /// Borrow and encode to Bech32m (raw bytes remain available).
203    pub fn to_bech32m(&self, hrp: &str) -> crate::encoding::bech32::Bech32String {
204        let hrp = Hrp::parse(hrp).expect("invalid HRP");
205        let encoded = bech32::encode::<Bech32m>(hrp, self.expose_secret())
206            .expect("encoding valid random bytes cannot fail");
207
208        crate::encoding::bech32::Bech32String::new_unchecked(
209            encoded,
210            crate::encoding::bech32::EncodingVariant::Bech32m,
211        )
212    }
213}
214
215#[cfg(all(feature = "rand", feature = "encoding-hex"))]
216impl<const N: usize> FixedRng<N> {
217    /// Consume self and return the random bytes as a validated hex string.
218    ///
219    /// The raw bytes are zeroized immediately after encoding.
220    pub fn into_hex(self) -> crate::encoding::hex::HexString {
221        let hex_str = hex::encode(self.expose_secret());
222        crate::encoding::hex::HexString::new_unchecked(hex_str)
223    }
224
225    /// Encode to hex without consuming self, for cases where raw is still needed briefly.
226    pub fn to_hex(&self) -> crate::encoding::hex::HexString {
227        let hex_str = hex::encode(self.expose_secret());
228        crate::encoding::hex::HexString::new_unchecked(hex_str)
229    }
230}
231
232#[cfg(all(feature = "rand", feature = "encoding-base64"))]
233impl<const N: usize> FixedRng<N> {
234    /// Consume self and return the random bytes as a validated base64 string.
235    ///
236    /// The raw bytes are zeroized immediately after encoding.
237    pub fn into_base64(self) -> crate::encoding::base64::Base64String {
238        let encoded = URL_SAFE_NO_PAD.encode(self.expose_secret());
239        crate::encoding::base64::Base64String::new_unchecked(encoded)
240    }
241
242    /// Encode to base64 without consuming self, for cases where raw is still needed briefly.
243    pub fn to_base64(&self) -> crate::encoding::base64::Base64String {
244        let encoded = URL_SAFE_NO_PAD.encode(self.expose_secret());
245        crate::encoding::base64::Base64String::new_unchecked(encoded)
246    }
247}
248
249impl<const N: usize> From<FixedRng<N>> for Fixed<[u8; N]> {
250    /// Convert a `FixedRng` to `Fixed`, transferring ownership.
251    ///
252    /// This preserves all security guarantees. The `FixedRng` type
253    /// ensures the value came from secure RNG, and this conversion
254    /// transfers that value to `Fixed` without exposing bytes.
255    ///
256    /// # Example
257    ///
258    /// ```
259    /// # #[cfg(feature = "rand")]
260    /// # {
261    /// use secure_gate::{Fixed, random::FixedRng};
262    /// let key: Fixed<[u8; 32]> = FixedRng::<32>::generate().into();
263    /// # }
264    /// ```
265    #[inline(always)]
266    fn from(rng: FixedRng<N>) -> Self {
267        rng.into_inner()
268    }
269}