bc_components/
hkdf_rng.rs

1use bc_crypto::hash::hkdf_hmac_sha256;
2use rand_core::{CryptoRng, RngCore};
3use zeroize::ZeroizeOnDrop;
4
5/// A deterministic random number generator based on HKDF-HMAC-SHA256.
6///
7/// `HKDFRng` uses the HMAC-based Key Derivation Function (HKDF) to generate
8/// deterministic random numbers from a combination of key material and salt. It
9/// serves as a key-stretching mechanism that can produce an arbitrary amount of
10/// random-looking bytes from a single seed.
11///
12/// Since it produces deterministic output based on the same inputs, it's useful
13/// for situations where repeatable randomness is required, such as in testing
14/// or when deterministically deriving keys from a master seed.
15///
16/// Security considerations:
17/// - The security of the generator depends on the entropy and secrecy of the
18///   key material
19/// - The same key material and salt will always produce the same sequence
20/// - Use a secure random seed for cryptographic applications
21/// - Never reuse the same HKDFRng instance for different purposes
22///
23/// The implementation automatically handles buffer management, fetching new
24/// data using HKDF as needed with an incrementing counter to ensure unique
25/// output for each request.
26#[derive(ZeroizeOnDrop)]
27pub struct HKDFRng {
28    /// Internal buffer of generated bytes
29    buffer: Vec<u8>,
30    /// Current position in the buffer
31    position: usize,
32    /// Source key material (seed)
33    key_material: Vec<u8>,
34    /// Salt value to combine with the key material
35    salt: String,
36    /// Length of each "page" of generated data
37    page_length: usize,
38    /// Current page index
39    page_index: usize,
40}
41
42impl HKDFRng {
43    /// Creates a new `HKDFRng` with a custom page length.
44    ///
45    /// # Parameters
46    ///
47    /// * `key_material` - The seed material to derive random numbers from
48    /// * `salt` - A salt value to mix with the key material
49    /// * `page_length` - The number of bytes to generate in each HKDF call
50    ///
51    /// # Returns
52    ///
53    /// A new `HKDFRng` instance configured with the specified parameters.
54    ///
55    /// # Example
56    ///
57    /// ```
58    /// use bc_components::HKDFRng;
59    /// use rand_core::RngCore;
60    ///
61    /// // Create an HKDF-based RNG with a 64-byte page length
62    /// let mut rng = HKDFRng::new_with_page_length(
63    ///     b"my secure seed",
64    ///     "application-context",
65    ///     64,
66    /// );
67    ///
68    /// // Generate some random bytes
69    /// let random_u32 = rng.next_u32();
70    /// ```
71    pub fn new_with_page_length(
72        key_material: impl AsRef<[u8]>,
73        salt: &str,
74        page_length: usize,
75    ) -> Self {
76        Self {
77            buffer: Vec::new(),
78            position: 0,
79            key_material: key_material.as_ref().to_vec(),
80            salt: salt.to_string(),
81            page_length,
82            page_index: 0,
83        }
84    }
85
86    /// Creates a new `HKDFRng` with the default page length of 32 bytes.
87    ///
88    /// # Parameters
89    ///
90    /// * `key_material` - The seed material to derive random numbers from
91    /// * `salt` - A salt value to mix with the key material
92    ///
93    /// # Returns
94    ///
95    /// A new `HKDFRng` instance configured with the specified key material and
96    /// salt.
97    ///
98    /// # Example
99    ///
100    /// ```
101    /// use bc_components::HKDFRng;
102    /// use rand_core::RngCore;
103    ///
104    /// // Create an HKDF-based RNG
105    /// let mut rng = HKDFRng::new(b"my secure seed", "wallet-derivation");
106    ///
107    /// // Generate two u32 values
108    /// let random1 = rng.next_u32();
109    /// let random2 = rng.next_u32();
110    ///
111    /// // The same seed and salt will always produce the same sequence
112    /// let mut rng2 = HKDFRng::new(b"my secure seed", "wallet-derivation");
113    /// assert_eq!(random1, rng2.next_u32());
114    /// assert_eq!(random2, rng2.next_u32());
115    /// ```
116    pub fn new(key_material: impl AsRef<[u8]>, salt: &str) -> Self {
117        Self::new_with_page_length(key_material, salt, 32)
118    }
119
120    /// Refills the internal buffer with new deterministic random bytes.
121    ///
122    /// This method is called automatically when the internal buffer is
123    /// exhausted. It uses HKDF-HMAC-SHA256 to generate a new page of random
124    /// bytes using the key material, salt, and current page index.
125    fn fill_buffer(&mut self) {
126        let salt_string = format!("{}-{}", self.salt, self.page_index);
127        let hkdf =
128            hkdf_hmac_sha256(&self.key_material, salt_string, self.page_length);
129        self.buffer = hkdf;
130        self.position = 0;
131        self.page_index += 1;
132    }
133
134    /// Generates the specified number of deterministic random bytes.
135    ///
136    /// # Parameters
137    ///
138    /// * `length` - The number of bytes to generate
139    ///
140    /// # Returns
141    ///
142    /// A vector containing the requested number of deterministic random bytes.
143    fn next_bytes(&mut self, length: usize) -> Vec<u8> {
144        let mut result = Vec::new();
145        while result.len() < length {
146            if self.position >= self.buffer.len() {
147                self.fill_buffer();
148            }
149            let remaining = length - result.len();
150            let available = self.buffer.len() - self.position;
151            let take = remaining.min(available);
152            result.extend_from_slice(
153                &self.buffer[self.position..self.position + take],
154            );
155            self.position += take;
156        }
157        result
158    }
159}
160
161/// Implementation of the `RngCore` trait for `HKDFRng`.
162///
163/// This allows `HKDFRng` to be used with any code that accepts a random
164/// number generator implementing the standard Rust traits.
165impl RngCore for HKDFRng {
166    /// Generates a random `u32` value.
167    ///
168    /// # Returns
169    ///
170    /// A deterministic random 32-bit unsigned integer.
171    fn next_u32(&mut self) -> u32 {
172        let bytes = self.next_bytes(4);
173        u32::from_le_bytes(bytes.try_into().unwrap())
174    }
175
176    /// Generates a random `u64` value.
177    ///
178    /// # Returns
179    ///
180    /// A deterministic random 64-bit unsigned integer.
181    fn next_u64(&mut self) -> u64 {
182        let bytes = self.next_bytes(8);
183        u64::from_le_bytes(bytes.try_into().unwrap())
184    }
185
186    /// Fills the provided buffer with random bytes.
187    ///
188    /// # Parameters
189    ///
190    /// * `dest` - The buffer to fill with random bytes
191    fn fill_bytes(&mut self, dest: &mut [u8]) {
192        let bytes = self.next_bytes(dest.len());
193        dest.copy_from_slice(&bytes);
194    }
195
196    /// Attempts to fill the provided buffer with random bytes.
197    ///
198    /// This implementation never fails, so it simply calls `fill_bytes`.
199    ///
200    /// # Parameters
201    ///
202    /// * `dest` - The buffer to fill with random bytes
203    ///
204    /// # Returns
205    ///
206    /// Always returns `Ok(())` as this implementation cannot fail.
207    fn try_fill_bytes(
208        &mut self,
209        dest: &mut [u8],
210    ) -> Result<(), rand_core::Error> {
211        self.fill_bytes(dest);
212        Ok(())
213    }
214}
215
216/// Implementation of the `CryptoRng` marker trait for `HKDFRng`.
217///
218/// This marker indicates that `HKDFRng` is suitable for cryptographic use
219/// when seeded with appropriately secure key material.
220impl CryptoRng for HKDFRng {}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_hkdf_rng_new() {
228        let rng = HKDFRng::new(b"key_material", "salt");
229        assert_eq!(rng.key_material, b"key_material".to_vec());
230        assert_eq!(rng.salt, "salt");
231        assert_eq!(rng.page_length, 32);
232        assert_eq!(rng.page_index, 0);
233        assert!(rng.buffer.is_empty());
234        assert_eq!(rng.position, 0);
235    }
236
237    #[test]
238    fn test_hkdf_rng_fill_buffer() {
239        let mut rng = HKDFRng::new(b"key_material", "salt");
240        rng.fill_buffer();
241        assert!(!rng.buffer.is_empty()); // Buffer should be filled
242        assert_eq!(rng.position, 0); // Position should be reset
243        assert_eq!(rng.page_index, 1); // Page index should be incremented
244    }
245
246    #[test]
247    fn test_hkdf_rng_next_bytes() {
248        let mut rng = HKDFRng::new(b"key_material", "salt");
249        assert_eq!(
250            hex::encode(rng.next_bytes(16)),
251            "1032ac8ffea232a27c79fe381d7eb7e4"
252        );
253        assert_eq!(
254            hex::encode(rng.next_bytes(16)),
255            "aeaaf727d35b6f338218391f9f8fa1f3"
256        );
257        assert_eq!(
258            hex::encode(rng.next_bytes(16)),
259            "4348a59427711deb1e7d8a6959c6adb4"
260        );
261        assert_eq!(
262            hex::encode(rng.next_bytes(16)),
263            "5d937a42cb5fb090fe1a1ec88f56e32b"
264        );
265    }
266
267    #[test]
268    fn test_hkdf_rng_next_u32() {
269        let mut rng = HKDFRng::new(b"key_material", "salt");
270        let num = rng.next_u32();
271        assert_eq!(num, 2410426896);
272    }
273
274    #[test]
275    fn test_hkdf_rng_next_u64() {
276        let mut rng = HKDFRng::new(b"key_material", "salt");
277        let num = rng.next_u64();
278        assert_eq!(num, 11687583197195678224);
279    }
280
281    #[test]
282    fn test_hkdf_rng_fill_bytes() {
283        let mut rng = HKDFRng::new(b"key_material", "salt");
284        let mut dest = [0u8; 16];
285        rng.fill_bytes(&mut dest);
286        assert_eq!(hex::encode(dest), "1032ac8ffea232a27c79fe381d7eb7e4");
287    }
288
289    #[test]
290    fn test_hkdf_rng_try_fill_bytes() {
291        let mut rng = HKDFRng::new(b"key_material", "salt");
292        let mut dest = [0u8; 16];
293        assert!(rng.try_fill_bytes(&mut dest).is_ok()); // Should succeed without errors
294        assert_eq!(hex::encode(dest), "1032ac8ffea232a27c79fe381d7eb7e4");
295    }
296}