1#![allow(unsafe_code)]
21
22mod aes_ctr;
23mod entropy;
24mod fork;
25
26pub use entropy::cpu_rng_available;
27
28use zeroize::{Zeroize as _, Zeroizing};
29
30pub const KEY_SIZE: usize = 32;
32pub const NONCE_SIZE: usize = 12;
34pub const AES_CTR_SEED_SIZE: usize = 48;
36
37const AES_BLOCK_SIZE: usize = 16;
38const DEFAULT_RESEED_INTERVAL_BYTES: u64 = 1 << 30;
39
40#[derive(Debug)]
42#[non_exhaustive]
43pub enum Error {
44 OsEntropy(getrandom::Error),
46 UnsupportedCpu {
48 required: &'static str,
50 },
51 ForkDetected,
53}
54
55impl std::fmt::Display for Error {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 Self::OsEntropy(err) => write!(f, "OS entropy failed: {err}"),
59 Self::UnsupportedCpu { required } => {
60 write!(
61 f,
62 "required AES-CTR hardware support is unavailable: {required}"
63 )
64 }
65 Self::ForkDetected => f.write_str("random generator state crossed a process fork"),
66 }
67 }
68}
69
70impl std::error::Error for Error {}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74enum ForkGuard {
75 Generation(u64),
78 ProcessId(u32),
81}
82
83impl ForkGuard {
84 fn capture() -> Self {
85 fork::generation().map_or_else(|| Self::ProcessId(current_process_id()), Self::Generation)
86 }
87
88 fn check(self) -> Result<(), Error> {
89 let unchanged = match self {
90 Self::Generation(seen) => fork::generation() == Some(seen),
91 Self::ProcessId(seen) => current_process_id() == seen,
92 };
93 if unchanged {
94 Ok(())
95 } else {
96 Err(Error::ForkDetected)
97 }
98 }
99
100 #[cfg(test)]
101 fn corrupted(self) -> Self {
102 match self {
103 Self::Generation(seen) => Self::Generation(seen.wrapping_add(1)),
104 Self::ProcessId(seen) => Self::ProcessId(seen.wrapping_add(1)),
105 }
106 }
107}
108
109pub trait KeyGenerator {
111 fn fill_bytes(&mut self, out: &mut [u8]) -> Result<(), Error>;
117
118 fn key_32(&mut self) -> Result<[u8; KEY_SIZE], Error> {
124 let mut out = [0_u8; KEY_SIZE];
125 self.fill_bytes(&mut out)?;
126 Ok(out)
127 }
128
129 fn nonce_12(&mut self) -> Result<[u8; NONCE_SIZE], Error> {
135 let mut out = [0_u8; NONCE_SIZE];
136 self.fill_bytes(&mut out)?;
137 Ok(out)
138 }
139}
140
141fn fill_from_os(out: &mut [u8]) -> Result<(), Error> {
143 getrandom::fill(out).map_err(Error::OsEntropy)
144}
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
148pub struct StateLayout {
149 pub size: usize,
151 pub align: usize,
153}
154
155pub struct AesCtrKeyGenerator {
161 backend: aes_ctr::Backend,
162 buffer: [u8; AES_BLOCK_SIZE],
163 buffer_pos: usize,
164 generated_since_reseed: u64,
165 reseed_interval: u64,
166 fork_guard: ForkGuard,
167}
168
169impl AesCtrKeyGenerator {
170 #[must_use]
173 pub fn hardware_available() -> bool {
174 aes_ctr::hardware_available()
175 }
176
177 #[must_use]
179 pub const fn state_layout() -> StateLayout {
180 StateLayout {
181 size: core::mem::size_of::<Self>(),
182 align: core::mem::align_of::<Self>(),
183 }
184 }
185
186 pub fn from_os_entropy() -> Result<Self, Error> {
194 if !Self::hardware_available() {
195 return Err(Error::UnsupportedCpu {
196 required: aes_ctr::REQUIRED_FEATURES,
197 });
198 }
199
200 let mut seed = Zeroizing::new([0_u8; AES_CTR_SEED_SIZE]);
201 fill_from_os(seed.as_mut())?;
202 Self::from_seed_bytes(&seed)
203 }
204
205 pub fn from_seed(seed: &mut [u8; AES_CTR_SEED_SIZE]) -> Result<Self, Error> {
217 let result = Self::from_seed_bytes(seed);
218 seed.zeroize();
219 result
220 }
221
222 fn from_seed_bytes(seed: &[u8; AES_CTR_SEED_SIZE]) -> Result<Self, Error> {
223 if !Self::hardware_available() {
224 return Err(Error::UnsupportedCpu {
225 required: aes_ctr::REQUIRED_FEATURES,
226 });
227 }
228
229 let (key, counter) = split_seed(seed);
230 let backend = aes_ctr::Backend::new(key, counter).ok_or(Error::UnsupportedCpu {
231 required: aes_ctr::REQUIRED_FEATURES,
232 })?;
233
234 Ok(Self {
235 backend,
236 buffer: [0_u8; AES_BLOCK_SIZE],
237 buffer_pos: AES_BLOCK_SIZE,
238 generated_since_reseed: 0,
239 reseed_interval: DEFAULT_RESEED_INTERVAL_BYTES,
240 fork_guard: ForkGuard::capture(),
241 })
242 }
243
244 pub fn key_32(&mut self) -> Result<[u8; KEY_SIZE], Error> {
250 KeyGenerator::key_32(self)
251 }
252
253 pub fn nonce_12(&mut self) -> Result<[u8; NONCE_SIZE], Error> {
259 KeyGenerator::nonce_12(self)
260 }
261
262 fn drain_buffer(&mut self, out: &mut [u8]) -> usize {
263 let available = AES_BLOCK_SIZE - self.buffer_pos;
267 let take = available.min(out.len());
268 if take == 0 {
269 return 0;
270 }
271
272 let end = self.buffer_pos + take;
273 out[..take].copy_from_slice(&self.buffer[self.buffer_pos..end]);
274 self.buffer[self.buffer_pos..end].zeroize();
275 self.buffer_pos = end;
276 take
277 }
278
279 fn reseed(&mut self) -> Result<(), Error> {
286 let mut entropy_input = Zeroizing::new([0_u8; AES_CTR_SEED_SIZE]);
287 if entropy::cpu_rng_fill(entropy_input.as_mut()) {
288 self.reseed_blend(&entropy_input);
289 } else {
290 fill_from_os(entropy_input.as_mut())?;
291 self.reset_from_seed(&entropy_input)?;
292 }
293 self.buffer.zeroize();
294 self.buffer_pos = AES_BLOCK_SIZE;
295 self.generated_since_reseed = 0;
296 self.fork_guard = ForkGuard::capture();
297 Ok(())
298 }
299
300 fn reset_from_seed(&mut self, seed: &[u8; AES_CTR_SEED_SIZE]) -> Result<(), Error> {
302 let (key, counter) = split_seed(seed);
303 self.backend = aes_ctr::Backend::new(key, counter).ok_or(Error::UnsupportedCpu {
304 required: aes_ctr::REQUIRED_FEATURES,
305 })?;
306 Ok(())
307 }
308
309 fn reseed_blend(&mut self, cpu_entropy: &[u8; AES_CTR_SEED_SIZE]) {
322 let mut seed = Zeroizing::new([0_u8; AES_CTR_SEED_SIZE]);
323 for block in seed.chunks_exact_mut(AES_BLOCK_SIZE) {
325 let mut keystream = Zeroizing::new([0_u8; AES_BLOCK_SIZE]);
326 self.backend.fill_block(&mut keystream);
327 block.copy_from_slice(keystream.as_ref());
328 }
329 for (seed_byte, entropy_byte) in seed.iter_mut().zip(cpu_entropy.iter()) {
330 *seed_byte ^= entropy_byte;
331 }
332 let (key, counter) = split_seed(&seed);
333 if let Some(backend) = aes_ctr::Backend::new(key, counter) {
336 self.backend = backend;
337 }
338 }
339
340 fn fill_checked(&mut self, mut out: &mut [u8]) -> Result<(), Error> {
341 self.fork_guard.check()?;
342 while !out.is_empty() {
343 let reseed_interval = self.reseed_interval.max(1);
346 if self.generated_since_reseed >= reseed_interval {
347 self.reseed()?;
348 }
349
350 let remaining_before_reseed = reseed_interval - self.generated_since_reseed;
351 let take = out
352 .len()
353 .min(usize::try_from(remaining_before_reseed).unwrap_or(usize::MAX));
354 let (chunk, rest) = out.split_at_mut(take);
355 self.fill_without_lifecycle_checks(chunk);
356 self.generated_since_reseed += u64::try_from(chunk.len()).unwrap_or(u64::MAX);
357 out = rest;
358 }
359 Ok(())
360 }
361
362 fn fill_without_lifecycle_checks(&mut self, mut out: &mut [u8]) {
363 let copied = self.drain_buffer(out);
364 out = &mut out[copied..];
365
366 let mut chunks = out.chunks_exact_mut(AES_BLOCK_SIZE);
367 for chunk in &mut chunks {
368 let mut block = Zeroizing::new([0_u8; AES_BLOCK_SIZE]);
369 self.backend.fill_block(&mut block);
370 chunk.copy_from_slice(block.as_ref());
371 }
372
373 let remainder = chunks.into_remainder();
374 if !remainder.is_empty() {
375 self.backend.fill_block(&mut self.buffer);
376 remainder.copy_from_slice(&self.buffer[..remainder.len()]);
377 self.buffer[..remainder.len()].zeroize();
378 self.buffer_pos = remainder.len();
379 }
380 }
381}
382
383impl Drop for AesCtrKeyGenerator {
384 fn drop(&mut self) {
385 self.buffer.zeroize();
386 self.buffer_pos = AES_BLOCK_SIZE;
387 self.generated_since_reseed = 0;
388 self.reseed_interval = 0;
389 }
390}
391
392impl std::fmt::Debug for AesCtrKeyGenerator {
393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394 f.debug_struct("AesCtrKeyGenerator").finish_non_exhaustive()
395 }
396}
397
398impl KeyGenerator for AesCtrKeyGenerator {
399 fn fill_bytes(&mut self, out: &mut [u8]) -> Result<(), Error> {
401 self.fill_checked(out)
402 }
403}
404
405fn current_process_id() -> u32 {
406 std::process::id()
407}
408
409#[allow(clippy::expect_used)] fn split_seed(seed: &[u8; AES_CTR_SEED_SIZE]) -> (&[u8; KEY_SIZE], &[u8; AES_BLOCK_SIZE]) {
412 const _: () = assert!(AES_CTR_SEED_SIZE == KEY_SIZE + AES_BLOCK_SIZE);
413 let (key, counter) = seed.split_at(KEY_SIZE);
414 (
415 key.try_into().expect("seed key segment is KEY_SIZE bytes"),
416 counter
417 .try_into()
418 .expect("seed counter segment is AES_BLOCK_SIZE bytes"),
419 )
420}
421
422#[cfg(test)]
423mod tests {
424 #![allow(clippy::expect_used)]
425
426 use super::{AesCtrKeyGenerator, KeyGenerator, AES_BLOCK_SIZE, AES_CTR_SEED_SIZE, KEY_SIZE};
427
428 #[cfg(unix)]
429 #[test]
430 fn detects_state_inherited_across_real_fork() {
431 if !AesCtrKeyGenerator::hardware_available() {
432 return;
433 }
434
435 let mut rng =
436 AesCtrKeyGenerator::from_os_entropy().expect("OS entropy should be available");
437 let mut out = [0_u8; 1];
438 rng.fill_bytes(&mut out)
439 .expect("parent generation should succeed");
440
441 let pid = unsafe { libc::fork() };
445 assert!(pid >= 0, "fork should succeed");
446 if pid == 0 {
447 let code = match rng.fill_bytes(&mut out) {
448 Err(super::Error::ForkDetected) => 0,
449 Ok(()) | Err(_) => 1,
450 };
451 unsafe { libc::_exit(code) };
453 }
454
455 let mut status = 0;
456 let waited = unsafe { libc::waitpid(pid, &raw mut status, 0) };
459 assert_eq!(waited, pid, "waitpid should reap the forked child");
460 assert!(libc::WIFEXITED(status), "child should exit normally");
461 assert_eq!(
462 libc::WEXITSTATUS(status),
463 0,
464 "child must observe ForkDetected from inherited generator state"
465 );
466 }
467
468 #[test]
469 fn reseed_blend_is_deterministic_and_entropy_dependent() {
470 if !AesCtrKeyGenerator::hardware_available() {
471 return;
472 }
473
474 let entropy_a = [0x11_u8; AES_CTR_SEED_SIZE];
478 let entropy_b = [0x22_u8; AES_CTR_SEED_SIZE];
479
480 let mut seed = [0x5e_u8; AES_CTR_SEED_SIZE];
481 let mut rng_a = AesCtrKeyGenerator::from_seed(&mut seed.clone())
482 .expect("AES hardware should be available");
483 let mut rng_a2 = AesCtrKeyGenerator::from_seed(&mut seed.clone())
484 .expect("AES hardware should be available");
485 let mut rng_b =
486 AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
487
488 rng_a.reseed_blend(&entropy_a);
489 rng_a2.reseed_blend(&entropy_a);
490 rng_b.reseed_blend(&entropy_b);
491
492 let mut out_a = [0_u8; 32];
493 let mut out_a2 = [0_u8; 32];
494 let mut out_b = [0_u8; 32];
495 rng_a.fill_without_lifecycle_checks(&mut out_a);
496 rng_a2.fill_without_lifecycle_checks(&mut out_a2);
497 rng_b.fill_without_lifecycle_checks(&mut out_b);
498
499 assert_eq!(out_a, out_a2, "same seed + same entropy must blend equally");
500 assert_ne!(
501 out_a, out_b,
502 "different entropy must change the blended state"
503 );
504 }
505
506 #[test]
507 fn reseed_blend_changes_state() {
508 if !AesCtrKeyGenerator::hardware_available() {
509 return;
510 }
511
512 let mut seed = [0x33_u8; AES_CTR_SEED_SIZE];
513 let mut rng =
514 AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
515 let mut before = [0_u8; 32];
516 rng.fill_without_lifecycle_checks(&mut before);
517
518 rng.reseed_blend(&[0x44_u8; AES_CTR_SEED_SIZE]);
519 let mut after = [0_u8; 32];
520 rng.fill_without_lifecycle_checks(&mut after);
521 assert_ne!(before, after, "reseed must change subsequent output");
522 }
523
524 #[test]
525 fn cpu_rng_available_does_not_panic() {
526 let _ = super::cpu_rng_available();
529 }
530
531 #[test]
532 fn aes_ctr_matches_aes256_ctr_known_answer() {
533 if !AesCtrKeyGenerator::hardware_available() {
534 return;
535 }
536
537 let mut seed = [0_u8; AES_CTR_SEED_SIZE];
538 seed[..KEY_SIZE].copy_from_slice(&[
539 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
540 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
541 0x1c, 0x1d, 0x1e, 0x1f,
542 ]);
543 seed[KEY_SIZE..].copy_from_slice(&[
544 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
545 0xee, 0xff,
546 ]);
547
548 let mut rng =
549 AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
550 let mut out = [0_u8; AES_BLOCK_SIZE * 2];
551 rng.fill_bytes(&mut out)
552 .expect("AES-CTR generation should succeed");
553
554 assert_eq!(
555 out,
556 [
557 0x8e, 0xa2, 0xb7, 0xca, 0x51, 0x67, 0x45, 0xbf, 0xea, 0xfc, 0x49, 0x90, 0x4b, 0x49,
558 0x60, 0x89, 0x81, 0xae, 0x7d, 0x5e, 0x41, 0x38, 0xbf, 0x73, 0x0d, 0x2a, 0x88, 0x71,
559 0xfe, 0xc2, 0xcd, 0x0c,
560 ]
561 );
562 }
563
564 #[test]
565 fn aes_ctr_fill_bytes_is_contiguous_across_partial_calls() {
566 if !AesCtrKeyGenerator::hardware_available() {
567 return;
568 }
569
570 let mut seed = [7_u8; AES_CTR_SEED_SIZE];
571 let mut seed_copy = seed;
572 let mut one_shot =
573 AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
574 let mut split = AesCtrKeyGenerator::from_seed(&mut seed_copy)
575 .expect("AES hardware should be available");
576
577 let mut expected = [0_u8; 64];
578 one_shot
579 .fill_bytes(&mut expected)
580 .expect("AES-CTR generation should succeed");
581
582 let mut actual = [0_u8; 64];
583 split
584 .fill_bytes(&mut actual[..3])
585 .expect("AES-CTR generation should succeed");
586 split
587 .fill_bytes(&mut actual[3..19])
588 .expect("AES-CTR generation should succeed");
589 split
590 .fill_bytes(&mut actual[19..31])
591 .expect("AES-CTR generation should succeed");
592 split
593 .fill_bytes(&mut actual[31..])
594 .expect("AES-CTR generation should succeed");
595
596 assert_eq!(actual, expected);
597 }
598
599 #[test]
600 fn aes_ctr_from_seed_zeroizes_seed_buffer() {
601 if !AesCtrKeyGenerator::hardware_available() {
602 return;
603 }
604
605 let mut seed = [0x5a_u8; AES_CTR_SEED_SIZE];
606 let _rng =
607 AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
608
609 assert_eq!(seed, [0_u8; AES_CTR_SEED_SIZE]);
610 }
611
612 #[test]
613 fn aes_ctr_reseeds_after_generation_interval() {
614 if !AesCtrKeyGenerator::hardware_available() {
615 return;
616 }
617
618 let mut seed = [3_u8; AES_CTR_SEED_SIZE];
619 let mut rng =
620 AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
621 rng.reseed_interval = 8;
622 let mut out = [0_u8; 32];
623 rng.fill_bytes(&mut out)
624 .expect("fill across reseed boundary should succeed");
625 assert!(rng.generated_since_reseed <= rng.reseed_interval);
626 }
627
628 #[test]
629 fn aes_ctr_rejects_forked_state() {
630 if !AesCtrKeyGenerator::hardware_available() {
631 return;
632 }
633
634 let mut seed = [3_u8; AES_CTR_SEED_SIZE];
635 let mut rng =
636 AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
637 rng.fork_guard = rng.fork_guard.corrupted();
638 let mut out = [0_u8; 1];
639 assert!(matches!(
640 rng.fill_bytes(&mut out),
641 Err(super::Error::ForkDetected)
642 ));
643 }
644
645 #[test]
646 fn aes_ctr_state_stays_within_initial_target_size() {
647 if !AesCtrKeyGenerator::hardware_available() {
648 return;
649 }
650
651 let layout = AesCtrKeyGenerator::state_layout();
652 assert!(
653 layout.size <= 320,
654 "AES-CTR state size {} exceeded 320-byte target",
655 layout.size
656 );
657 assert!(
658 layout.align >= 16,
659 "AES-CTR state alignment {} should hold AES block vectors",
660 layout.align
661 );
662 }
663}