use chacha20::cipher::{KeyIvInit, StreamCipher};
use chacha20::ChaCha20;
use zeroize::ZeroizeOnDrop;
const PERMUTATION_NONCE: [u8; 12] = [0u8; 12];
const KEYSTREAM_BUFFER_BYTES: usize = 4096;
const DRAW_BYTES: usize = 8;
#[derive(ZeroizeOnDrop)]
struct KeystreamReader {
#[zeroize(skip)]
cipher: ChaCha20,
buffer: [u8; KEYSTREAM_BUFFER_BYTES],
cursor: usize,
}
impl KeystreamReader {
fn new(seed: &[u8; 32]) -> Self {
Self {
cipher: ChaCha20::new(seed.into(), (&PERMUTATION_NONCE).into()),
buffer: [0u8; KEYSTREAM_BUFFER_BYTES],
cursor: KEYSTREAM_BUFFER_BYTES,
}
}
fn next_u64(&mut self) -> u64 {
if self.cursor + DRAW_BYTES > self.buffer.len() {
self.refill();
}
let mut word = [0u8; DRAW_BYTES];
if let Some(bytes) = self.buffer.get(self.cursor..self.cursor + DRAW_BYTES) {
word.copy_from_slice(bytes);
self.cursor += DRAW_BYTES;
}
u64::from_le_bytes(word)
}
fn refill(&mut self) {
self.buffer = [0u8; KEYSTREAM_BUFFER_BYTES];
self.cipher.apply_keystream(&mut self.buffer);
self.cursor = 0;
}
}
pub(crate) fn generate_pixel_permutation(n_pixels: usize, stc_seed: &[u8; 32]) -> Vec<usize> {
let mut permutation: Vec<usize> = (0..n_pixels).collect();
if n_pixels < 2 {
return permutation;
}
let mut keystream = KeystreamReader::new(stc_seed);
for index in (1..n_pixels).rev() {
let bound = index as u64 + 1;
let ceiling = u64::MAX - u64::MAX % bound;
let draw = loop {
let value = keystream.next_u64();
if value < ceiling {
break value;
}
};
permutation.swap(index, (draw % bound) as usize);
}
permutation
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
const SEED: [u8; 32] = [0x3Cu8; 32];
const POSITIONS: usize = 4_096;
#[test]
fn the_shuffle_is_a_bijection() {
let permutation = generate_pixel_permutation(POSITIONS, &SEED);
assert_eq!(permutation.len(), POSITIONS);
assert_eq!(
permutation
.iter()
.copied()
.collect::<HashSet<usize>>()
.len(),
POSITIONS
);
assert!(permutation.iter().all(|&index| index < POSITIONS));
}
#[test]
fn the_shuffle_is_reproducible() {
assert_eq!(
generate_pixel_permutation(POSITIONS, &SEED),
generate_pixel_permutation(POSITIONS, &SEED)
);
}
#[test]
fn a_different_seed_visits_the_positions_differently() {
let other = generate_pixel_permutation(POSITIONS, &[0xC3u8; 32]);
assert_ne!(generate_pixel_permutation(POSITIONS, &SEED), other);
}
#[test]
fn the_shuffle_moves_almost_every_position() {
let permutation = generate_pixel_permutation(POSITIONS, &SEED);
let fixed = permutation
.iter()
.enumerate()
.filter(|(index, &value)| *index == value)
.count();
assert!(
fixed < POSITIONS / 100,
"{fixed} positions were left in place"
);
}
#[test]
fn nothing_is_shuffled_below_two_positions() {
assert!(generate_pixel_permutation(0, &SEED).is_empty());
assert_eq!(generate_pixel_permutation(1, &SEED), vec![0]);
}
}