pub trait Csprng {
fn fill_bytes(&mut self, out: &mut [u8]);
}
pub struct ChaCha20Rng {
key: [u32; 8],
nonce: [u32; 3],
counter: u32,
buf: [u8; 64],
buf_pos: usize,
}
impl core::fmt::Debug for ChaCha20Rng {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ChaCha20Rng")
.field("buf_pos", &self.buf_pos)
.field("counter", &self.counter)
.finish_non_exhaustive()
}
}
impl Drop for ChaCha20Rng {
fn drop(&mut self) {
for w in self.key.iter_mut() {
unsafe { core::ptr::write_volatile(w, 0u32) };
}
for w in self.nonce.iter_mut() {
unsafe { core::ptr::write_volatile(w, 0u32) };
}
unsafe { core::ptr::write_volatile(&mut self.counter, 0u32) };
for b in self.buf.iter_mut() {
unsafe { core::ptr::write_volatile(b, 0u8) };
}
unsafe { core::ptr::write_volatile(&mut self.buf_pos, 0usize) };
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
}
impl ChaCha20Rng {
#[must_use]
pub fn from_os_entropy(os: &mut OsRng) -> Self {
let mut seed = [0u8; 32];
os.fill_bytes(&mut seed);
Self::from_seed(&seed)
}
#[must_use]
pub fn from_seed(seed: &[u8; 32]) -> Self {
let mut key = [0u32; 8];
for (i, k) in key.iter_mut().enumerate() {
*k = u32::from_le_bytes([
seed[i * 4],
seed[i * 4 + 1],
seed[i * 4 + 2],
seed[i * 4 + 3],
]);
}
Self {
key,
nonce: [0; 3],
counter: 0,
buf: [0; 64],
buf_pos: 64,
}
}
fn refill(&mut self) {
const C0: u32 = 0x6170_7865;
const C1: u32 = 0x3320_646e;
const C2: u32 = 0x7962_2d32;
const C3: u32 = 0x6b20_6574;
let mut state: [u32; 16] = [
C0,
C1,
C2,
C3,
self.key[0],
self.key[1],
self.key[2],
self.key[3],
self.key[4],
self.key[5],
self.key[6],
self.key[7],
self.counter,
self.nonce[0],
self.nonce[1],
self.nonce[2],
];
let mut init = state;
for _ in 0..10 {
quarter_round(&mut state, 0, 4, 8, 12);
quarter_round(&mut state, 1, 5, 9, 13);
quarter_round(&mut state, 2, 6, 10, 14);
quarter_round(&mut state, 3, 7, 11, 15);
quarter_round(&mut state, 0, 5, 10, 15);
quarter_round(&mut state, 1, 6, 11, 12);
quarter_round(&mut state, 2, 7, 8, 13);
quarter_round(&mut state, 3, 4, 9, 14);
}
for i in 0..16 {
state[i] = state[i].wrapping_add(init[i]);
}
for (i, word) in state.iter().enumerate() {
self.buf[i * 4..(i + 1) * 4].copy_from_slice(&word.to_le_bytes());
}
let (next, carry) = self.counter.overflowing_add(1);
self.counter = next;
if carry {
for slot in self.nonce.iter_mut() {
let (v, c) = slot.overflowing_add(1);
*slot = v;
if !c {
break;
}
}
assert!(
!(self.nonce[0] == 0 && self.nonce[1] == 0 && self.nonce[2] == 0
&& self.counter == 0),
"ChaCha20Rng exhausted: 2^128 blocks generated under one key",
);
}
self.buf_pos = 0;
for w in state.iter_mut() {
unsafe { core::ptr::write_volatile(w, 0u32) };
}
for w in init.iter_mut() {
unsafe { core::ptr::write_volatile(w, 0u32) };
}
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
}
fn quarter_round(s: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) {
s[a] = s[a].wrapping_add(s[b]);
s[d] ^= s[a];
s[d] = s[d].rotate_left(16);
s[c] = s[c].wrapping_add(s[d]);
s[b] ^= s[c];
s[b] = s[b].rotate_left(12);
s[a] = s[a].wrapping_add(s[b]);
s[d] ^= s[a];
s[d] = s[d].rotate_left(8);
s[c] = s[c].wrapping_add(s[d]);
s[b] ^= s[c];
s[b] = s[b].rotate_left(7);
}
impl Csprng for ChaCha20Rng {
fn fill_bytes(&mut self, out: &mut [u8]) {
let mut written = 0;
while written < out.len() {
if self.buf_pos == 64 {
self.refill();
}
let want = (out.len() - written).min(64 - self.buf_pos);
out[written..written + want]
.copy_from_slice(&self.buf[self.buf_pos..self.buf_pos + want]);
self.buf_pos += want;
written += want;
}
}
}
pub struct OsRng {
file: std::fs::File,
}
impl core::fmt::Debug for OsRng {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("OsRng(<entropy source, fd elided>)")
}
}
impl OsRng {
pub fn new() -> std::io::Result<Self> {
let file = std::fs::File::open("/dev/urandom")?;
Ok(Self { file })
}
}
impl Csprng for OsRng {
fn fill_bytes(&mut self, out: &mut [u8]) {
use std::io::Read;
let mut written = 0;
while written < out.len() {
let n = self
.file
.read(&mut out[written..])
.expect("/dev/urandom read failed");
assert!(n > 0, "/dev/urandom unexpectedly returned 0 bytes");
written += n;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fills_buffer_deterministically_from_seed() {
let mut a = ChaCha20Rng::from_seed(&[0x42u8; 32]);
let mut b = ChaCha20Rng::from_seed(&[0x42u8; 32]);
let mut buf_a = [0u8; 200];
let mut buf_b = [0u8; 200];
a.fill_bytes(&mut buf_a);
b.fill_bytes(&mut buf_b);
assert_eq!(buf_a, buf_b);
}
#[test]
fn different_seeds_produce_different_streams() {
let mut a = ChaCha20Rng::from_seed(&[0x01u8; 32]);
let mut b = ChaCha20Rng::from_seed(&[0x02u8; 32]);
let mut buf_a = [0u8; 64];
let mut buf_b = [0u8; 64];
a.fill_bytes(&mut buf_a);
b.fill_bytes(&mut buf_b);
assert_ne!(buf_a, buf_b);
}
#[test]
fn matches_rfc7539_test_vector_zero_key_zero_nonce() {
let mut rng = ChaCha20Rng::from_seed(&[0u8; 32]);
let mut got = [0u8; 64];
rng.fill_bytes(&mut got);
let expected: [u8; 64] = [
0x76, 0xb8, 0xe0, 0xad, 0xa0, 0xf1, 0x3d, 0x90, 0x40, 0x5d, 0x6a, 0xe5, 0x53, 0x86,
0xbd, 0x28, 0xbd, 0xd2, 0x19, 0xb8, 0xa0, 0x8d, 0xed, 0x1a, 0xa8, 0x36, 0xef, 0xcc,
0x8b, 0x77, 0x0d, 0xc7, 0xda, 0x41, 0x59, 0x7c, 0x51, 0x57, 0x48, 0x8d, 0x77, 0x24,
0xe0, 0x3f, 0xb8, 0xd8, 0x4a, 0x37, 0x6a, 0x43, 0xb8, 0xf4, 0x15, 0x18, 0xa1, 0x1c,
0xc3, 0x87, 0xb6, 0x69, 0xb2, 0xee, 0x65, 0x86,
];
assert_eq!(got, expected);
}
#[test]
fn os_rng_fills_buffer_with_distinct_bytes() {
let Ok(mut os) = OsRng::new() else {
return;
};
let mut a = [0u8; 32];
let mut b = [0u8; 32];
os.fill_bytes(&mut a);
os.fill_bytes(&mut b);
assert!(a.iter().any(|&v| v != 0), "all-zero draw is overwhelmingly unlikely");
assert_ne!(a, b, "two draws should differ");
}
#[test]
fn chacha20_from_os_entropy_does_not_panic() {
let Ok(mut os) = OsRng::new() else {
return;
};
let mut rng = ChaCha20Rng::from_os_entropy(&mut os);
let mut buf = [0u8; 64];
rng.fill_bytes(&mut buf);
assert!(buf.iter().any(|&v| v != 0));
}
#[test]
fn chacha20_drop_does_not_panic() {
for _ in 0..10 {
let mut rng = ChaCha20Rng::from_seed(&[0xA7u8; 32]);
let mut buf = [0u8; 32];
rng.fill_bytes(&mut buf);
}
}
#[test]
fn supports_short_and_long_fills() {
let mut rng = ChaCha20Rng::from_seed(&[0xFFu8; 32]);
let mut single_call = [0u8; 200];
rng.fill_bytes(&mut single_call);
let mut rng2 = ChaCha20Rng::from_seed(&[0xFFu8; 32]);
let mut split = [0u8; 200];
let mut written = 0;
for chunk in [1, 7, 64, 50, 1, 77].iter().copied() {
let end = (written + chunk).min(split.len());
rng2.fill_bytes(&mut split[written..end]);
written = end;
}
rng2.fill_bytes(&mut split[written..]);
assert_eq!(single_call, split);
}
}