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}