bc_components/
hkdf_rng.rs

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