diff --git a/.github/workflows/crypto_box.yml b/.github/workflows/crypto_box.yml index e1543a2..ba46f0f 100644 --- a/.github/workflows/crypto_box.yml +++ b/.github/workflows/crypto_box.yml @@ -54,3 +54,4 @@ jobs: override: true - run: cargo test --release --features std - run: cargo test --release --features std,heapless + - run: cargo test --release --features std,serde diff --git a/Cargo.lock b/Cargo.lock index 196a443..543ff4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,21 @@ dependencies = [ "rand_core 0.6.3", ] +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "blake2" version = "0.10.0" @@ -111,10 +126,14 @@ dependencies = [ name = "crypto_box" version = "0.7.0" dependencies = [ + "bincode", "chacha20", "chacha20poly1305", + "rand", "rand_core 0.6.3", + "rmp-serde", "salsa20", + "serde", "x25519-dalek", "xsalsa20poly1305", "zeroize", @@ -294,6 +313,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "opaque-debug" version = "0.3.0" @@ -317,6 +345,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "proc-macro2" version = "1.0.33" @@ -335,6 +369,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.3", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -353,6 +409,36 @@ dependencies = [ "getrandom 0.2.3", ] +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rmp" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f55e5fa1446c4d5dd1f5daeed2a4fe193071771a2636274d0d7a3b082aa7ad6" +dependencies = [ + "byteorder", + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723ecff9ad04f4ad92fe1c8ca6c20d2196d9286e9c60727c4cb5511629260e9d" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "salsa20" version = "0.9.0" diff --git a/crypto_box/Cargo.toml b/crypto_box/Cargo.toml index 4e936d5..d0de870 100644 --- a/crypto_box/Cargo.toml +++ b/crypto_box/Cargo.toml @@ -25,10 +25,26 @@ x25519-dalek = { version = "1", default-features = false } xsalsa20poly1305 = { version = "0.8", default-features = false, features = ["rand_core"] } zeroize = { version = ">=1, <1.5", default-features = false } +[dependencies.serde_crate] +package = "serde" +optional = true +version = "1" +default-features = false + +[dev-dependencies] +bincode = "1" +rand = "0.8" +rmp-serde = "0.15" + [features] default = ["alloc", "u64_backend"] +serde = ["serde_crate"] std = ["rand_core/std", "xsalsa20poly1305/std"] alloc = ["xsalsa20poly1305/alloc"] heapless = ["xsalsa20poly1305/heapless"] u32_backend = ["x25519-dalek/u32_backend"] u64_backend = ["x25519-dalek/u64_backend"] + +[package.metadata.docs.rs] +features = ["serde"] +rustdoc-args = ["--cfg", "docsrs"] diff --git a/crypto_box/src/lib.rs b/crypto_box/src/lib.rs index 9bb9518..22ca238 100644 --- a/crypto_box/src/lib.rs +++ b/crypto_box/src/lib.rs @@ -207,6 +207,12 @@ use xsalsa20poly1305::aead::{ use xsalsa20poly1305::XSalsa20Poly1305; use zeroize::{Zeroize, Zeroizing}; +#[cfg(feature = "serde")] +use serde_crate::{ + de::{Deserialize, Deserializer}, + ser::{Serialize, Serializer}, +}; + /// Size of a `crypto_box` public or secret key in bytes. pub const KEY_SIZE: usize = 32; @@ -215,7 +221,7 @@ pub const KEY_SIZE: usize = 32; /// Implemented as an alias for [`GenericArray`]. pub type Tag = GenericArray; -/// `crypto_box` secret key +/// A `crypto_box` secret key. #[derive(Clone)] pub struct SecretKey([u8; KEY_SIZE]); @@ -265,7 +271,9 @@ impl Drop for SecretKey { } } -/// `crypto_box` public key +/// A `crypto_box` public key. +/// +/// This type can be serialized if the `serde` feature is enabled. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct PublicKey([u8; KEY_SIZE]); @@ -294,6 +302,68 @@ impl From<[u8; KEY_SIZE]> for PublicKey { } } +#[cfg(feature = "serde")] +#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +impl Serialize for PublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(&self.0) + } +} + +#[cfg(feature = "serde")] +#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +impl<'de> Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use core::convert::TryInto; + use serde_crate::de::{Error, SeqAccess, Visitor}; + + struct PublicKeyVisitor; + + impl<'de> Visitor<'de> for PublicKeyVisitor { + type Value = PublicKey; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a 32-byte public key") + } + + fn visit_seq(self, mut seq: S) -> Result + where + S: SeqAccess<'de>, + { + let mut key_bytes = [0; KEY_SIZE]; + for i in 0..KEY_SIZE { + key_bytes[i] = match seq.next_element()? { + Some(val) => val, + None => { + return Err(Error::invalid_length(i - 1, &self)); + } + } + } + Ok(PublicKey::from(key_bytes)) + } + + fn visit_bytes(self, bytes: &[u8]) -> Result + where + E: Error, + { + // Convert to array (with length check) + let array: [u8; KEY_SIZE] = bytes + .try_into() + .map_err(|_| Error::invalid_length(bytes.len(), &self))?; + Ok(PublicKey::from(array)) + } + } + + deserializer.deserialize_bytes(PublicKeyVisitor) + } +} + macro_rules! impl_aead_in_place { ($box:ty, $nonce_size:ty, $tag_size:ty, $ct_overhead:ty) => { impl AeadCore for $box { @@ -415,3 +485,41 @@ impl ChaChaBox { } impl_aead_in_place!(ChaChaBox, U24, U16, U0); + +#[cfg(test)] +mod tests { + #[test] + #[cfg(feature = "serde")] + fn test_public_key_serialization() { + use super::PublicKey; + use rand_core::RngCore; + + // Random PK bytes + let mut public_key_bytes = [0; 32]; + let mut rng = rand::thread_rng(); + rng.fill_bytes(&mut public_key_bytes); + + // Create public key + let public_key = PublicKey::from(public_key_bytes); + + // Round-trip serialize with bincode + let serialized = + bincode::serialize(&public_key).expect("Public key could not be serialized"); + let deserialized: PublicKey = + bincode::deserialize(&serialized).expect("Public key could not be deserialized"); + assert_eq!( + deserialized, public_key, + "Deserialized public key does not match original" + ); + + // Round-trip serialize with rmp (msgpack) + let serialized = + rmp_serde::to_vec_named(&public_key).expect("Public key could not be serialized"); + let deserialized: PublicKey = + rmp_serde::from_slice(&serialized).expect("Public key could not be deserialized"); + assert_eq!( + deserialized, public_key, + "Deserialized public key does not match original" + ); + } +}