Skip to content

Commit

Permalink
feat: IETF-compliant ChaCha
Browse files Browse the repository at this point in the history
  • Loading branch information
aspen committed Dec 3, 2020
1 parent b4e0743 commit dfceab0
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 87 deletions.
37 changes: 20 additions & 17 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -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
]
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -16,6 +16,15 @@ fn main() {
println!("Random number: {}", rng.generate::<u64>());
}
```
### 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::<u64>());
}
```
### Generating a number in a range
```rust
use nanorand::{RNG, WyRand};
Expand Down Expand Up @@ -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.
Expand Down
139 changes: 106 additions & 33 deletions src/crypto/chacha.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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);
Expand All @@ -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<u8> = hex::decode($keystream_hex).unwrap();

let mut state = chacha_init(key, nonce);
let mut keystream: Vec<u8> = 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"
);
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u64>());
//! }
//! ```
Expand Down
34 changes: 21 additions & 13 deletions src/rand/chacha.rs
Original file line number Diff line number Diff line change
@@ -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],
Expand All @@ -17,16 +20,16 @@ impl ChaCha {
key.copy_from_slice(&crate::entropy::entropy_from_system(
core::mem::size_of::<u8>() * 32,
));
let mut nonce: [u8; core::mem::size_of::<u8>() * 16] = Default::default();
let mut nonce: [u8; core::mem::size_of::<u8>() * 8] = Default::default();
nonce.copy_from_slice(&crate::entropy::entropy_from_system(
core::mem::size_of::<u8>() * 16,
core::mem::size_of::<u8>() * 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 }
}
Expand All @@ -38,9 +41,9 @@ impl Default for ChaCha {
key.copy_from_slice(&crate::entropy::entropy_from_system(
core::mem::size_of::<u8>() * 32,
));
let mut nonce: [u8; core::mem::size_of::<u8>() * 16] = Default::default();
let mut nonce: [u8; core::mem::size_of::<u8>() * 8] = Default::default();
nonce.copy_from_slice(&crate::entropy::entropy_from_system(
core::mem::size_of::<u8>() * 16,
core::mem::size_of::<u8>() * 8,
));
let state = chacha::chacha_init(key, nonce);
Self { state, rounds: 20 }
Expand All @@ -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
}

Expand All @@ -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);
}
}
Expand All @@ -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)
}
}
Loading

0 comments on commit dfceab0

Please sign in to comment.