use crate::error::{Error, Result};
use hmac::{Hmac, Mac};
use rand_core::{CryptoRng, Error as RandError, RngCore};
use sha2::Sha512;
use zeroize::Zeroize;
type HmacSha512 = Hmac<Sha512>;
const LEN: usize = 64;
const BYTE_00: &[u8] = &[0x00];
const BYTE_01: &[u8] = &[0x01];
pub struct DetRng {
mac: HmacSha512,
v: [u8; LEN],
buf: [u8; LEN],
pos: usize,
}
impl core::fmt::Debug for DetRng {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("DetRng")
.field("mac", &"<redacted>")
.field("v", &"<redacted>")
.field("buf", &"<redacted>")
.field("pos", &self.pos)
.finish()
}
}
fn hmac(key: &[u8], parts: &[&[u8]]) -> Result<[u8; LEN]> {
let mut mac = HmacSha512::new_from_slice(key)
.map_err(|_| Error::DetRngError("failed to initialize HMAC-SHA-512".to_string()))?;
for part in parts {
mac.update(part);
}
Ok(mac.finalize().into_bytes().into())
}
impl DetRng {
pub fn new(domain: &[u8], secret: &[u8], message: &[u8]) -> Result<Self> {
let mut v = [0x01u8; LEN];
let mut k = [0x00u8; LEN];
k = hmac(&k, &[&v, BYTE_00, domain, secret, message])?;
v = hmac(&k, &[&v])?;
k = hmac(&k, &[&v, BYTE_01, domain, secret, message])?;
v = hmac(&k, &[&v])?;
let mac = HmacSha512::new_from_slice(&k)
.map_err(|_| Error::DetRngError("failed to initialize HMAC-SHA-512".to_string()))?;
k.zeroize();
Ok(Self {
mac,
v,
buf: [0u8; LEN],
pos: LEN,
})
}
fn refill(&mut self) {
let mut mac = self.mac.clone();
mac.update(&self.v);
let out = mac.finalize().into_bytes();
self.v.copy_from_slice(&out);
self.buf.copy_from_slice(&out);
self.pos = 0;
}
}
impl RngCore for DetRng {
fn next_u32(&mut self) -> u32 {
let mut buf = [0u8; 4];
self.fill_bytes(&mut buf);
u32::from_le_bytes(buf)
}
fn next_u64(&mut self) -> u64 {
let mut buf = [0u8; 8];
self.fill_bytes(&mut buf);
u64::from_le_bytes(buf)
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
let mut off = 0usize;
while off < dest.len() {
if self.pos == LEN {
self.refill();
}
let take = (LEN - self.pos).min(dest.len() - off);
dest[off..off + take].copy_from_slice(&self.buf[self.pos..self.pos + take]);
self.pos += take;
off += take;
}
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> core::result::Result<(), RandError> {
self.fill_bytes(dest);
Ok(())
}
}
impl CryptoRng for DetRng {}
impl Drop for DetRng {
fn drop(&mut self) {
self.v.zeroize();
self.buf.zeroize();
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn draw(domain: &[u8], secret: &[u8], message: &[u8], n: usize) -> Vec<u8> {
let mut rng = DetRng::new(domain, secret, message).unwrap();
let mut out = vec![0u8; n];
rng.fill_bytes(&mut out);
out
}
#[test]
fn same_inputs_give_same_stream() {
assert_eq!(
draw(b"dom", b"key", b"msg", 200),
draw(b"dom", b"key", b"msg", 200)
);
}
#[test]
fn each_input_separately_changes_the_stream() {
let base = draw(b"dom", b"key", b"msg", 64);
assert_ne!(base, draw(b"dom", b"key", b"msg2", 64), "message is bound");
assert_ne!(base, draw(b"dom", b"key2", b"msg", 64), "secret is bound");
assert_ne!(base, draw(b"dom2", b"key", b"msg", 64), "domain is bound");
}
#[test]
fn draws_are_consistent_across_block_boundaries() {
let whole = draw(b"dom", b"seed", b"m", 130);
let mut split = DetRng::new(b"dom", b"seed", b"m").unwrap();
let mut first = [0u8; 50];
let mut second = [0u8; 80];
split.fill_bytes(&mut first);
split.fill_bytes(&mut second);
assert_eq!(&whole[..50], &first[..]);
assert_eq!(&whole[50..], &second[..]);
}
#[test]
fn consecutive_blocks_differ() {
let stream = draw(b"dom", b"key", b"msg", LEN * 3);
assert_ne!(&stream[..LEN], &stream[LEN..LEN * 2]);
assert_ne!(&stream[LEN..LEN * 2], &stream[LEN * 2..]);
assert!(
stream.iter().any(|&b| b != 0),
"stream must not be all zero"
);
}
#[test]
fn next_u32_and_u64_match_the_equivalent_fill_bytes_draw() {
let mut rng = DetRng::new(b"dom", b"key", b"msg").unwrap();
let a = rng.next_u32();
let b = rng.next_u64();
let raw = draw(b"dom", b"key", b"msg", 12);
assert_eq!(a, u32::from_le_bytes(raw[..4].try_into().unwrap()));
assert_eq!(b, u64::from_le_bytes(raw[4..12].try_into().unwrap()));
}
#[test]
fn debug_redacts_state() {
let rng = DetRng::new(b"dom", b"key", b"msg").unwrap();
let rendered = format!("{:?}", rng);
assert!(rendered.contains("<redacted>"));
assert_eq!(rendered.matches("<redacted>").count(), 3);
}
}