From 7f3108db62090384eeeaf746f65d6961059932ce Mon Sep 17 00:00:00 2001 From: Kaz Wesley Date: Mon, 10 May 2021 14:30:32 -0700 Subject: [PATCH 1/7] chacha: some new accessors The new getters correspond to existing setters. --- rand_chacha/Cargo.toml | 2 +- rand_chacha/src/guts.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/rand_chacha/Cargo.toml b/rand_chacha/Cargo.toml index 0a653113d85..46facbd545e 100644 --- a/rand_chacha/Cargo.toml +++ b/rand_chacha/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rand_chacha" -version = "0.3.0" +version = "0.3.1" authors = ["The Rand Project Developers", "The Rust Project Developers", "The CryptoCorrosion Contributors"] license = "MIT OR Apache-2.0" readme = "README.md" diff --git a/rand_chacha/src/guts.rs b/rand_chacha/src/guts.rs index 27ff957a92c..7f837246cf1 100644 --- a/rand_chacha/src/guts.rs +++ b/rand_chacha/src/guts.rs @@ -92,6 +92,11 @@ impl ChaCha { get_stream_param(self, param) } + #[inline(always)] + pub fn get_seed(&self) -> [u8; 32] { + get_seed(self) + } + /// Return whether rhs is equal in all parameters except current 64-bit position. #[inline] pub fn stream64_eq(&self, rhs: &Self) -> bool { @@ -205,6 +210,17 @@ dispatch_light128!(m, Mach, { } }); +dispatch_light128!(m, Mach, { + fn get_seed(state: &ChaCha) -> [u8; 32] { + let b: Mach::u32x4 = m.unpack(state.b); + let c: Mach::u32x4 = m.unpack(state.c); + let mut key = [0u8; 32]; + b.write_le(&mut key[..16]); + c.write_le(&mut key[16..]); + key + } +}); + fn read_u32le(xs: &[u8]) -> u32 { assert_eq!(xs.len(), 4); u32::from(xs[0]) | (u32::from(xs[1]) << 8) | (u32::from(xs[2]) << 16) | (u32::from(xs[3]) << 24) From d22607b65805314ce9ba6070c6f3d75128dcb6d3 Mon Sep 17 00:00:00 2001 From: Kaz Wesley Date: Mon, 10 May 2021 14:38:27 -0700 Subject: [PATCH 2/7] chacha: derive eq from a representation of the abstract state Equivalent to the previous implementation, but clearer in intent. The representable-abstract-state framework introduced here is general-purpose and can also be used e.g. for serialization. --- rand_chacha/src/chacha.rs | 66 +++++++++++++++++++++++++++++++++++---- rand_chacha/src/guts.rs | 8 ----- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/rand_chacha/src/chacha.rs b/rand_chacha/src/chacha.rs index a3de5ed4267..b14475d8784 100644 --- a/rand_chacha/src/chacha.rs +++ b/rand_chacha/src/chacha.rs @@ -69,7 +69,7 @@ impl fmt::Debug for Array64 { } macro_rules! chacha_impl { - ($ChaChaXCore:ident, $ChaChaXRng:ident, $rounds:expr, $doc:expr) => { + ($ChaChaXCore:ident, $ChaChaXRng:ident, $rounds:expr, $doc:expr, $abst:ident) => { #[doc=$doc] #[derive(Clone, PartialEq, Eq)] pub struct $ChaChaXCore { @@ -245,6 +245,24 @@ macro_rules! chacha_impl { self.set_word_pos(wp); } } + + /// Get the stream number. + #[inline] + pub fn get_stream(&self) -> u64 { + self.rng + .core + .state + .get_stream_param(STREAM_PARAM_NONCE) + } + + /// Get the seed. + #[inline] + pub fn get_seed(&self) -> [u8; 32] { + self.rng + .core + .state + .get_seed() + } } impl CryptoRng for $ChaChaXRng {} @@ -259,17 +277,53 @@ macro_rules! chacha_impl { impl PartialEq<$ChaChaXRng> for $ChaChaXRng { fn eq(&self, rhs: &$ChaChaXRng) -> bool { - self.rng.core.state.stream64_eq(&rhs.rng.core.state) - && self.get_word_pos() == rhs.get_word_pos() + let a: $abst::$ChaChaXRng = self.into(); + let b: $abst::$ChaChaXRng = rhs.into(); + a == b } } impl Eq for $ChaChaXRng {} + + mod $abst { + // The abstract state of a ChaCha stream, independent of implementation choices. The + // comparison and serialization of this object is considered a semver-covered part of + // the API. + #[derive(Debug, PartialEq, Eq)] + pub(crate) struct $ChaChaXRng { + seed: [u8; 32], + stream: u64, + word_pos: u128, + } + + impl From<&super::$ChaChaXRng> for $ChaChaXRng { + // Forget all information about the input except what is necessary to determine the + // outputs of any sequence of pub API calls. + fn from(r: &super::$ChaChaXRng) -> Self { + Self { + seed: r.get_seed(), + stream: r.get_stream(), + word_pos: r.get_word_pos(), + } + } + } + + impl From<&$ChaChaXRng> for super::$ChaChaXRng { + // Construct one of the possible concrete RNGs realizing an abstract state. + fn from(a: &$ChaChaXRng) -> Self { + use rand_core::SeedableRng; + let mut r = Self::from_seed(a.seed); + r.set_stream(a.stream); + r.set_word_pos(a.word_pos); + r + } + } + } } } -chacha_impl!(ChaCha20Core, ChaCha20Rng, 10, "ChaCha with 20 rounds"); -chacha_impl!(ChaCha12Core, ChaCha12Rng, 6, "ChaCha with 12 rounds"); -chacha_impl!(ChaCha8Core, ChaCha8Rng, 4, "ChaCha with 8 rounds"); +chacha_impl!(ChaCha20Core, ChaCha20Rng, 10, "ChaCha with 20 rounds", abstract20); +chacha_impl!(ChaCha12Core, ChaCha12Rng, 6, "ChaCha with 12 rounds", abstract12); +chacha_impl!(ChaCha8Core, ChaCha8Rng, 4, "ChaCha with 8 rounds", abstract8); #[cfg(test)] mod test { diff --git a/rand_chacha/src/guts.rs b/rand_chacha/src/guts.rs index 7f837246cf1..992d1d0aadf 100644 --- a/rand_chacha/src/guts.rs +++ b/rand_chacha/src/guts.rs @@ -96,14 +96,6 @@ impl ChaCha { pub fn get_seed(&self) -> [u8; 32] { get_seed(self) } - - /// Return whether rhs is equal in all parameters except current 64-bit position. - #[inline] - pub fn stream64_eq(&self, rhs: &Self) -> bool { - let self_d: [u32; 4] = self.d.into(); - let rhs_d: [u32; 4] = rhs.d.into(); - self.b == rhs.b && self.c == rhs.c && self_d[3] == rhs_d[3] && self_d[2] == rhs_d[2] - } } #[allow(clippy::many_single_char_names)] From 22029be3e306f4cbea5d3b71bdd1ef64cfafec13 Mon Sep 17 00:00:00 2001 From: Kaz Wesley Date: Mon, 10 May 2021 14:29:23 -0700 Subject: [PATCH 3/7] chacha: optional serde The format is semver-stable. --- rand_chacha/Cargo.toml | 2 ++ rand_chacha/src/chacha.rs | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/rand_chacha/Cargo.toml b/rand_chacha/Cargo.toml index 46facbd545e..3d941af3cf3 100644 --- a/rand_chacha/Cargo.toml +++ b/rand_chacha/Cargo.toml @@ -17,8 +17,10 @@ edition = "2018" [dependencies] rand_core = { path = "../rand_core", version = "0.6.0" } ppv-lite86 = { version = "0.2.8", default-features = false, features = ["simd"] } +serde = { version = "1.0", features = ["derive"], optional = true } [features] default = ["std"] std = ["ppv-lite86/std"] simd = [] # deprecated +serde1 = ["serde"] diff --git a/rand_chacha/src/chacha.rs b/rand_chacha/src/chacha.rs index b14475d8784..76738df28db 100644 --- a/rand_chacha/src/chacha.rs +++ b/rand_chacha/src/chacha.rs @@ -16,6 +16,8 @@ use crate::guts::ChaCha; use rand_core::block::{BlockRng, BlockRngCore}; use rand_core::{CryptoRng, Error, RngCore, SeedableRng}; +#[cfg(feature = "serde1")] use serde::{Serialize, Deserialize, Serializer, Deserializer}; + const STREAM_PARAM_NONCE: u32 = 1; const STREAM_PARAM_BLOCK: u32 = 0; @@ -284,11 +286,31 @@ macro_rules! chacha_impl { } impl Eq for $ChaChaXRng {} + #[cfg(feature = "serde1")] + impl Serialize for $ChaChaXRng { + fn serialize(&self, s: S) -> Result + where S: Serializer { + $abst::$ChaChaXRng::from(self).serialize(s) + } + } + #[cfg(feature = "serde1")] + impl<'de> Deserialize<'de> for $ChaChaXRng { + fn deserialize(d: D) -> Result where D: Deserializer<'de> { + $abst::$ChaChaXRng::deserialize(d).map(|x| Self::from(&x)) + } + } + mod $abst { + #[cfg(feature = "serde1")] use serde::{Serialize, Deserialize}; + // The abstract state of a ChaCha stream, independent of implementation choices. The // comparison and serialization of this object is considered a semver-covered part of // the API. #[derive(Debug, PartialEq, Eq)] + #[cfg_attr( + feature = "serde1", + derive(Serialize, Deserialize), + )] pub(crate) struct $ChaChaXRng { seed: [u8; 32], stream: u64, From d7a9e303149a36698ef33d048eda1cd45cc8ff62 Mon Sep 17 00:00:00 2001 From: Kaz Wesley Date: Mon, 10 May 2021 14:48:33 -0700 Subject: [PATCH 4/7] chacha: test for serde roundtrip --- rand_chacha/Cargo.toml | 4 ++++ rand_chacha/src/chacha.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/rand_chacha/Cargo.toml b/rand_chacha/Cargo.toml index 3d941af3cf3..56ecbf7e1a6 100644 --- a/rand_chacha/Cargo.toml +++ b/rand_chacha/Cargo.toml @@ -19,6 +19,10 @@ rand_core = { path = "../rand_core", version = "0.6.0" } ppv-lite86 = { version = "0.2.8", default-features = false, features = ["simd"] } serde = { version = "1.0", features = ["derive"], optional = true } +[dev-dependencies] +# Only to test serde1 +bincode = "1.2.1" + [features] default = ["std"] std = ["ppv-lite86/std"] diff --git a/rand_chacha/src/chacha.rs b/rand_chacha/src/chacha.rs index 76738df28db..de766c50787 100644 --- a/rand_chacha/src/chacha.rs +++ b/rand_chacha/src/chacha.rs @@ -351,8 +351,38 @@ chacha_impl!(ChaCha8Core, ChaCha8Rng, 4, "ChaCha with 8 rounds", abstract8); mod test { use rand_core::{RngCore, SeedableRng}; + #[cfg(feature = "serde1")] use super::{ChaCha20Rng, ChaCha12Rng, ChaCha8Rng}; + type ChaChaRng = super::ChaCha20Rng; + #[cfg(feature = "serde1")] + #[test] + fn test_chacha_serde_roundtrip() { + let seed = [ + 1, 0, 52, 0, 0, 0, 0, 0, 1, 0, 10, 0, 22, 32, 0, 0, 2, 0, 55, 49, 0, 11, 0, 0, 3, 0, 0, 0, 0, + 0, 2, 92, + ]; + let mut rng1 = ChaCha20Rng::from_seed(seed); + let mut rng2 = ChaCha12Rng::from_seed(seed); + let mut rng3 = ChaCha8Rng::from_seed(seed); + + let encoded1 = bincode::serialize(&rng1).unwrap(); + let encoded2 = bincode::serialize(&rng2).unwrap(); + let encoded3 = bincode::serialize(&rng3).unwrap(); + + let mut decoded1: ChaCha20Rng = bincode::deserialize(&encoded1).unwrap(); + let mut decoded2: ChaCha12Rng = bincode::deserialize(&encoded2).unwrap(); + let mut decoded3: ChaCha8Rng = bincode::deserialize(&encoded3).unwrap(); + + assert_eq!(rng1, decoded1); + assert_eq!(rng2, decoded2); + assert_eq!(rng3, decoded3); + + assert_eq!(rng1.next_u32(), decoded1.next_u32()); + assert_eq!(rng2.next_u32(), decoded2.next_u32()); + assert_eq!(rng3.next_u32(), decoded3.next_u32()); + } + #[test] fn test_chacha_construction() { let seed = [ From 31a1d56aeaa67cb5676d9df6e81df618550c502c Mon Sep 17 00:00:00 2001 From: Kaz Wesley Date: Mon, 10 May 2021 15:55:36 -0700 Subject: [PATCH 5/7] chacha: add a test for serde format stability --- rand_chacha/Cargo.toml | 2 +- rand_chacha/src/chacha.rs | 31 +++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/rand_chacha/Cargo.toml b/rand_chacha/Cargo.toml index 56ecbf7e1a6..53f9f591107 100644 --- a/rand_chacha/Cargo.toml +++ b/rand_chacha/Cargo.toml @@ -21,7 +21,7 @@ serde = { version = "1.0", features = ["derive"], optional = true } [dev-dependencies] # Only to test serde1 -bincode = "1.2.1" +serde_json = "1.0" [features] default = ["std"] diff --git a/rand_chacha/src/chacha.rs b/rand_chacha/src/chacha.rs index de766c50787..5c1c6742e97 100644 --- a/rand_chacha/src/chacha.rs +++ b/rand_chacha/src/chacha.rs @@ -366,13 +366,13 @@ mod test { let mut rng2 = ChaCha12Rng::from_seed(seed); let mut rng3 = ChaCha8Rng::from_seed(seed); - let encoded1 = bincode::serialize(&rng1).unwrap(); - let encoded2 = bincode::serialize(&rng2).unwrap(); - let encoded3 = bincode::serialize(&rng3).unwrap(); + let encoded1 = serde_json::to_string(&rng1).unwrap(); + let encoded2 = serde_json::to_string(&rng2).unwrap(); + let encoded3 = serde_json::to_string(&rng3).unwrap(); - let mut decoded1: ChaCha20Rng = bincode::deserialize(&encoded1).unwrap(); - let mut decoded2: ChaCha12Rng = bincode::deserialize(&encoded2).unwrap(); - let mut decoded3: ChaCha8Rng = bincode::deserialize(&encoded3).unwrap(); + let mut decoded1: ChaCha20Rng = serde_json::from_str(&encoded1).unwrap(); + let mut decoded2: ChaCha12Rng = serde_json::from_str(&encoded2).unwrap(); + let mut decoded3: ChaCha8Rng = serde_json::from_str(&encoded3).unwrap(); assert_eq!(rng1, decoded1); assert_eq!(rng2, decoded2); @@ -383,6 +383,25 @@ mod test { assert_eq!(rng3.next_u32(), decoded3.next_u32()); } + // This test validates that: + // 1. a hard-coded serialization demonstrating the format at time of initial release can still + // be deserialized to a ChaChaRng + // 2. re-serializing the resultant object produces exactly the original string + // + // Condition 2 is stronger than necessary: an equivalent serialization (e.g. with field order + // permuted, or whitespace differences) would also be admissible, but would fail this test. + // However testing for equivalence of serialized data is difficult, and there shouldn't be any + // reason we need to violate the stronger-than-needed condition, e.g. by changing the field + // definition order. + #[cfg(feature = "serde1")] + #[test] + fn test_chacha_serde_format_stability() { + let j = r#"{"seed":[4,8,15,16,23,42,4,8,15,16,23,42,4,8,15,16,23,42,4,8,15,16,23,42,4,8,15,16,23,42,4,8],"stream":27182818284,"word_pos":314159265359}"#; + let r: ChaChaRng = serde_json::from_str(&j).unwrap(); + let j1 = serde_json::to_string(&r).unwrap(); + assert_eq!(j, j1); + } + #[test] fn test_chacha_construction() { let seed = [ From eb8823642ffc9894bfa7b6181d89798106ea278c Mon Sep 17 00:00:00 2001 From: Kaz Wesley Date: Wed, 12 May 2021 09:49:11 -0700 Subject: [PATCH 6/7] chacha: update changelog --- rand_chacha/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rand_chacha/CHANGELOG.md b/rand_chacha/CHANGELOG.md index 8a073900765..e1ccf61aa5a 100644 --- a/rand_chacha/CHANGELOG.md +++ b/rand_chacha/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.1] - 2021-05-12 +- add getters corresponding to existing setters: `get_seed`, `get_state` +- add serde support, gated by the `serde1` feature + ## [0.3.0] - 2020-12-08 - Bump `rand_core` version to 0.6.0 - Bump MSRV to 1.36 (#1011) From 2f4bdfc2e115b71140b37403042acf10ccf8fc62 Mon Sep 17 00:00:00 2001 From: Kaz Wesley Date: Wed, 12 May 2021 09:12:52 -0700 Subject: [PATCH 7/7] chacha: hide numbered-param interface in guts --- rand_chacha/src/chacha.rs | 11 ++++------- rand_chacha/src/guts.rs | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/rand_chacha/src/chacha.rs b/rand_chacha/src/chacha.rs index 5c1c6742e97..50da81bfafe 100644 --- a/rand_chacha/src/chacha.rs +++ b/rand_chacha/src/chacha.rs @@ -18,9 +18,6 @@ use rand_core::{CryptoRng, Error, RngCore, SeedableRng}; #[cfg(feature = "serde1")] use serde::{Serialize, Deserialize, Serializer, Deserializer}; -const STREAM_PARAM_NONCE: u32 = 1; -const STREAM_PARAM_BLOCK: u32 = 0; - // NB. this must remain consistent with some currently hard-coded numbers in this module const BUF_BLOCKS: u8 = 4; // number of 32-bit words per ChaCha block (fixed by algorithm definition) @@ -196,7 +193,7 @@ macro_rules! chacha_impl { #[inline] pub fn get_word_pos(&self) -> u128 { let buf_start_block = { - let buf_end_block = self.rng.core.state.get_stream_param(STREAM_PARAM_BLOCK); + let buf_end_block = self.rng.core.state.get_block_pos(); u64::wrapping_sub(buf_end_block, BUF_BLOCKS.into()) }; let (buf_offset_blocks, block_offset_words) = { @@ -221,7 +218,7 @@ macro_rules! chacha_impl { self.rng .core .state - .set_stream_param(STREAM_PARAM_BLOCK, block); + .set_block_pos(block); self.rng.generate_and_set((word_offset % u128::from(BLOCK_WORDS)) as usize); } @@ -241,7 +238,7 @@ macro_rules! chacha_impl { self.rng .core .state - .set_stream_param(STREAM_PARAM_NONCE, stream); + .set_nonce(stream); if self.rng.index() != 64 { let wp = self.get_word_pos(); self.set_word_pos(wp); @@ -254,7 +251,7 @@ macro_rules! chacha_impl { self.rng .core .state - .get_stream_param(STREAM_PARAM_NONCE) + .get_nonce() } /// Get the seed. diff --git a/rand_chacha/src/guts.rs b/rand_chacha/src/guts.rs index 992d1d0aadf..cee8cf75d4c 100644 --- a/rand_chacha/src/guts.rs +++ b/rand_chacha/src/guts.rs @@ -21,6 +21,9 @@ const BUFBLOCKS: u64 = 1 << LOG2_BUFBLOCKS; pub(crate) const BUFSZ64: u64 = BLOCK64 * BUFBLOCKS; pub(crate) const BUFSZ: usize = BUFSZ64 as usize; +const STREAM_PARAM_NONCE: u32 = 1; +const STREAM_PARAM_BLOCK: u32 = 0; + #[derive(Clone, PartialEq, Eq)] pub struct ChaCha { pub(crate) b: vec128_storage, @@ -83,13 +86,23 @@ impl ChaCha { } #[inline(always)] - pub fn set_stream_param(&mut self, param: u32, value: u64) { - set_stream_param(self, param, value) + pub fn set_block_pos(&mut self, value: u64) { + set_stream_param(self, STREAM_PARAM_BLOCK, value) + } + + #[inline(always)] + pub fn get_block_pos(&self) -> u64 { + get_stream_param(self, STREAM_PARAM_BLOCK) + } + + #[inline(always)] + pub fn set_nonce(&mut self, value: u64) { + set_stream_param(self, STREAM_PARAM_NONCE, value) } #[inline(always)] - pub fn get_stream_param(&self, param: u32) -> u64 { - get_stream_param(self, param) + pub fn get_nonce(&self) -> u64 { + get_stream_param(self, STREAM_PARAM_NONCE) } #[inline(always)]