diff --git a/Cargo.lock b/Cargo.lock index aa21ea4c..f8c32318 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,8 @@ dependencies = [ "proptest", "rand_chacha", "rand_core 0.10.0-rc-3", + "serde", + "serde_json", "zeroize", ] @@ -163,6 +165,12 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "libc" version = "0.2.178" @@ -175,6 +183,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + [[package]] name = "num-traits" version = "0.2.19" @@ -348,6 +362,49 @@ dependencies = [ "hex-literal", ] +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "syn" version = "2.0.111" @@ -454,3 +511,9 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/chacha20/Cargo.toml b/chacha20/Cargo.toml index b72ac425..5db701d0 100644 --- a/chacha20/Cargo.toml +++ b/chacha20/Cargo.toml @@ -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 } @@ -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] diff --git a/chacha20/build.rs b/chacha20/build.rs new file mode 100644 index 00000000..2311c0c5 --- /dev/null +++ b/chacha20/build.rs @@ -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." + ); + } +} diff --git a/chacha20/src/rng.rs b/chacha20/src/rng.rs index eade9265..9d212839 100644 --- a/chacha20/src/rng.rs +++ b/chacha20/src/rng.rs @@ -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}, @@ -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 { @@ -471,11 +475,34 @@ macro_rules! impl_chacha_rng { } } + #[cfg(feature = "serde")] + impl Serialize for $ChaChaXRng { + fn serialize(&self, s: S) -> Result + where + S: Serializer, + { + $abst::$ChaChaXRng::from(self).serialize(s) + } + } + #[cfg(feature = "serde")] + 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 = "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, @@ -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);