Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions chacha20/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ rand_core-compatible RNGs based on those ciphers.
cfg-if = "1"
cipher = { version = "0.5.0-rc.3", optional = true, features = ["stream-wrapper"] }
rand_core = { version = "0.10.0-rc-3", optional = true, default-features = false }
serde = { version = "1.0", features = ["derive"], optional = true }

# `zeroize` is an explicit dependency because this crate may be used without the `cipher` crate
zeroize = { version = "1.8.1", optional = true, default-features = false }
Expand All @@ -34,11 +35,13 @@ cipher = { version = "0.5.0-rc.3", features = ["dev"] }
hex-literal = "1"
proptest = "1"
rand_chacha = "0.9"
serde_json = "1.0.120"

[features]
default = ["cipher"]
legacy = ["cipher"]
rng = ["rand_core"]
serde = ["dep:serde"]
xchacha = ["cipher"]

[package.metadata.docs.rs]
Expand Down
7 changes: 7 additions & 0 deletions chacha20/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fn main() {
if cfg!(feature = "serde") {
println!(
"cargo:warning=`serde` feature is enabled. Serializing CSPRNG states can leave unzeroizable copies of the seed in memory."
);
}
}
74 changes: 74 additions & 0 deletions chacha20/src/rng.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ use rand_core::{
#[cfg(feature = "zeroize")]
use zeroize::{Zeroize, ZeroizeOnDrop};

#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::{
ChaChaCore, R8, R12, R20, Rounds, backends,
variants::{Legacy, Variant},
Expand All @@ -29,6 +32,7 @@ pub(crate) const BLOCK_WORDS: u8 = 16;
/// The seed for ChaCha20. Implements ZeroizeOnDrop when the
/// zeroize feature is enabled.
#[derive(PartialEq, Eq, Default, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Seed([u8; 32]);

impl AsRef<[u8; 32]> for Seed {
Expand Down Expand Up @@ -471,11 +475,34 @@ macro_rules! impl_chacha_rng {
}
}

#[cfg(feature = "serde")]
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 = "serde")]
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 = "serde")]
use serde::{Deserialize, Serialize};

// 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 = "serde", derive(Serialize, Deserialize))]
pub(crate) struct $ChaChaXRng {
seed: crate::rng::Seed,
stream: u64,
Expand Down Expand Up @@ -540,6 +567,53 @@ pub(crate) mod tests {
26, 27, 28, 29, 30, 31, 32,
];

#[cfg(feature = "serde")]
#[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 = "serde")]
#[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_rng_output() {
let mut rng = ChaCha20Rng::from_seed(KEY);
Expand Down
Loading