diff --git a/src/hazardous/base.rs b/src/hazardous/base.rs new file mode 100644 index 00000000..78e42e34 --- /dev/null +++ b/src/hazardous/base.rs @@ -0,0 +1,670 @@ +//! # Base +//! +//! This module provides convenient, type-parameterized byte-storage types +//! that can be used to quickly create newtypes for keys, digests, etc. +//! The goal is to support all the various cryptography types that are +//! basically just bags of bytes, but **absolutely cannot** afford to +//! be confused with one another. +//! +//! To that end, we define two structs — `Public` and `Secret` — +//! to act as generic containers for public and secret information, respectively. +//! These containers are each parameterized by the same two type parameters: +//! `B` (for "bytes") and `C` (for "context"). +//! +//! +//! ## Parameter: `C` (Context) +//! `C` parameterizes over the **context** of the data. Primarily, +//! this allows us to leverage the type system to protect against +//! misuse of keys (e.g. using one key for two different primitives). +//! In practice, `C` will be a unit struct named after an intended +//! use of the data, such as `chacha::Key`. This will prevent +//! its use in a function that requires instead `aes::Key`. +//! +//! The following example demonstrates how we can leverage the type +//! system and the concept of "contexts" to quickly create types +//! that implement the functionality we want from byte storage objects, +//! but are still logically separate from each other and, in that way, +//! "misuse-resistant". +//! +//! ```rust +//! # #[cfg(feature = "safe_api")] { +//! use orion::hazardous::base::{Context, Generate, Secret, VecData}; +//! +//! // Let's say you hypothetically had keys of two different types: +//! +//! const KEY_SIZE: usize = 32; +//! +//! +//! // AES 256-bit keys +//! struct AesContext; +//! +//! impl Context for AesContext { +//! const NAME: &'static str = "Aes256Key"; +//! const MIN: usize = KEY_SIZE; +//! const MAX: usize = KEY_SIZE; +//! } +//! +//! impl Generate for AesContext { +//! const GEN_SIZE: usize = KEY_SIZE; +//! } +//! +//! +//! // Ed25519 256-bit keys +//! struct EdContext; +//! +//! impl Context for EdContext { +//! const NAME: &'static str = "Ed256Key"; +//! const MIN: usize = KEY_SIZE; +//! const MAX: usize = KEY_SIZE; +//! } +//! +//! impl Generate for EdContext { +//! const GEN_SIZE: usize = KEY_SIZE; +//! } +//! +//! +//! type AesSecretKey = Secret; +//! type EdSecretKey = Secret; +//! +//! let aes_key0 = AesSecretKey::generate(); +//! let aes_key1 = AesSecretKey::generate(); +//! +//! let ed_key0 = EdSecretKey::generate(); +//! let ed_key1 = EdSecretKey::generate(); +//! +//! // We can compare two Ed25519 keys. +//! assert_eq!(&ed_key0, &ed_key0); +//! assert_ne!(&ed_key0, &ed_key1); +//! +//! // We can compare two AES keys. +//! assert_eq!(&aes_key0, &aes_key0); +//! assert_ne!(&aes_key0, &aes_key1); +//! +//! // The below code will NOT compile. This is a good thing. Reusing +//! // keys in different contexts is not only incorrect; it can be +//! // disastrous cryptographically, and can even end up revealing +//! // the secret keys themselves. +//! // +//! // Will error: +//! // assert_eq!(&aes_key0, &des_key0); +//! # } +//! ``` +//! +//! ## Parameter: `D` (Data) +//! `D` parameterizes over the **byte storage**. In practice, this is: +//! +//! - [`ArrayData`][a] for types that hold an amount of data known at +//! compile-time +//! - [`ArrayVecData`][d] for types that hold a compile-time +//! *maximum* amount of data +//! - [`VecData`][b] for types that hold a +//! dynamic amount of data, size known only at runtime. +//! +//! This allows us to implement methods on any type that can be converted +//! from or interpreted as a `&[u8]`. This also makes it possible to add +//! compatibility with, for example, the [`Bytes`][c] type for +//! zero-copy creation of cryptographic types arriving from the network. +//! +//! The following example demonstrates how we can create a type with various +//! types for storage. +//! ```rust +//! # #[cfg(feature = "safe_api")] { +//! use orion::hazardous::base::{ +//! Secret, Context, Generate, ArrayData, VecData, +//! }; +//! +//! struct Password; +//! +//! impl Context for Password { +//! const NAME: &'static str = "Password"; +//! const MIN: usize = 8; +//! const MAX: usize = usize::MAX; +//! } +//! +//! impl Generate for Password { +//! const GEN_SIZE: usize = 32; +//! } +//! +//! type PasswordVec = Secret; +//! type PasswordArray = Secret>; +//! # } +//! ``` +//! +//! +//! [a]: crate::hazardous::base::ArrayData +//! [b]: crate::hazardous::base::VecData +//! [c]: https://docs.rs/bytes/latest/bytes/struct.Bytes.html +//! [d]: crate::hazardous::base::VecData +//! + +use crate::errors::UnknownCryptoError; +use core::{convert::TryFrom, fmt, marker::PhantomData}; + +pub use self::{array_data::ArrayData, array_vec_data::ArrayVecData, vec_data::VecData}; + +/// A simple container for bytes that are considered non-sensitive. +pub struct Public { + context: PhantomData, + data: D, +} + +/// A simple container for bytes that contain sensitive information. +pub struct Secret { + context: PhantomData, + data: D, +} + +/// A small trait containing static information about the name, and +/// minimum and maximum size (in bytes) of a type containing data. +pub trait Context { + /// The type name that will appear in Debug impls. + const NAME: &'static str; + + /// The smallest number of bytes this type should be allowed to hold. + const MIN: usize; + + // TODO: Should this be an exclusive bound? + /// The largest number of bytes this type should be allowed to hold. + const MAX: usize; +} + +/// A trait to express the fact that a type can be (validly) generated +/// from secure random bytes, and the length of that generated type. +/// +/// Note that `Public` and `Secret` implement +/// `Default` if and only if `C` implements `Generate`. +/// +/// When a context type `C` implements `Generate`, the following methods +/// are implemented on `Public` and `Secret`. See those methods' +/// documentation for usage information. +/// +/// - [`Public::generate`](Public::generate) +/// - [`Secret::generate`](Secret::generate) +/// - [`Public::generate_with_size`](Public::generate_with_size) +/// - [`Secret::generate_with_size`](Secret::generate_with_size) +pub trait Generate: Context { + /// The size in bytes of the type when generated randomly. Note that + /// it is a logical error for `GEN_SIZE` to be less than + /// `::MIN` or greater than `::MAX`. + const GEN_SIZE: usize; +} + +/// A trait indicating that some basic operations on byte-slice-like types +/// are available. +pub trait Data: AsRef<[u8]> + AsMut<[u8]> + TryFromBytes {} + +/// A stricter version of `TryFrom<&[u8]>`. By implementing this trait for +/// a type `T`, we prove to the compiler that an *owned* `T` can be +/// generated from a byte slice, versus `TryFrom<&[u8]>` which may be +/// implemented even if the type holds a reference to the slice. +pub trait TryFromBytes: Sized { + /// Convert from a byte slice to an owned `Data`. + fn try_from_bytes(bytes: &[u8]) -> Result; +} + +impl<'a, C, D> Drop for Secret +where + D: Data, +{ + fn drop(&mut self) { + use zeroize::Zeroize; + self.data.as_mut().iter_mut().zeroize(); + } +} + +impl Public +where + C: Context, + D: Data, +{ + /// Create a `Public` from a byte slice. + /// + /// ## Errors + /// This function will return an error if: + /// - The length of the given `slice` is not contained by the range + /// specified by `::MIN` and `::MAX`). + /// - The underlying storage type did not have capacity to hold the + /// given slice. In practice, this condition is usually a subset + /// of the above and does not need to be considered separately. + pub fn from_slice(slice: &[u8]) -> Result { + if !(C::MIN..=C::MAX).contains(&slice.len()) { + return Err(UnknownCryptoError); + } + + Ok(Self { + data: D::try_from_bytes(slice)?, + context: PhantomData, + }) + } + + /// Get the length of the contained byte slice. + pub fn len(&self) -> usize { + self.data.as_ref().len() + } + + /// Check if the contained byte slice is empty. + pub fn is_empty(&self) -> bool { + self.data.as_ref().is_empty() + } + + /// Get a reference to the inner byte slice. + pub fn data(&self) -> &[u8] { + self.as_ref() + } +} + +impl Secret +where + C: Context, + D: Data, +{ + /// Create a `Secret` from a byte slice. Only available when the context + /// type parameter is [`Context`](crate::hazardous::base::Context). + /// + /// ## Errors + /// This function will return an error if: + /// - The length of the given `slice` is not contained by the range + /// specified by `::MIN` and `::MAX`). + /// - The underlying storage type did not have capacity to hold the + /// given slice. In practice, this condition is usually a subset + /// of the above and does not need to be considered separately. + pub fn from_slice(slice: &[u8]) -> Result { + if !(C::MIN..=C::MAX).contains(&slice.len()) { + return Err(UnknownCryptoError); + } + + Ok(Self { + context: PhantomData, + data: D::try_from_bytes(slice)?, + }) + } + + /// Get the length of the contained byte slice. + pub fn len(&self) -> usize { + self.data.as_ref().len() + } + + /// Check if the contained byte slice is empty. + pub fn is_empty(&self) -> bool { + self.data.as_ref().is_empty() + } + + /// Return the object as byte slice. __**Warning**__: Should not be + /// used unless strictly needed. This __**breaks protections**__ that + /// the type implements. + pub fn unprotected_as_bytes(&self) -> &[u8] { + self.data.as_ref() + } +} + +impl AsRef<[u8]> for Public +where + C: Context, + D: Data, +{ + /// Get a reference to the underlying byte slice. + fn as_ref(&self) -> &[u8] { + self.data.as_ref() + } +} + +#[cfg(feature = "safe_api")] +impl Public +where + C: Context + Generate, + D: Data, +{ + /// Use a CSPRNG to fill a new instance of this type with secure random bytes. + /// + /// # Panic + /// This will panic if the underyling call to + /// [`secure_rand_bytes`](crate::util::secure_rand_bytes) fails; + /// see its documentation for more info. + pub fn generate() -> Self { + let mut data = vec![0u8; C::GEN_SIZE]; + crate::util::secure_rand_bytes(&mut data).unwrap(); + Self { + data: D::try_from_bytes(data.as_slice()).unwrap(), + context: PhantomData, + } + } + + /// Use a CSPRNG to fill a new instance of this type with secure random bytes. + /// + /// # Errors + /// - If the passed `size` is less than `::MIN`. + /// - If the passed `size` is greater than `::MAX`. + /// - If the configured data storage parameter cannot hold `size` bytes. + /// + /// # Panic + /// This will panic if the underyling call to + /// [`secure_rand_bytes`](crate::util::secure_rand_bytes) fails; + /// see its documentation for more info. + pub fn generate_with_size(size: usize) -> Result { + if size < ::MIN || size > ::MAX { + return Err(UnknownCryptoError); + } + + let mut data = vec![0u8; size]; + crate::util::secure_rand_bytes(&mut data).unwrap(); + + Ok(Self { + data: D::try_from_bytes(data.as_slice())?, + context: PhantomData, + }) + } +} + +#[cfg(feature = "safe_api")] +impl Secret +where + C: Context + Generate, + D: Data, +{ + /// Use a CSPRNG to fill a new instance of this type with a given number + /// of secure random bytes. + /// + /// # Panic + /// This will panic if the underyling call to + /// [`secure_rand_bytes`](crate::util::secure_rand_bytes) fails; + /// see its documentation for more info. + pub fn generate() -> Self { + let mut data = vec![0u8; C::GEN_SIZE]; + crate::util::secure_rand_bytes(&mut data).unwrap(); + Self { + data: D::try_from_bytes(data.as_slice()).unwrap(), + context: PhantomData, + } + } + + /// Use a CSPRNG to fill a new instance of this type with a given number + /// of secure random bytes. + /// + /// # Errors + /// - If the passed `size` is less than `::MIN`. + /// - If the passed `size` is greater than `::MAX`. + /// - If the configured data storage parameter cannot hold `size` bytes. + /// + /// # Panic + /// This will panic if the underyling call to + /// [`secure_rand_bytes`](crate::util::secure_rand_bytes) fails; + /// see its documentation for more info. + pub fn generate_with_size(size: usize) -> Result { + if size < ::MIN || size > ::MAX { + return Err(UnknownCryptoError); + } + + let mut data = vec![0u8; size]; + crate::util::secure_rand_bytes(&mut data).unwrap(); + + Ok(Self { + data: D::try_from_bytes(data.as_slice())?, + context: PhantomData, + }) + } +} + +/// Delegates to [`B::try_from_bytes`](crate::hazardous::base::TryFromBytes) under the hood. +impl TryFrom<&[u8]> for Public +where + C: Context, + D: TryFromBytes, +{ + type Error = UnknownCryptoError; + + fn try_from(value: &[u8]) -> Result { + Ok(Self { + data: D::try_from_bytes(value).unwrap(), + context: PhantomData, + }) + } +} + +/// Delegates to [`B::try_from_bytes`](crate::hazardous::base::TryFromBytes) under the hood. +impl TryFrom<&[u8]> for Secret +where + C: Context, + D: Data, +{ + type Error = UnknownCryptoError; + + fn try_from(value: &[u8]) -> Result { + Ok(Self { + data: D::try_from_bytes(value).unwrap(), + context: PhantomData, + }) + } +} + +// We define `PartialEq` such that we can compare only with +// other `Public` that have the same "context". +impl PartialEq for Public +where + C: Context, + D: Data, +{ + fn eq(&self, other: &Self) -> bool { + self.data.as_ref().eq(other.data.as_ref()) + } +} + +impl PartialEq<[u8]> for Public +where + C: Context, + D: Data, +{ + fn eq(&self, other: &[u8]) -> bool { + self.data.as_ref().eq(other) + } +} + +// We define `PartialEq` such that we can compare only with +// other `Public` that have the same "context". +impl PartialEq for Secret +where + C: Context, + D: Data, +{ + fn eq(&self, other: &Secret) -> bool { + use subtle::ConstantTimeEq; + self.unprotected_as_bytes() + .ct_eq(other.unprotected_as_bytes()) + .into() + } +} + +impl PartialEq<[u8]> for Secret +where + C: Context, + D: Data, +{ + fn eq(&self, other: &[u8]) -> bool { + use subtle::ConstantTimeEq; + self.unprotected_as_bytes().ct_eq(other).into() + } +} + +impl fmt::Debug for Public +where + C: Context, + D: Data, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {:?}", C::NAME, self.data.as_ref()) + } +} + +impl fmt::Debug for Secret +where + C: Context, + D: Data, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: REDACTED", C::NAME) + } +} + +mod array_data { + use super::{Data, TryFromBytes}; + use crate::errors::UnknownCryptoError; + use std::convert::TryFrom; + + /// A convenient type for holding data with a statically known size. + /// The bytes are held with a static array (`[u8; LEN]`). + // + // NOTE: Deriving PartialEq here is okay becuase we don't use it for + // timing-sensitive comparisons. `ArrayData` is always wrapped in a + // [`Secret`](crate::hazardous::base::Secret) if it's used for + // sensitive information, which implements constant-time comparisons. + // + // Same thing for Debug: the Secret wrapper will handle it.. + #[derive(Clone, Debug, PartialEq)] + pub struct ArrayData { + pub(crate) bytes: [u8; LEN], + } + + impl Data for ArrayData {} + + impl TryFromBytes for ArrayData { + fn try_from_bytes(slice: &[u8]) -> Result { + let bytes = <[u8; LEN]>::try_from(slice).map_err(|_| UnknownCryptoError)?; + Ok(Self { bytes }) + } + } + + impl AsRef<[u8]> for ArrayData { + fn as_ref(&self) -> &[u8] { + self.bytes.as_slice() + } + } + + impl AsMut<[u8]> for ArrayData { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.bytes + } + } +} + +mod array_vec_data { + use super::{Data, TryFromBytes}; + use crate::errors::UnknownCryptoError; + + /// A convenient type for holding data with a static upper bound on + /// its size. The bytes are held with a static array (`[u8; MAX]`). + #[derive(Clone, Debug)] + pub struct ArrayVecData { + pub(crate) bytes: [u8; MAX], + pub(crate) len: usize, + } + + impl Data for ArrayVecData {} + + impl TryFromBytes for ArrayVecData { + fn try_from_bytes(slice: &[u8]) -> Result { + if slice.len() > MAX { + return Err(UnknownCryptoError); + } + + let mut bytes = [0u8; MAX]; + + // PANIC: This is ok because we just checked that the length + // was less than MAX above. Violating that condition is the + // only thing that would cause this to panic. + bytes + .get_mut(0..slice.len()) + .unwrap() + .copy_from_slice(slice); + + Ok(Self { + bytes, + len: slice.len(), + }) + } + } + + impl AsRef<[u8]> for ArrayVecData { + fn as_ref(&self) -> &[u8] { + // PANIC: This unwrap is ok because the type's len is checked at + // construction time to be less than MAX. + self.bytes.get(..self.len).unwrap() + } + } + + impl AsMut<[u8]> for ArrayVecData { + fn as_mut(&mut self) -> &mut [u8] { + // PANIC: This unwrap is ok because the type's len is checked at + // construction time to be less than MAX. + self.bytes.get_mut(..self.len).unwrap() + } + } +} + +// // NOTE: Using non-constant-time comparison here is okay becuase we don't +// // use it for timing-sensitive comparisons. `ArrayVecData` is always wrapped +// // in a [`Secret`](crate::hazardous::base::Secret) if it's used for +// // sensitive information, which implements constant-time comparisons. +// // +// // Same thing for Debug: the Secret wrapper will handle it. +// impl PartialEq for ArrayVecData { +// fn eq(&self, other: &Self) -> bool { +// self.bytes.get(..self.len).eq(&other.bytes.get(..other.len)) +// } +// } + +mod vec_data { + use super::{Data, TryFromBytes}; + use crate::errors::UnknownCryptoError; + use std::convert::TryFrom; + + /// A convenient type for holding data in dynamically allocated buffer. + // TODO: Should we just use a `Vec` here? We could implement all of the + // same traits for a regular old Vec. + // + // NOTE: Deriving PartialEq here is okay becuase we don't use it for + // timing-sensitive comparisons. `VecData` is always wrapped in a + // [`Secret`](crate::hazardous::base::Secret) if it's used for + // sensitive information, which implements constant-time comparisons. + // + // Same thing for Debug: the Secret wrapper will handle it.. + #[cfg(feature = "safe_api")] + #[derive(Clone, Debug, PartialEq)] + pub struct VecData { + pub(crate) bytes: Vec, + } + + #[cfg(feature = "safe_api")] + impl Data for VecData {} + + #[cfg(feature = "safe_api")] + impl TryFromBytes for VecData { + fn try_from_bytes(slice: &[u8]) -> Result { + Ok(Self { + bytes: slice.into(), + }) + } + } + + #[cfg(feature = "safe_api")] + impl AsRef<[u8]> for VecData { + fn as_ref(&self) -> &[u8] { + self.bytes.as_slice() + } + } + + #[cfg(feature = "safe_api")] + impl AsMut<[u8]> for VecData { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.bytes + } + } + + #[cfg(feature = "safe_api")] + impl<'a> TryFrom<&'a [u8]> for VecData { + type Error = UnknownCryptoError; + + fn try_from(value: &'a [u8]) -> Result { + let bytes = Vec::from(value); + Ok(VecData { bytes }) + } + } +} diff --git a/src/hazardous/hash/blake2/blake2b.rs b/src/hazardous/hash/blake2/blake2b.rs index 46f43994..b3eb627f 100644 --- a/src/hazardous/hash/blake2/blake2b.rs +++ b/src/hazardous/hash/blake2/blake2b.rs @@ -62,20 +62,28 @@ //! [`mac::blake2b`]: crate::hazardous::mac::blake2b use crate::errors::UnknownCryptoError; +use crate::hazardous::base::{ArrayVecData, Context, Public}; use crate::hazardous::hash::blake2::blake2b_core; use crate::hazardous::hash::blake2::blake2b_core::BLAKE2B_OUTSIZE; #[cfg(feature = "safe_api")] use std::io; -construct_public! { - /// A type to represent the `Digest` that BLAKE2b returns. - /// - /// # Errors: - /// An error will be returned if: - /// - `slice` is empty. - /// - `slice` is greater than 64 bytes. - (Digest, test_digest, 1, BLAKE2B_OUTSIZE) +/// A type to represent the `Digest` that BLAKE2b returns. +/// +/// # Errors: +/// An error will be returned if: +/// - `slice` is empty. +/// - `slice` is greater than 64 bytes. +pub type Digest = Public>; + +/// A marker type to declare that this data represents a Blake2b digest. +pub struct BlakeDigest; + +impl Context for BlakeDigest { + const NAME: &'static str = "Blake2bDigest"; + const MIN: usize = 1; + const MAX: usize = BLAKE2B_OUTSIZE; } #[derive(Debug, Clone)] @@ -409,4 +417,14 @@ mod public { hash_a == hash_b } } + + mod test_base { + use crate::hazardous::hash::blake2::blake2b::{Blake2b, Digest}; + fn gen_test_data() -> Digest { + let mut hasher = Blake2b::new(64).unwrap(); + hasher.update(b"test data").unwrap(); + hasher.finalize().unwrap() + } + crate::test_base!(Digest, gen_test_data, public); + } } diff --git a/src/hazardous/mod.rs b/src/hazardous/mod.rs index 0817404e..4b5728e1 100644 --- a/src/hazardous/mod.rs +++ b/src/hazardous/mod.rs @@ -44,3 +44,6 @@ pub mod stream; /// Elliptic-Curve Cryptography. pub mod ecc; + +/// Primary byte-container types used in the public API. +pub mod base; diff --git a/src/high_level/aead.rs b/src/high_level/aead.rs index 4ab5003c..aee5aa4a 100644 --- a/src/high_level/aead.rs +++ b/src/high_level/aead.rs @@ -80,11 +80,11 @@ #![cfg_attr(docsrs, doc(cfg(feature = "safe_api")))] -pub use super::hltypes::SecretKey; use crate::{ errors::UnknownCryptoError, hazardous::{ aead, + base::{Context, Generate, Secret, VecData}, mac::poly1305::POLY1305_OUTSIZE, stream::{ chacha20, @@ -93,6 +93,29 @@ use crate::{ }, }; +/// A type to represent the `SecretKey` used in AEAD. +pub type SecretKey = Secret; + +/// A marker type to declare that this data represents an AEAD secret key. +// TODO: Should this be named something more specific? Like `ChaChaKey`? +pub struct AeadKey; + +impl Context for AeadKey { + const NAME: &'static str = "AeadKey"; + const MIN: usize = 32; // TODO: Is this the right min size? + const MAX: usize = 1024 * 1024; // TODO: Is there a less arbitrary upper bound than a MB? +} + +impl Generate for AeadKey { + const GEN_SIZE: usize = 32; +} + +impl Default for SecretKey { + fn default() -> Self { + Self::generate() + } +} + #[must_use = "SECURITY WARNING: Ignoring a Result can have real security implications."] /// Authenticated encryption using XChaCha20Poly1305. pub fn seal(secret_key: &SecretKey, plaintext: &[u8]) -> Result, UnknownCryptoError> { @@ -399,7 +422,7 @@ mod public { #[test] fn test_secret_length_err() { - let key = SecretKey::generate(31).unwrap(); + let key = SecretKey::generate_with_size(31).unwrap(); let plaintext = "Secret message".as_bytes(); assert!(seal(&key, plaintext).is_err()); @@ -513,7 +536,7 @@ mod public { #[test] fn test_secret_length_err() { - let key = SecretKey::generate(31).unwrap(); + let key = SecretKey::generate_with_size(31).unwrap(); assert!(StreamSealer::new(&key).is_err()); assert!(StreamOpener::new(&key, &Nonce::generate()).is_err()); } @@ -628,4 +651,18 @@ mod public { open(&sk2, &ct).is_err() } } + + mod test_base { + use crate::{ + hazardous::base::VecData, + high_level::aead::{AeadKey, SecretKey}, + }; + + fn gen_test_data() -> SecretKey { + SecretKey::generate_with_size(64).unwrap() + } + + crate::test_base!(SecretKey, gen_test_data, secret); + crate::test_generate!(AeadKey, VecData, secret); + } } diff --git a/src/lib.rs b/src/lib.rs index c6f42fea..20290a8f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,10 @@ pub use high_level::kdf; #[cfg(feature = "safe_api")] pub use high_level::kex; +#[cfg(feature = "safe_api")] +pub use hazardous::base::{Public, Secret}; + #[doc(hidden)] +#[macro_use] /// Testing framework. pub mod test_framework; diff --git a/src/test_framework/base_interface.rs b/src/test_framework/base_interface.rs new file mode 100644 index 00000000..5079b76f --- /dev/null +++ b/src/test_framework/base_interface.rs @@ -0,0 +1,220 @@ +use crate::{ + hazardous::base::{Context, Data, Generate}, + Public, Secret, +}; + +// TODO: Do we need to export this? Is it bad if we do? +#[macro_export] +macro_rules! test_base { + ($newtype_alias:ident, $gen_test_data:ident, public) => { + #[test] + fn test_normal_debug() { + crate::test_framework::base_interface::test_normal_debug($gen_test_data()); + } + + #[test] + fn test_as_bytes_public() { + crate::test_framework::base_interface::test_as_bytes_public($gen_test_data()); + } + }; + + ($newtype_alias:ident, $gen_test_data: ident, secret) => { + #[test] + fn test_omitted_debug() { + crate::test_framework::base_interface::test_omitted_debug($gen_test_data()); + } + + #[test] + fn test_as_bytes_secret() { + crate::test_framework::base_interface::test_as_bytes_secret($gen_test_data()); + } + }; +} + +// TODO: Do we need to export this? Is it bad if we do? +#[macro_export] +macro_rules! test_generate { + ($context_type:ident, $data_type:ident, public) => { + #[cfg(feature = "safe_api")] + #[test] + fn test_generate_public() { + crate::test_framework::base_interface::test_generate_public::< + $context_type, + $data_type, + >(); + } + + #[cfg(feature = "safe_api")] + #[test] + fn test_generate_with_size_public() { + crate::test_framework::base_interface::test_generate_with_size_public::< + $context_type, + $data_type, + >(); + } + }; + + ($context_type:ident, $data_type:ident, secret) => { + #[cfg(feature = "safe_api")] + #[test] + fn test_generate_secret() { + crate::test_framework::base_interface::test_generate_secret::< + $context_type, + $data_type, + >(); + } + + #[cfg(feature = "safe_api")] + #[test] + fn test_generate_with_size_secret() { + crate::test_framework::base_interface::test_generate_with_size_secret::< + $context_type, + $data_type, + >(); + } + }; +} + +pub(crate) fn test_omitted_debug(secret: Secret) +where + C: Context, + D: Data, +{ + let secret_data = format!("{:?}", secret.unprotected_as_bytes()); + let debug_contents = format!("{:?}", secret); + assert!(!debug_contents.contains(&secret_data)); +} + +pub(crate) fn test_normal_debug(public: Public) +where + C: Context, + D: Data, +{ + let public_data = format!("{:?}", public.as_ref()); + let debug_contents = format!("{:?}", public_data); + assert!(debug_contents.contains(&public_data)); +} + +pub(crate) fn test_as_bytes_secret(secret: Secret) +where + C: Context, + D: Data, +{ + // Test fixed-length definitions + assert_eq!(secret.unprotected_as_bytes().len(), secret.len()); + assert!(!secret.is_empty()); + + if C::MIN == C::MAX { + assert_eq!(secret.len(), C::MIN); + assert_eq!(secret.len(), C::MAX); + } else if C::MIN != C::MAX { + // Test non-fixed-length definitions + let data = secret.unprotected_as_bytes(); + + let secret_subset = Secret::::from_slice(&data[..C::MIN]).unwrap(); + + assert_eq!(secret_subset.len(), C::MIN); + assert_eq!(secret_subset.is_empty(), false); + + assert_eq!( + secret_subset.unprotected_as_bytes().len(), + secret_subset.len() + ); + } +} + +pub(crate) fn test_as_bytes_public(public: Public) +where + C: Context, + D: Data, +{ + if C::MIN == C::MAX { + // Test fixed-length definitions + assert_eq!(public.as_ref().len(), public.len()); + assert!(!public.is_empty()); + assert_eq!(public.len(), C::MIN); + assert_eq!(public.len(), C::MAX); + } else { + // Test non-fixed-length definitions + let data = public.as_ref(); + let public_lower = Public::::from_slice(&data[..C::MIN]).unwrap(); + let public_upper = Public::::from_slice(&data[..C::MAX]).unwrap(); + + assert_eq!(public_lower.len(), C::MIN); + assert_eq!(public_upper.len(), C::MAX); + + assert_eq!(public_lower.as_ref().len(), public_lower.len()); + assert_eq!(public_upper.as_ref().len(), public_upper.len()); + + assert_eq!(public_lower.is_empty(), false); + assert_eq!(public_upper.is_empty(), false); + } +} + +#[cfg(feature = "safe_api")] +pub(crate) fn test_generate_secret() +where + C: Context + Generate, + D: Data, +{ + let generated = Secret::::generate(); + + assert!(!generated + .unprotected_as_bytes() + .iter() + .copied() + .all(|b| b == 0)); + + assert_eq!(generated.len(), C::GEN_SIZE); + assert_eq!(generated.unprotected_as_bytes().len(), C::GEN_SIZE); +} + +#[cfg(feature = "safe_api")] +pub(crate) fn test_generate_public() +where + C: Context + Generate, + D: Data, +{ + let generated = Public::::generate(); + assert!(!generated.as_ref().iter().copied().all(|b| b == 0)); + assert_eq!(generated.len(), C::GEN_SIZE); + assert_eq!(generated.as_ref().len(), C::GEN_SIZE); +} + +#[cfg(feature = "safe_api")] +pub(crate) fn test_generate_with_size_secret() +where + C: Context + Generate, + D: Data, +{ + // least, middle, greatest possible value + let sizes = Vec::from([C::MIN, (C::MIN + (C::MAX - C::MIN) / 2), C::MAX]); + + for size in sizes { + let generated = Secret::::generate_with_size(size).unwrap(); + assert!(!generated + .unprotected_as_bytes() + .iter() + .copied() + .all(|b| b == 0)); + assert_eq!(generated.len(), size); + assert_eq!(generated.unprotected_as_bytes().len(), size); + } +} + +#[cfg(feature = "safe_api")] +pub(crate) fn test_generate_with_size_public() +where + C: Context + Generate, + D: Data, +{ + // least, middle, greatest possible value + let sizes = Vec::from([C::MIN, (C::MIN + (C::MAX - C::MIN) / 2), C::MAX]); + + for size in sizes { + let generated = Public::::generate_with_size(size).unwrap(); + assert!(!generated.as_ref().iter().copied().all(|b| b == 0)); + assert_eq!(generated.len(), size); + assert_eq!(generated.as_ref().len(), size); + } +} diff --git a/src/test_framework/mod.rs b/src/test_framework/mod.rs index 8612baec..755d7122 100644 --- a/src/test_framework/mod.rs +++ b/src/test_framework/mod.rs @@ -28,3 +28,7 @@ pub mod aead_interface; /// Tests for stream ciphers such as `chacha20`. pub mod streamcipher_interface; + +#[cfg(test)] +#[macro_use] +pub mod base_interface;