diff --git a/README.md b/README.md index 4f68b98..4b8bf14 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,30 @@ Terms can be interpreted in a number of different ways, depending on the context Terms further have mappings as different data types: -- BYTES: if the term maps as a STRING, decode it using base64 - INT: if the term maps as a STRING, decode it as an integer written in decimal notation +- BYTES: if the term maps as a STRING, decode it using base64. Since a STRING cannot be empty, the string `-` is used to represent an empty byte string. +- Cryptographic data types (see below) + +## Cryptographic data types + +Cryptographic values such as keys, hashes, signatures, etc. are encoded +as STRING with a prefix indicating the algorithm used, followed by ":", +followed by the base64-encoded value. + +Prefixes are as follows: + +- `pk.box:` public key for NaCl's box API +- `sk.box:` secret key for NaCl's box API +- `sk.sbox:` secret key for NaCl's secretbox API +- `h.sha256:` sha256 hash +- `h.sha512:` sha512 hash +- `h.sha3:` sha3 hash +- `h.b2:` blake2b hash +- `h.b3:` blake3 hash +- `sig.ed25519:` ed25519 signature +- `pk.ed25519:` ed25519 public signing key +- `sk.ed25519:` ed25519 secret signing key + +More can be added. + - HASH, PUBKEY, SECKEY, SIGNATURE, ENCKEY, DECKEY, SYMKEY: a bunch of things that interpret BYTES as specific cryptographic items diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index a5bce00..81be363 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,48 +1,156 @@ //! Helpers to use cryptographic data types in nettext +pub use dryoc::types::Bytes; pub use dryoc::*; -use dryoc::types::{Bytes, StackByteArray}; - +use crate::dec; use crate::enc; -pub type SigningKeyPair = sign::SigningKeyPair; +const BM_HASH: &str = "h.b2"; -impl enc::Encode for StackByteArray { - fn term(&self) -> enc::Result<'_> { - Ok(enc::bytes(self.as_slice())) +const BM_SIGNATURE: &str = "sig.ed25519"; +const BM_SIGN_KEYPAIR: &str = "sk.ed25519"; +const BM_SIGN_PUBKEY: &str = "pk.ed25519"; + +// ---- types ---- + +#[derive(Eq, PartialEq, Clone, Debug)] +pub struct Hash(pub generichash::Hash); + +#[derive(Eq, PartialEq, Clone, Debug)] +pub struct Signature(pub sign::Signature); +#[derive(Eq, PartialEq, Clone, Debug)] +pub struct SigningPublicKey(pub sign::PublicKey); +#[derive(PartialEq, Clone, Debug)] +pub struct SigningKeyPair(pub sign::SigningKeyPair); + +impl SigningKeyPair { + /// Return the public key of this keypair + pub fn public_key(&self) -> SigningPublicKey { + SigningPublicKey(self.0.public_key.clone()) } } -impl enc::Encode for sign::SigningKeyPair { +// ---- encoding ---- + +impl enc::Encode for Hash { fn term(&self) -> enc::Result<'_> { - Ok(enc::bytes(self.secret_key.as_slice())) + enc::marked_bytes(BM_HASH, self.0.as_slice()) } } -// ---- helpers ---- +impl enc::Encode for Signature { + fn term(&self) -> enc::Result<'_> { + enc::marked_bytes(BM_SIGNATURE, self.0.as_slice()) + } +} + +impl enc::Encode for SigningPublicKey { + fn term(&self) -> enc::Result<'_> { + enc::marked_bytes(BM_SIGN_PUBKEY, self.0.as_slice()) + } +} + +impl enc::Encode for SigningKeyPair { + fn term(&self) -> enc::Result<'_> { + enc::marked_bytes(BM_SIGN_KEYPAIR, self.0.secret_key.as_slice()) + } +} + +// ---- calculating hashes, signatures, etc ---- /// Compute the hash of a payload with default dryoc parameters and optionnal key -pub fn compute_hash(bytes: &[u8], key: Option<&[u8; 32]>) -> generichash::Hash { - generichash::GenericHash::hash_with_defaults(bytes, key).unwrap() +pub fn compute_hash(bytes: &[u8], key: Option<&[u8; 32]>) -> Hash { + Hash(generichash::GenericHash::hash_with_defaults(bytes, key).unwrap()) +} + +/// Generate a new signing keypair +pub fn gen_signing_keypair() -> SigningKeyPair { + SigningKeyPair(sign::SigningKeyPair::gen_with_defaults()) } /// Compute the ed25519 signature of a message using a secret key -pub fn compute_signature(message: &[u8], secret_key: &sign::SecretKey) -> sign::Signature { - SigningKeyPair::from_secret_key(secret_key.clone()) - .sign_with_defaults(message) - .unwrap() - .into_parts() - .0 +pub fn compute_signature(message: &[u8], keypair: &SigningKeyPair) -> Signature { + Signature( + keypair + .0 + .sign_with_defaults(message) + .unwrap() + .into_parts() + .0, + ) } /// Verify the ed25519 signature of a message using a public key pub fn verify_signature( - signature: &sign::Signature, + signature: &Signature, message: &[u8], - public_key: &sign::PublicKey, + public_key: &SigningPublicKey, ) -> bool { - sign::SignedMessage::from_parts(signature.clone(), message) - .verify(public_key) + sign::SignedMessage::from_parts(signature.0.clone(), message) + .verify(&public_key.0) .is_ok() } + +// ---- decode helpers ---- + +pub trait CryptoDec { + /// Try to interpret this string as a Blake2b512 digest + /// (32-bytes base64 encoded, prefixed by `h.b2:`) + /// + /// Example: + /// + /// ``` + /// use nettext::dec::decode; + /// use nettext::crypto::{compute_hash, CryptoDec}; + /// + /// let term = decode(b"{ + /// message = hello; + /// hash = h.b2:Mk3PAn3UowqTLEQfNlol6GsXPe-kuOWJSCU0cbgbcs8; + /// }").unwrap(); + /// let [msg, hash] = term.dict_of(["message", "hash"], false).unwrap(); + /// let expected_hash = compute_hash(msg.raw(), None); + /// assert_eq!(hash.hash().unwrap(), expected_hash); + /// ``` + fn hash(&self) -> Result; + + /// Try to interpret this string as an ed25519 signature + /// (64 bytes base64 encoded, prefixed by `sig.ed25519:`) + fn signature(&self) -> Result; + + /// Try to interpret this string as an ed25519 keypair + /// (64 bytes base64 encoded, prefixed by `sk.ed25519:`) + fn keypair(&self) -> Result; + + /// Try to interpret this string as an ed25519 public key + /// (32 bytes base64 encoded, prefixed by `pk.ed25519:`) + fn public_key(&self) -> Result; +} + +impl<'a, 'b> CryptoDec for dec::Term<'a, 'b> { + fn hash(&self) -> Result { + Ok(Hash(generichash::Hash::from( + self.marked_bytes_exact(BM_HASH)?, + ))) + } + + /// Try to interpret this string as an ed25519 signature (64 bytes base64 encoded) + fn signature(&self) -> Result { + Ok(Signature(sign::Signature::from( + self.marked_bytes_exact(BM_SIGNATURE)?, + ))) + } + + fn keypair(&self) -> Result { + let secret_key = sign::SecretKey::from(self.marked_bytes_exact(BM_SIGN_KEYPAIR)?); + Ok(SigningKeyPair(sign::SigningKeyPair::from_secret_key( + secret_key, + ))) + } + + fn public_key(&self) -> Result { + Ok(SigningPublicKey(sign::PublicKey::from( + self.marked_bytes_exact(BM_SIGN_PUBKEY)?, + ))) + } +} diff --git a/src/dec/error.rs b/src/dec/error.rs index a318e07..936d90d 100644 --- a/src/dec/error.rs +++ b/src/dec/error.rs @@ -8,6 +8,9 @@ pub enum TypeError { /// The term could not be decoded in the given type #[error(display = "Not a {}", _0)] WrongType(&'static str), + /// The term did not have the correct marker + #[error(display = "Byte marker was not {}", _0)] + WrongMarker(&'static str), /// The term is not an array of the requested length #[error(display = "Expected {} items, got {}", _0, _1)] diff --git a/src/dec/mod.rs b/src/dec/mod.rs index fa81431..b6c7533 100644 --- a/src/dec/mod.rs +++ b/src/dec/mod.rs @@ -5,9 +5,6 @@ mod error; use std::collections::HashMap; -#[cfg(any(feature = "dryoc"))] -use crate::crypto; - use crate::debug; pub use decode::*; @@ -420,7 +417,7 @@ impl<'a, 'b> Term<'a, 'b> { }; match self.0.mkref() { AnyTerm::Str(encoded) => { - if encoded == b"." { + if encoded == b"-" { Ok(vec![]) } else { decode(encoded) @@ -442,68 +439,40 @@ impl<'a, 'b> Term<'a, 'b> { } /// Try to interpret this string as base64-encoded bytes, - /// with an exact length. + /// with a marker prefix and an exact byte length. + /// This is typically used for cryptographic data types such as hashes, + /// keys, signatures, ... /// /// Example: /// /// ``` /// use nettext::dec::decode; /// - /// let term = decode(b"aGVsbG8sIHdvcmxkIQ").unwrap(); - /// assert_eq!(&term.bytes_exact::<13>().unwrap(), b"hello, world!"); + /// let term = decode(b"test:aGVsbG8sIHdvcmxkIQ").unwrap(); + /// assert_eq!(&term.marked_bytes_exact::<13>("test").unwrap(), b"hello, world!"); /// ``` - pub fn bytes_exact(&self) -> Result<[u8; N], TypeError> { - let bytes = self.bytes()?; - let bytes_len = bytes.len(); - bytes - .try_into() - .map_err(|_| TypeError::WrongLength(bytes_len, N)) - } -} - -// ---- CRYPTO HELPERS ---- - -#[cfg(feature = "dryoc")] -impl<'a, 'b> Term<'a, 'b> { - /// Try to interpret this string as a Blake2b512 digest (32-bytes base64 encoded) - /// - /// Example: - /// - /// ``` - /// use nettext::dec::decode; - /// use nettext::crypto::generichash::GenericHash; - /// - /// let term = decode(b"{ - /// message = hello; - /// hash = Mk3PAn3UowqTLEQfNlol6GsXPe-kuOWJSCU0cbgbcs8; - /// }").unwrap(); - /// let [msg, hash] = term.dict_of(["message", "hash"], false).unwrap(); - /// let expected_hash = GenericHash::hash_with_defaults(msg.raw(), None::<&Vec>).unwrap(); - /// assert_eq!(hash.hash().unwrap(), expected_hash); - /// ``` - pub fn hash(&self) -> Result { - Ok(crypto::generichash::Hash::from(self.bytes_exact()?)) - } - - /// Try to interpret this string as an ed25519 keypair (64 bytes base64 encoded) - pub fn keypair(&self) -> Result { - let secret_key = crypto::sign::SecretKey::from(self.bytes_exact()?); - Ok(crypto::SigningKeyPair::from_secret_key(secret_key)) - } - - /// Try to interpret this string as an ed25519 public key (32 bytes base64 encoded) - pub fn public_key(&self) -> Result { - Ok(crypto::sign::PublicKey::from(self.bytes_exact()?)) - } - - /// Try to interpret this string as an ed25519 secret key (32 bytes base64 encoded) - pub fn secret_key(&self) -> Result { - Ok(crypto::sign::SecretKey::from(self.bytes_exact()?)) - } - - /// Try to interpret this string as an ed25519 signature (64 bytes base64 encoded) - pub fn signature(&self) -> Result { - Ok(crypto::sign::Signature::from(self.bytes_exact()?)) + pub fn marked_bytes_exact( + &self, + marker: &'static str, + ) -> Result<[u8; N], TypeError> { + let mkr = marker.as_bytes(); + match &self.0 { + AnyTerm::Str(s) + if s.len() >= mkr.len() + 2 && &s[..mkr.len()] == mkr && s[mkr.len()] == b':' => + { + let bytes = match &s[mkr.len() + 1..] { + b"-" => vec![], + bytes => base64::decode_config(bytes, base64::URL_SAFE_NO_PAD) + .map_err(|_| TypeError::WrongType("BYTES"))?, + }; + let bytes_len = bytes.len(); + bytes + .try_into() + .map_err(|_| TypeError::WrongLength(bytes_len, N)) + } + AnyTerm::Str(_) => Err(TypeError::WrongMarker(marker)), + _ => Err(TypeError::WrongType("BYTES")), + } } } diff --git a/src/enc/mod.rs b/src/enc/mod.rs index 920f5fe..60168ed 100644 --- a/src/enc/mod.rs +++ b/src/enc/mod.rs @@ -22,9 +22,9 @@ mod error; use std::borrow::{Borrow, Cow}; use std::collections::HashMap; -use crate::*; use crate::dec::{self, decode}; -use crate::{is_string_char, is_whitespace, BytesEncoding}; +use crate::*; +use crate::{is_string_char, is_whitespace}; pub use error::Error; @@ -100,61 +100,77 @@ pub fn raw(bytes: &[u8]) -> Result<'_> { /// Term corresponding to a byte slice, /// encoding using base64 url-safe encoding without padding. /// Since empty strings are not possible in nettext, -/// an empty byte string is encoded as an empty list (`[]`). +/// an empty byte string is encoded as the special string `-`. /// /// Example: /// /// ``` /// use nettext::enc::*; /// +/// assert_eq!(bytes(b"").encode(), b"-"); /// assert_eq!(bytes(b"hello, world!").encode(), b"aGVsbG8sIHdvcmxkIQ"); /// ``` pub fn bytes(bytes: &[u8]) -> Term<'static> { - bytes_format(bytes, BytesEncoding::Base64 { split: false }) + if bytes.is_empty() { + Term(T::Str(b"-")) + } else { + Term(T::OwnedStr( + base64::encode_config(bytes, base64::URL_SAFE_NO_PAD).into_bytes(), + )) + } } /// Same as `bytes()`, but splits the byte slice in 48-byte chunks /// and encodes each chunk separately, putting them in a sequence of terms. -/// Usefull for long byte slices to have cleaner representations, -/// mainly usefull for dictionnary keys. +/// Usefull for long byte slices to have cleaner representations. pub fn bytes_split(bytes: &[u8]) -> Term<'static> { - bytes_format(bytes, BytesEncoding::Base64 { split: true }) + if bytes.is_empty() { + Term(T::Str(b"-")) + } else { + let chunks = bytes + .chunks(48) + .map(|b| T::OwnedStr(base64::encode_config(b, base64::URL_SAFE_NO_PAD).into_bytes())) + .collect::>(); + if chunks.len() > 1 { + Term(T::Seq(chunks)) + } else { + Term(chunks.into_iter().next().unwrap()) + } + } } -pub fn bytes_format(bytes: &[u8], encoding: BytesEncoding) -> Term<'static> { - match encoding { - BytesEncoding::Base64 { .. } | BytesEncoding::Hex { .. } if bytes.is_empty() => { - Term(T::List(vec![])) - } - BytesEncoding::Base64 { split: false } => Term(T::OwnedStr( - base64::encode_config(bytes, base64::URL_SAFE_NO_PAD).into_bytes(), - )), - BytesEncoding::Base64 { split: true } => { - let chunks = bytes - .chunks(48) - .map(|b| { - T::OwnedStr(base64::encode_config(b, base64::URL_SAFE_NO_PAD).into_bytes()) - }) - .collect::>(); - if chunks.len() > 1 { - Term(T::Seq(chunks)) - } else { - Term(chunks.into_iter().next().unwrap()) - } - } - BytesEncoding::Hex { split: false } => Term(T::OwnedStr(hex::encode(bytes).into_bytes())), - BytesEncoding::Hex { split: true } => { - let chunks = bytes - .chunks(32) - .map(|b| T::OwnedStr(hex::encode(b).into_bytes())) - .collect::>(); - if chunks.len() > 1 { - Term(T::Seq(chunks)) - } else { - Term(chunks.into_iter().next().unwrap()) - } +/// Term corresponding to a byte slice, +/// encoding using base64 url-safe encoding without padding, +/// with a prefix used to identify its content type. +/// The marker prefix is typically used in crypto settings to identify +/// a cryptographic protocol or algorithm; it may not contain the `:` character. +/// +/// Example: +/// +/// ``` +/// use nettext::enc::*; +/// +/// assert_eq!(marked_bytes("mytype", b"").unwrap().encode(), b"mytype:-"); +/// assert_eq!(marked_bytes("mytype", b"hello, world!").unwrap().encode(), b"mytype:aGVsbG8sIHdvcmxkIQ"); +/// ``` +pub fn marked_bytes(marker: &str, bytes: &[u8]) -> Result<'static> { + for c in marker.as_bytes().iter() { + if !is_string_char(*c) || *c == b':' { + return Err(Error::InvalidCharacter(*c)); } } + if bytes.is_empty() { + Ok(Term(T::OwnedStr(format!("{}:-", marker).into_bytes()))) + } else { + Ok(Term(T::OwnedStr( + format!( + "{}:{}", + marker, + base64::encode_config(bytes, base64::URL_SAFE_NO_PAD) + ) + .into_bytes(), + ))) + } } // ---- composed terms ----- diff --git a/src/lib.rs b/src/lib.rs index 33b3fed..2a6f99a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ //! use nettext::crypto::*; //! //! let final_payload = { -//! let keypair = SigningKeyPair::gen_with_defaults(); +//! let keypair = gen_signing_keypair(); //! //! // Encode a fist object that represents a payload that will be hashed and signed //! let signed_payload = seq([ @@ -18,12 +18,12 @@ //! ("c", raw(b"{ a = 12; b = 42 }").unwrap()), //! ("d", bytes_split(&((0..128u8).collect::>()))), //! ]).unwrap(), -//! keypair.public_key.term().unwrap(), +//! keypair.public_key().term().unwrap(), //! ]).unwrap().encode(); //! eprintln!("{}", std::str::from_utf8(&signed_payload).unwrap()); //! //! let hash = compute_hash(&signed_payload, None); -//! let sign = compute_signature(&signed_payload[..], &keypair.secret_key); +//! let sign = compute_signature(&signed_payload[..], &keypair); //! //! // Encode a second object that represents the signed and hashed payload //! dict([ @@ -62,13 +62,13 @@ //! d = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4v //! MDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5f //! YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8; -//! } ZCkE-mTMlK3355u_0UzabRbSNcNO3CWAur7dAhglYtI +//! } pk.ed25519:inYgWFyL_BzZTsXNKp71r2aVct_3Izi_bkerbzOiz94 //! ``` //! //! And the value of `final_payload` would be as follows: //! ```raw //! { -//! hash = fTTk8Hm0HLGwaskCIqFBzRVMrVTeXGetmNBK2X3pNyY; +//! hash = h.b2:B1AnRocS90DmqxynGyvvBNuh-brucNO7-5hrsGplJr0; //! payload = CALL myfunction { //! a = hello; //! b = world; @@ -76,8 +76,8 @@ //! d = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4v //! MDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5f //! YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8; -//! } ZCkE-mTMlK3355u_0UzabRbSNcNO3CWAur7dAhglYtI; -//! signature = XPMrlhAIMfZb6a5Fh5F_ZaEf61olJ1hK4I2kh7vEPT1n20S-943X5cH35bb0Bfwkvy_ENfOTbb3ep1zn2lSIBg; +//! } pk.ed25519:inYgWFyL_BzZTsXNKp71r2aVct_3Izi_bkerbzOiz94; +//! signature = sig.ed25519:LvLC1gHxNxUH44HHQRO-zWtLM4WyXhiYLFr94qTdI311Wa-kmgZsaWqSWe3jcjkS4PnsWSNt5apgbhR68cWWCg; //! } //! ``` //! @@ -93,30 +93,6 @@ pub mod crypto; #[cfg(feature = "serde")] pub mod serde; -/// Possible encodings for byte strings in NetText -#[derive(Clone, Copy)] -pub enum BytesEncoding { - /// Base64 encoding (default) - Base64 { split: bool }, - /// Hexadecimal encoding - Hex { split: bool }, -} - -impl Default for BytesEncoding { - fn default() -> Self { - BytesEncoding::Base64 { split: true } - } -} - -impl BytesEncoding { - pub fn without_whitespace(&self) -> Self { - match self { - BytesEncoding::Base64 { .. } => BytesEncoding::Base64 { split: false }, - BytesEncoding::Hex { .. } => BytesEncoding::Hex { split: false }, - } - } -} - // ---- syntactic elements of the data format ---- pub(crate) const DICT_OPEN: u8 = b'{'; diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 06df916..2efb08b 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -4,7 +4,6 @@ mod de; mod error; mod ser; -pub use crate::BytesEncoding; pub use de::{from_bytes, from_term, Deserializer}; pub use error::{Error, Result}; pub use ser::{to_bytes, to_term, Serializer}; diff --git a/src/serde/ser.rs b/src/serde/ser.rs index 5d9e0b5..3424fd1 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -2,15 +2,11 @@ use serde::{ser, Serialize}; use crate::enc::*; use crate::serde::error::{Error, Result}; -use crate::BytesEncoding; use serde::ser::Error as SerError; /// Serde serializer for nettext #[derive(Clone, Copy, Default)] -pub struct Serializer { - pub string_format: BytesEncoding, - pub bytes_format: BytesEncoding, -} +pub struct Serializer; /// Serialize value to nettext encoder term pub fn to_term(value: &T) -> Result> @@ -94,11 +90,11 @@ impl<'a> ser::Serializer for &'a mut Serializer { } fn serialize_str(self, v: &str) -> Result { - Ok(bytes_format(v.as_bytes(), self.string_format)) + Ok(bytes(v.as_bytes())) } fn serialize_bytes(self, v: &[u8]) -> Result { - Ok(bytes_format(v, self.bytes_format)) + Ok(bytes(v)) } fn serialize_none(self) -> Result { @@ -313,10 +309,7 @@ impl ser::SerializeMap for MapSerializer { where T: ?Sized + Serialize, { - let mut ser = Serializer { - string_format: self.ser.string_format.without_whitespace(), - bytes_format: self.ser.bytes_format.without_whitespace(), - }; + let mut ser = Serializer; self.next = Some(key.serialize(&mut ser)?.encode()); Ok(()) }