From dfceab0597aadf2d8c4201387d027a935315e80c Mon Sep 17 00:00:00 2001 From: aspen Date: Thu, 3 Dec 2020 12:19:03 -0500 Subject: [PATCH] feat: IETF-compliant ChaCha --- .github/workflows/tests.yml | 37 +++++----- Cargo.toml | 2 +- README.md | 20 ++++-- src/crypto/chacha.rs | 139 +++++++++++++++++++++++++++--------- src/lib.rs | 2 +- src/rand/chacha.rs | 34 +++++---- src/rand/pcg64.rs | 10 +-- src/rand/wyrand.rs | 10 +-- src/tls.rs | 15 ++-- 9 files changed, 182 insertions(+), 87 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a55da3d..417fa0b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,31 +1,34 @@ -name: "Tests" +name: "Run Tests" on: [push, pull_request] jobs: tests: - name: "Test with Rust ${{ matrix.rust.version }} on ${{ matrix.os }} with features ${{ matrix.rust.features }}" - runs-on: ${{ matrix.os }} + name: Test on ${{ matrix.target }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: "Install ${{ matrix.rust.version }} Rust toolchain" + - name: Install stable Rust toolchain uses: actions-rs/toolchain@v1 with: override: true profile: minimal - toolchain: "${{ matrix.rust.version }}" - - name: Run 'cargo test' + toolchain: stable + - name: Run 'cross test' uses: actions-rs/cargo@v1 with: - args: "--features ${{matrix.rust.features}}" + use-cross: true command: test + args: --all-features --target ${{ matrix.target }} strategy: matrix: - rust: - - version: stable - features: std,wyrand,pcg64,getrandom - - version: nightly - features: std,wyrand,pcg64,chacha,getrandom - - version: stable - features: std,wyrand,pcg64 - - version: nightly - features: std,wyrand,pcg64,chacha - os: [windows-latest, ubuntu-latest, macos-latest] + target: [ + x86_64-unknown-linux-gnu + aarch64-unknown-linux-gnu + arm-unknown-linux-gnueabihf + armv5te-unknown-linux-gnueabi + armv7-unknown-linux-gnueabihf + mips64-unknown-linux-gnuabi64 + mips64el-unknown-linux-gnuabi64 + powerpc-unknown-linux-gnu + powerpc64le-unknown-linux-gnu + riscv64gc-unknown-linux-gnu + ] diff --git a/Cargo.toml b/Cargo.toml index 77aeb91..6fac988 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" license = "Zlib" [features] -default = ["std", "tls", "wyrand", "pcg64"] +default = ["std", "tls", "wyrand", "pcg64", "chacha"] std = [] tls = ["std", "wyrand"] wyrand = [] diff --git a/README.md b/README.md index 2b0a239..cde715d 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ # nanorand -Current version: **0.4.4** +Current version: **0.5.0** A library meant for fast, random number generation with quick compile time, and minimal dependencies. ## Examples -### Generating a number +### Generating a number with an initialized RNG ```rust use nanorand::{RNG, WyRand}; @@ -16,6 +16,15 @@ fn main() { println!("Random number: {}", rng.generate::()); } ``` +### Generating a number with a thread-local RNG +```rust +use nanorand::RNG; + +fn main() { + let mut rng = nanorand::tls_rng(); + println!("Random number: {}", rng.generate::()); +} +``` ### Generating a number in a range ```rust use nanorand::{RNG, WyRand}; @@ -52,15 +61,16 @@ ChaCha|[nanohash::ChaCha](rand/chacha/struct.ChaCha.html)|512 bits (`[u32; 16]`) ### Entropy Sources -* Unix-like (Linux, Android, macOS, iOS, FreeBSD, OpenBSD) - first `/dev/urandom`, else `/dev/random`, else system time. (`#[forbid(unsafe_code)]`) -* Windows - `BCryptGenRandom` with system-preferred RNG. (`#[deny(unsafe_code)]`) +* Unix-like (Linux, Android, macOS, iOS, FreeBSD, OpenBSD) - first `/dev/urandom`, else `/dev/random`, else system time. +* Windows - `BCryptGenRandom` with system-preferred RNG. ### Feature Flags * `std` (default) - Enables Rust `std` lib features, such as seeding from OS entropy sources. +* `tls` (default) - Enables a thread-local WyRand RNG (see below). Requires `tls` to be enabled. * `wyrand` (default) - Enable the [wyrand](rand/wyrand/struct.WyRand.html) RNG. * `pcg64` (default) - Enable the [Pcg64](rand/pcg64/struct.Pcg64.html) RNG. -* `chacha` (**Nightly-only**) - Enable the [ChaCha](rand/chacha/struct.ChaCha.html) RNG. +* `chacha` - Enable the [ChaCha](rand/chacha/struct.ChaCha.html) RNG. Requires Rust 1.47 or later. * `rdseed` - On x86/x86_64 platforms, the `rdseed` intrinsic will be used when OS entropy isn't available. * `zeroize` - Implement the [Zeroize](https://crates.io/crates/zeroize) trait for all RNGs. * `getrandom` - Use the [`getrandom`](https://crates.io/crates/getrandom) crate as an entropy source. diff --git a/src/crypto/chacha.rs b/src/crypto/chacha.rs index 78f09e3..ea9a1fa 100644 --- a/src/crypto/chacha.rs +++ b/src/crypto/chacha.rs @@ -1,31 +1,28 @@ -const CHACHA_TAU: &[u8] = b"expand 16-byte k"; +const CHACHA_TAU: &[u8] = b"expand 32-byte k"; -fn chacha_rotl(a: u32, b: u32) -> u32 { - (a << b) | (a >> (32 - b)) -} +fn chacha_quarter_round(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) { + state[a] = state[a].wrapping_add(state[b]); + state[d] ^= state[a]; + state[d] = state[d].rotate_left(16); + + state[c] = state[c].wrapping_add(state[d]); + state[b] ^= state[c]; + state[b] = state[b].rotate_left(12); + + state[a] = state[a].wrapping_add(state[b]); + state[d] ^= state[a]; + state[d] = state[d].rotate_left(8); -fn chacha_quarter_round(x: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) { - x[a] += x[b]; - x[d] ^= x[a]; - x[d] = chacha_rotl(x[d], 16); - x[c] += x[d]; - x[b] ^= x[c]; - x[b] = chacha_rotl(x[b], 12); - x[a] += x[b]; - x[d] ^= x[a]; - x[d] = chacha_rotl(x[d], 8); - x[c] += x[d]; - x[b] ^= x[c]; - x[b] = chacha_rotl(x[b], 7); + state[c] = state[c].wrapping_add(state[d]); + state[b] ^= state[c]; + state[b] = state[b].rotate_left(7); } -fn chacha_pack(x: &[u8], a: usize) -> u32 { - let mut res = 0_u32; - res |= ((x[a] as u32) << 0 * 8) as u32; - res |= ((x[a + 1] as u32) << 1 * 8) as u32; - res |= ((x[a + 2] as u32) << 2 * 8) as u32; - res |= ((x[a + 3] as u32) << 3 * 8) as u32; - res +fn chacha_pack(unpacked: &[u8], idx: usize) -> u32 { + (unpacked[idx] as u32) + | ((unpacked[idx + 1] as u32) << 8) + | ((unpacked[idx + 2] as u32) << 16) + | ((unpacked[idx + 3] as u32) << 24) } /// Do one ChaCha round on the input data. @@ -42,18 +39,18 @@ pub fn chacha_block(rounds: u8, input: [u32; 16]) -> [u32; 16] { chacha_quarter_round(&mut x, 3, 7, 11, 15); // Even rounds chacha_quarter_round(&mut x, 0, 5, 10, 15); - chacha_quarter_round(&mut x, 1, 6, 11, 14); + chacha_quarter_round(&mut x, 1, 6, 11, 12); chacha_quarter_round(&mut x, 2, 7, 8, 13); - chacha_quarter_round(&mut x, 3, 4, 9, 12); + chacha_quarter_round(&mut x, 3, 4, 9, 14); } x.iter_mut() - .zip(input.iter().copied()) - .for_each(|(l, r)| *l = l.wrapping_add(r)); + .zip(input.iter()) + .for_each(|(l, r)| *l = l.wrapping_add(*r)); x } -/// Initialize the ChaCha internal state, with a 32-byte kit. -pub fn chacha_init(key: [u8; 32], nonce: [u8; 16]) -> [u32; 16] { +/// Initialize the ChaCha internal state, with a 256-bit key and 64-bit nonce. +pub fn chacha_init(key: [u8; 32], nonce: [u8; 8]) -> [u32; 16] { let mut state = [0u32; 16]; state[0] = chacha_pack(CHACHA_TAU, 0); state[1] = chacha_pack(CHACHA_TAU, 4); @@ -69,9 +66,85 @@ pub fn chacha_init(key: [u8; 32], nonce: [u8; 16]) -> [u32; 16] { state[10] = chacha_pack(&key, 24); state[11] = chacha_pack(&key, 28); + // 64-bit counter state[12] = 0; - state[13] = chacha_pack(&nonce, 0); - state[14] = chacha_pack(&nonce, 4); - state[15] = chacha_pack(&nonce, 8); + state[13] = 0; + // Nonce + state[14] = chacha_pack(&nonce, 0); + state[15] = chacha_pack(&nonce, 4); state } + +/// Increment the 64-bit counter of the internal ChaCha20 state by 1. +/// Returns `false` if it overflows, `true` otherwise. +pub fn chacha_increment_counter(state: &mut [u32; 16]) -> bool { + let counter = ((state[13] as u64) << 32) | (state[12] as u64); + match counter.checked_add(1) { + Some(new_counter) => { + state[12] = (new_counter & 0xFFFFFFFF) as u32; + state[13] = ((counter >> 32) & 0xFFFFFFFF) as u32; + true + } + None => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::convert::TryInto; + + macro_rules! ietf_test_vector { + ($key_hex: tt, $nonce_hex: tt, $keystream_hex: tt) => { + let key: [u8; 32] = hex::decode($key_hex).unwrap().try_into().unwrap(); + let nonce: [u8; 8] = hex::decode($nonce_hex).unwrap().try_into().unwrap(); + let expected_keystream: Vec = hex::decode($keystream_hex).unwrap(); + + let mut state = chacha_init(key, nonce); + let mut keystream: Vec = Vec::with_capacity(expected_keystream.len()); + + while expected_keystream.len() > keystream.len() { + chacha_block(20, state) + .iter() + .for_each(|packed| keystream.extend_from_slice(&packed.to_le_bytes())); + chacha_increment_counter(&mut state); + } + keystream.resize(expected_keystream.len(), 0); + + assert_eq!(keystream, expected_keystream); + }; + } + + #[test] + fn test_ietf_chacha20_test_vectors() { + ietf_test_vector!( + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000", + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586" + ); + + ietf_test_vector!( + "0000000000000000000000000000000000000000000000000000000000000001", + "0000000000000000", + "4540f05a9f1fb296d7736e7b208e3c96eb4fe1834688d2604f450952ed432d41bbe2a0b6ea7566d2a5d1e7e20d42af2c53d792b1c43fea817e9ad275ae546963" + ); + + ietf_test_vector!( + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000001", + "de9cba7bf3d69ef5e786dc63973f653a0b49e015adbff7134fcb7df137821031e85a050278a7084527214f73efc7fa5b5277062eb7a0433e445f41e3" + ); + + ietf_test_vector!( + "0000000000000000000000000000000000000000000000000000000000000000", + "0100000000000000", + "ef3fdfd6c61578fbf5cf35bd3dd33b8009631634d21e42ac33960bd138e50d32111e4caf237ee53ca8ad6426194a88545ddc497a0b466e7d6bbdb0041b2f586b" + ); + + ietf_test_vector!( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "0001020304050607", + "f798a189f195e66982105ffb640bb7757f579da31602fc93ec01ac56f85ac3c134a4547b733b46413042c9440049176905d3be59ea1c53f15916155c2be8241a38008b9a26bc35941e2444177c8ade6689de95264986d95889fb60e84629c9bd9a5acb1cc118be563eb9b3a4a472f82e09a7e778492b562ef7130e88dfe031c79db9d4f7c7a899151b9a475032b63fc385245fe054e3dd5a97a5f576fe064025d3ce042c566ab2c507b138db853e3d6959660996546cc9c4a6eafdc777c040d70eaf46f76dad3979e5c5360c3317166a1c894c94a371876a94df7628fe4eaaf2ccb27d5aaae0ad7ad0f9d4b6ad3b54098746d4524d38407a6deb3ab78fab78c9" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index a213a97..6f48a64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ //! use nanorand::RNG; //! //! fn main() { -//! let mut rng = nanorand::tls_rng(); +//! let mut rng = nanorand::tls_rng(); //! println!("Random number: {}", rng.generate::()); //! } //! ``` diff --git a/src/rand/chacha.rs b/src/rand/chacha.rs index bf4dc40..fc0fe89 100644 --- a/src/rand/chacha.rs +++ b/src/rand/chacha.rs @@ -1,9 +1,12 @@ use crate::{crypto::chacha, RNG}; +use core::fmt::{self, Display, Formatter}; +#[cfg(feature = "zeroize")] +use zeroize::Zeroize; /// An instance of the ChaCha random number generator. /// Seeded from the system entropy generator when available. /// **This generator _is theoretically_ cryptographically secure.** -#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] +#[cfg_attr(feature = "zeroize", derive(Zeroize))] #[cfg_attr(feature = "zeroize", zeroize(drop))] pub struct ChaCha { state: [u32; 16], @@ -17,16 +20,16 @@ impl ChaCha { key.copy_from_slice(&crate::entropy::entropy_from_system( core::mem::size_of::() * 32, )); - let mut nonce: [u8; core::mem::size_of::() * 16] = Default::default(); + let mut nonce: [u8; core::mem::size_of::() * 8] = Default::default(); nonce.copy_from_slice(&crate::entropy::entropy_from_system( - core::mem::size_of::() * 16, + core::mem::size_of::() * 8, )); let state = chacha::chacha_init(key, nonce); Self { rounds, state } } /// Create a new [`ChaCha`] instance, using the provided key and nonce. - pub fn new_key(rounds: u8, key: [u8; 32], nonce: [u8; 16]) -> Self { + pub fn new_key(rounds: u8, key: [u8; 32], nonce: [u8; 8]) -> Self { let state = chacha::chacha_init(key, nonce); Self { rounds, state } } @@ -38,9 +41,9 @@ impl Default for ChaCha { key.copy_from_slice(&crate::entropy::entropy_from_system( core::mem::size_of::() * 32, )); - let mut nonce: [u8; core::mem::size_of::() * 16] = Default::default(); + let mut nonce: [u8; core::mem::size_of::() * 8] = Default::default(); nonce.copy_from_slice(&crate::entropy::entropy_from_system( - core::mem::size_of::() * 16, + core::mem::size_of::() * 8, )); let state = chacha::chacha_init(key, nonce); Self { state, rounds: 20 } @@ -61,7 +64,13 @@ impl RNG for ChaCha { ret[n + 2] = x[2]; ret[n + 3] = x[3]; }); - self.state[12] += 1; + // Now, we're going to just increment our counter so we get an entirely new output next time. + // If the counter overflows, we just reseed entirely instead. + if !chacha::chacha_increment_counter(&mut self.state) { + let mut new_seed: [u8; 40] = [42u8; 40]; + new_seed.copy_from_slice(&crate::entropy::entropy_from_system(40)); + self.reseed(&new_seed); + } ret } @@ -70,12 +79,12 @@ impl RNG for ChaCha { } fn reseed(&mut self, new_seed: &[u8]) { - let mut seed = [42u8; 48]; + let mut seed = [42u8; 40]; seed.iter_mut().zip(new_seed).for_each(|(a, b)| *a = *b); let mut key = [0u8; 32]; - let mut nonce = [0u8; 16]; + let mut nonce = [0u8; 8]; key.copy_from_slice(&seed[..32]); - nonce.copy_from_slice(&seed[32..48]); + nonce.copy_from_slice(&seed[32..40]); self.state = chacha::chacha_init(key, nonce); } } @@ -89,9 +98,8 @@ impl Clone for ChaCha { } } -#[cfg(feature = "std")] -impl std::fmt::Display for ChaCha { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +impl Display for ChaCha { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "ChaCha ({:p}, {} rounds)", self, self.rounds) } } diff --git a/src/rand/pcg64.rs b/src/rand/pcg64.rs index 5bc3e15..d4039a0 100644 --- a/src/rand/pcg64.rs +++ b/src/rand/pcg64.rs @@ -1,13 +1,16 @@ // Based off Robert Kern's C implementation at https://github.com/rkern/pcg64/blob/master/pcg64.c use crate::RNG; +use core::fmt::{self, Display, Formatter}; +#[cfg(feature = "zeroize")] +use zeroize::Zeroize; const PCG_DEFAULT_MULTIPLIER_128: u128 = 47026247687942121848144207491837523525; /// An instance of the Pcg64 random number generator. /// Seeded from the system entropy generator when available. /// **This generator is _NOT_ cryptographically secure.** -#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] +#[cfg_attr(feature = "zeroize", derive(Zeroize))] #[cfg_attr(feature = "zeroize", zeroize(drop))] pub struct Pcg64 { seed: u128, @@ -105,9 +108,8 @@ impl Clone for Pcg64 { } } -#[cfg(feature = "std")] -impl std::fmt::Display for Pcg64 { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +impl Display for Pcg64 { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "Pcg64 ({:p})", self) } } diff --git a/src/rand/wyrand.rs b/src/rand/wyrand.rs index 6e986b1..9649c3b 100644 --- a/src/rand/wyrand.rs +++ b/src/rand/wyrand.rs @@ -1,11 +1,14 @@ // Based off lemire's wyrand C++ code at https://github.com/lemire/testingRNG/blob/master/source/wyrand.h use crate::RNG; +use core::fmt::{self, Display, Formatter}; +#[cfg(feature = "zeroize")] +use zeroize::Zeroize; /// An instance of the WyRand random number generator. /// Seeded from the system entropy generator when available. /// **This generator is _NOT_ cryptographically secure.** -#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] +#[cfg_attr(feature = "zeroize", derive(Zeroize))] #[cfg_attr(feature = "zeroize", zeroize(drop))] pub struct WyRand { seed: u64, @@ -75,9 +78,8 @@ impl Clone for WyRand { } } -#[cfg(feature = "std")] -impl std::fmt::Display for WyRand { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +impl Display for WyRand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "WyRand ({:p})", self) } } diff --git a/src/tls.rs b/src/tls.rs index 374be69..097f2e4 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -33,20 +33,17 @@ impl DerefMut for TlsWyRand { /// ```rust /// use nanorand::RNG; /// -/// fn main() { -/// let mut rng = nanorand::tls_rng(); -/// println!("Random number: {}", rng.generate::()); -/// } +/// let mut rng = nanorand::tls_rng(); +/// println!("Random number: {}", rng.generate::()); /// ``` /// This cannot be passed to another thread, as something like this will fail to compile: /// ```compile_fail /// use nanorand::RNG; /// -/// fn main() { -/// let mut rng = nanorand::tls_rng(); -/// std::thread::spawn(move || { -/// println!("Random number: {}", rng.generate::()); -/// }); +/// let mut rng = nanorand::tls_rng(); +/// std::thread::spawn(move || { +/// println!("Random number: {}", rng.generate::()); +/// }); /// } /// ``` pub fn tls_rng() -> TlsWyRand {