Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

serde support for ChaCha #1124

Merged
merged 7 commits into from
May 22, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion rand_chacha/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -17,8 +17,14 @@ 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 }

[dev-dependencies]
# Only to test serde1
serde_json = "1.0"

[features]
default = ["std"]
std = ["ppv-lite86/std"]
simd = [] # deprecated
serde1 = ["serde"]
137 changes: 131 additions & 6 deletions rand_chacha/src/chacha.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -69,7 +71,7 @@ impl<T> fmt::Debug for Array64<T> {
}

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 {
Expand Down Expand Up @@ -245,6 +247,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 {}
Expand All @@ -259,24 +279,129 @@ 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 {}

#[cfg(feature = "serde1")]
impl Serialize for $ChaChaXRng {
fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
where S: Serializer {
$abst::$ChaChaXRng::from(self).serialize(s)
}
}
#[cfg(feature = "serde1")]
impl<'de> Deserialize<'de> for $ChaChaXRng {
fn deserialize<D>(d: D) -> Result<Self, D::Error> 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,
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 {
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 = 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 = 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);
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());
}

// 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 = [
Expand Down
20 changes: 14 additions & 6 deletions rand_chacha/src/guts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,9 @@ impl ChaCha {
get_stream_param(self, param)
}

/// 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]
#[inline(always)]
pub fn get_seed(&self) -> [u8; 32] {
get_seed(self)
}
}

Expand Down Expand Up @@ -205,6 +202,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)
Expand Down