use cryptography::ChaCha20;
use super::{OsRng, Rng};
const BLOCK_BYTES: usize = 64;
pub struct ChaCha20Rng {
cipher: ChaCha20,
buf: [u8; BLOCK_BYTES],
offset: usize,
}
impl ChaCha20Rng {
#[must_use]
pub fn from_os_rng() -> Self {
let mut os = OsRng::new();
let mut key = [0u8; 32];
let mut nonce = [0u8; 12];
for chunk in key.chunks_exact_mut(4) {
chunk.copy_from_slice(&os.next_u32().to_le_bytes());
}
for chunk in nonce.chunks_exact_mut(4) {
chunk.copy_from_slice(&os.next_u32().to_le_bytes());
}
let cipher = ChaCha20::new(&key, &nonce);
let mut rng = Self {
cipher,
buf: [0u8; BLOCK_BYTES],
offset: BLOCK_BYTES,
};
rng.refill();
rng
}
fn refill(&mut self) {
self.buf = self.cipher.keystream_block();
self.offset = 0;
}
fn take_bytes<const N: usize>(&mut self) -> [u8; N] {
const { assert!(N <= BLOCK_BYTES, "chunk larger than ChaCha20 block") }
if self.offset + N > BLOCK_BYTES {
self.refill();
}
let out = self.buf[self.offset..self.offset + N].try_into().unwrap();
self.offset += N;
out
}
}
impl Default for ChaCha20Rng {
fn default() -> Self {
Self::from_os_rng()
}
}
impl Rng for ChaCha20Rng {
fn next_u32(&mut self) -> u32 {
u32::from_le_bytes(self.take_bytes::<4>())
}
fn next_u64(&mut self) -> u64 {
u64::from_le_bytes(self.take_bytes::<8>())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chacha20_rng_nonzero() {
let mut rng = ChaCha20Rng::from_os_rng();
let v: u64 = (0..8).map(|_| rng.next_u64()).fold(0, |a, b| a | b);
assert_ne!(v, 0);
}
#[test]
fn chacha20_rng_advances() {
let mut rng = ChaCha20Rng::from_os_rng();
let v0 = rng.next_u64();
let v1 = rng.next_u64();
assert_ne!(v0, v1);
}
#[test]
fn chacha20_rng_refills_across_block_boundary() {
let mut rng = ChaCha20Rng::from_os_rng();
for _ in 0..16 {
let _ = rng.next_u32();
}
let v = rng.next_u32(); assert_ne!(v, 0xffff_ffff); }
}