From 4d4f9336d6fe9e7bd586a0c69d9cd33b727c58d3 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 17 Nov 2022 16:35:06 +0100 Subject: [PATCH] Add crypto and documentation --- Cargo.toml | 7 ++ src/crypto.rs | 36 ++++++++ src/dec/decode.rs | 25 +---- src/dec/mod.rs | 231 ++++++++++++++++++++++++++++++++++++---------- src/lib.rs | 1 + 5 files changed, 229 insertions(+), 71 deletions(-) create mode 100644 src/crypto.rs diff --git a/Cargo.toml b/Cargo.toml index f504e07..1d07763 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,10 @@ readme = "README.md" [dependencies] nom = "7.1" +base64 = "0.13" + +blake2 = { version = "0.10", optional = true } +ed25519-dalek = { version = "1.0", optional = true } + +[features] +default = [ "blake2", "ed25519-dalek" ] diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..c9847ec --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,36 @@ +pub enum CryptoError { + InvalidHash, + InvalidSignature, +} + +#[cfg(feature = "blake2")] +mod b2 { + use super::CryptoError; + use blake2::{Blake2b512, Digest}; + + #[derive(Clone, Copy, Eq, PartialEq)] + pub struct Blake2Sum([u8; 64]); + + impl Blake2Sum { + pub fn from_bytes(bytes: [u8; 64]) -> Self { + Self(bytes) + } + + pub fn compute(buf: &[u8]) -> Self { + let mut hasher = Blake2b512::new(); + hasher.update(buf); + Self(hasher.finalize()[..].try_into().unwrap()) + } + + pub fn check(&self, buf: &[u8]) -> Result<(), CryptoError> { + if Self::compute(buf) == *self { + Ok(()) + } else { + Err(CryptoError::InvalidHash) + } + } + } +} + +#[cfg(feature = "blake2")] +pub use b2::*; diff --git a/src/dec/decode.rs b/src/dec/decode.rs index 89f704e..a145d4f 100644 --- a/src/dec/decode.rs +++ b/src/dec/decode.rs @@ -23,7 +23,7 @@ const STR_EXTRA_CHARS: &[u8] = b"._-*?"; pub enum DecodeError<'a> { /// Indicates that there is trailing garbage at the end of the decoded string Garbage(&'a [u8]), - /// Indicates that the entered string does not represent a complete NetText term + /// Indicates that the entered string does not represent a complete nettext term IncompleteInput, /// Indicates a syntax error in the decoded term NomError(&'a [u8], nom::error::ErrorKind), @@ -39,9 +39,6 @@ impl<'a> std::fmt::Debug for DecodeError<'a> { } } -/// The result type returned by the `decode` function -pub type DecodeResult<'a, T> = std::result::Result>; - impl<'a> From>> for DecodeError<'a> { fn from(e: nom::Err>) -> DecodeError<'a> { match e { @@ -53,8 +50,8 @@ impl<'a> From>> for DecodeError<'a> { // ---- -/// Decodes a NetText string into the term it represents. -pub fn decode(input: &[u8]) -> DecodeResult<'_, Term<'_, 'static>> { +/// Decodes a nettext string into the term it represents. +pub fn decode(input: &[u8]) -> std::result::Result, DecodeError<'_>> { let (rest, term) = decode_term(input)?; let (end, _) = take_while(is_whitespace)(rest)?; if !end.is_empty() { @@ -64,11 +61,8 @@ pub fn decode(input: &[u8]) -> DecodeResult<'_, Term<'_, 'static>> { } fn decode_term(input: &[u8]) -> IResult<&'_ [u8], AnyTerm<'_, 'static>> { - eprintln!("DT: `{}`", debug(input)); let (start, _) = take_while(is_whitespace)(input)?; - eprintln!("DT2: `{}`", debug(start)); let (rest, list) = separated_list1(take_while1(is_whitespace), decode_nonlist_term)(start)?; - eprintln!("DT3: `{}`", debug(rest)); if list.len() == 1 { Ok((rest, list.into_iter().next().unwrap().into())) @@ -80,17 +74,14 @@ fn decode_term(input: &[u8]) -> IResult<&'_ [u8], AnyTerm<'_, 'static>> { } fn decode_nonlist_term(input: &[u8]) -> IResult<&'_ [u8], NonListTerm<'_, 'static>> { - eprintln!("DNLT: `{}`", debug(input)); let (rest, term) = alt(( map(decode_str, NonListTerm::Str), map(decode_dict, |(raw, d)| NonListTerm::Dict(raw, d)), ))(input)?; - eprintln!("DNLTend: `{}` {:?}", debug(rest), term); Ok((rest, term)) } fn decode_str(input: &[u8]) -> IResult<&'_ [u8], &'_ [u8]> { - eprintln!("DS: `{}`", debug(input)); let (rest, data) = take_while1(is_string_char)(input)?; Ok((rest, data)) } @@ -98,16 +89,11 @@ fn decode_str(input: &[u8]) -> IResult<&'_ [u8], &'_ [u8]> { type DictType<'a> = (&'a [u8], HashMap<&'a [u8], AnyTerm<'a, 'static>>); fn decode_dict(dict_begin: &[u8]) -> IResult<&'_ [u8], DictType<'_>> { - eprintln!("DDbegin: `{}`", debug(dict_begin)); let (d, _) = tag(DICT_OPEN)(dict_begin)?; - eprintln!("DD2: `{}`", debug(d)); let (d, items) = separated_list0(dict_separator, decode_dict_item)(d)?; - eprintln!("DD3: `{}`", debug(d)); let (d, _) = opt(dict_separator)(d)?; let (d, _) = take_while(is_whitespace)(d)?; - eprintln!("DD4: `{}`", debug(d)); let (dict_end, _) = tag(DICT_CLOSE)(d)?; - eprintln!("DDend: `{}`", debug(dict_end)); let dict = items.into_iter().collect::>(); @@ -124,16 +110,11 @@ fn dict_separator(d: &[u8]) -> IResult<&'_ [u8], ()> { } fn decode_dict_item(d: &[u8]) -> IResult<&'_ [u8], (&'_ [u8], AnyTerm<'_, 'static>)> { - eprintln!("DDI: `{}`", debug(d)); let (d, _) = take_while(is_whitespace)(d)?; - eprintln!("DDI1: `{}`", debug(d)); let (d, key) = decode_str(d)?; - eprintln!("DDI2: `{}`", debug(d)); let (d, _) = take_while(is_whitespace)(d)?; let (d, _) = tag(DICT_ASSIGN)(d)?; - eprintln!("DDI3: `{}`", debug(d)); let (d, value) = decode_term(d)?; - eprintln!("DDI4: `{}`", debug(d)); Ok((d, (key, value))) } diff --git a/src/dec/mod.rs b/src/dec/mod.rs index 94bdf1c..ea4ff1f 100644 --- a/src/dec/mod.rs +++ b/src/dec/mod.rs @@ -2,9 +2,10 @@ mod decode; use std::collections::HashMap; +use crate::crypto::*; pub use decode::*; -/// A parsed NetText term. +/// A parsed nettext term, with many helpers for destructuring /// /// Lifetime 'a is the lifetime of the buffer containing the encoded data. /// @@ -60,15 +61,30 @@ impl<'a> From> for Term<'a, 'static> { // ---- PUBLIC IMPLS ---- +/// The type of errors returned by helper functions on `Term` #[derive(Debug, Clone)] pub enum TypeError { + /// The term could not be decoded in the given type WrongType(&'static str), + /// The term is not an array of the requested length WrongLength(usize, usize), + /// The dictionnary is missing a key MissingKey(String), + /// The dictionnary contains an invalid key UnexpectedKey(String), + /// The underlying raw string contains garbage (should not happen in theory) + Garbage, +} + +impl From for TypeError { + fn from(_x: std::str::Utf8Error) -> TypeError { + TypeError::Garbage + } } impl<'a, 'b> Term<'a, 'b> { + // ---- STRUCTURAL MAPPINGS ---- + /// Get the term's raw representation /// /// Example: @@ -90,15 +106,15 @@ impl<'a, 'b> Term<'a, 'b> { /// ``` /// use nettext::dec::decode; /// - /// let term1 = decode(b" hello ").unwrap(); - /// assert_eq!(term1.str().unwrap(), b"hello"); + /// let term1 = decode(b"hello").unwrap(); + /// assert_eq!(term1.str().unwrap(), "hello"); /// /// let term2 = decode(b"hello world").unwrap(); /// assert!(term2.str().is_err()); /// ``` - pub fn str(&self) -> Result<&'a [u8], TypeError> { + pub fn str(&self) -> Result<&'a str, TypeError> { match &self.0 { - AnyTerm::Str(s) => Ok(s), + AnyTerm::Str(s) => Ok(std::str::from_utf8(s)?), _ => Err(TypeError::WrongType("STR")), } } @@ -111,16 +127,18 @@ impl<'a, 'b> Term<'a, 'b> { /// ``` /// use nettext::dec::decode; /// - /// let term1 = decode(b" hello world ").unwrap(); - /// assert_eq!(term1.string().unwrap(), b"hello world"); + /// let term1 = decode(b"hello world").unwrap(); + /// assert_eq!(term1.string().unwrap(), "hello world"); /// /// let term2 = decode(b"hello { a= 5}").unwrap(); /// assert!(term2.string().is_err()); /// ``` - pub fn string(&self) -> Result<&'a [u8], TypeError> { + pub fn string(&self) -> Result<&'a str, TypeError> { match &self.0 { - AnyTerm::Str(s) => Ok(s), - AnyTerm::List(r, l) if l.iter().all(|x| matches!(x, NonListTerm::Str(_))) => Ok(r), + AnyTerm::Str(s) => Ok(std::str::from_utf8(s)?), + AnyTerm::List(r, l) if l.iter().all(|x| matches!(x, NonListTerm::Str(_))) => { + Ok(std::str::from_utf8(r)?) + } _ => Err(TypeError::WrongType("STRING")), } } @@ -134,16 +152,16 @@ impl<'a, 'b> Term<'a, 'b> { /// ``` /// use nettext::dec::decode; /// - /// let term1 = decode(b" hello ").unwrap(); + /// let term1 = decode(b"hello").unwrap(); /// let list1 = term1.list(); /// assert_eq!(list1.len(), 1); - /// assert_eq!(list1[0].str().unwrap(), b"hello"); + /// assert_eq!(list1[0].str().unwrap(), "hello"); /// - /// let term2 = decode(b" hello world ").unwrap(); + /// let term2 = decode(b"hello world").unwrap(); /// let list2 = term2.list(); /// assert_eq!(list2.len(), 2); - /// assert_eq!(list2[0].str().unwrap(), b"hello"); - /// assert_eq!(list2[1].str().unwrap(), b"world"); + /// assert_eq!(list2[0].str().unwrap(), "hello"); + /// assert_eq!(list2[1].str().unwrap(), "world"); /// ``` pub fn list(&self) -> Vec> { match self.0.mkref() { @@ -152,7 +170,8 @@ impl<'a, 'b> Term<'a, 'b> { } } - /// Same as `.list()`, but deconstructs it in a const length array. + /// Same as `.list()`, but deconstructs it in a const length array, + /// dynamically checking if there are the correct number of items. /// This allows to directly bind the resulting list into discrete variables. /// /// Example: @@ -160,14 +179,14 @@ impl<'a, 'b> Term<'a, 'b> { /// ``` /// use nettext::dec::decode; /// - /// let term1 = decode(b" hello ").unwrap(); + /// let term1 = decode(b"hello").unwrap(); /// let [s1] = term1.list_of().unwrap(); - /// assert_eq!(s1.str().unwrap(), b"hello"); + /// assert_eq!(s1.str().unwrap(), "hello"); /// - /// let term2 = decode(b" hello world ").unwrap(); + /// let term2 = decode(b"hello world").unwrap(); /// let [s2a, s2b] = term2.list_of().unwrap(); - /// assert_eq!(s2a.str().unwrap(), b"hello"); - /// assert_eq!(s2b.str().unwrap(), b"world"); + /// assert_eq!(s2a.str().unwrap(), "hello"); + /// assert_eq!(s2b.str().unwrap(), "world"); /// ``` pub fn list_of(&self) -> Result<[Term<'a, '_>; N], TypeError> { let list = self.list(); @@ -186,14 +205,14 @@ impl<'a, 'b> Term<'a, 'b> { /// ``` /// use nettext::dec::decode; /// - /// let term1 = decode(b" hello world ").unwrap(); + /// let term1 = decode(b"hello world").unwrap(); /// let [s1a, s1b] = term1.list_of_first().unwrap(); - /// assert_eq!(s1a.str().unwrap(), b"hello"); - /// assert_eq!(s1b.str().unwrap(), b"world"); + /// assert_eq!(s1a.str().unwrap(), "hello"); + /// assert_eq!(s1b.str().unwrap(), "world"); /// - /// let term2 = decode(b" hello mighty world ").unwrap(); + /// let term2 = decode(b"hello mighty world").unwrap(); /// let [s2a, s2b] = term2.list_of_first().unwrap(); - /// assert_eq!(s2a.str().unwrap(), b"hello"); + /// assert_eq!(s2a.str().unwrap(), "hello"); /// assert_eq!(s2b.list().len(), 2); /// assert_eq!(s2b.raw(), b"mighty world"); /// ``` @@ -231,7 +250,8 @@ impl<'a, 'b> Term<'a, 'b> { } } - /// Checks term is a dictionnary and returns hashmap of inner terms + /// Checks term is a dictionnary and returns hashmap of inner terms. + /// For convenience, this transforms the keys of the dictionnary into `&str`'s. /// /// Example: /// @@ -240,12 +260,18 @@ impl<'a, 'b> Term<'a, 'b> { /// /// let term = decode(b"{ k1 = v1, k2 = v2 }").unwrap(); /// let dict = term.dict().unwrap(); - /// assert_eq!(dict.get(&b"k1"[..]).unwrap().str().unwrap(), b"v1"); - /// assert_eq!(dict.get(&b"k2"[..]).unwrap().str().unwrap(), b"v2"); + /// assert_eq!(dict.get("k1").unwrap().str().unwrap(), "v1"); + /// assert_eq!(dict.get("k2").unwrap().str().unwrap(), "v2"); /// ``` - pub fn dict(&self) -> Result>, TypeError> { + pub fn dict(&self) -> Result>, TypeError> { match self.0.mkref() { - AnyTerm::DictRef(_, d) => Ok(d.iter().map(|(k, t)| (*k, Term(t.mkref()))).collect()), + AnyTerm::DictRef(_, d) => { + let mut res = HashMap::with_capacity(d.len()); + for (k, t) in d.iter() { + res.insert(std::str::from_utf8(k)?, Term(t.mkref())); + } + Ok(res) + } _ => Err(TypeError::WrongType("DICT")), } } @@ -259,34 +285,34 @@ impl<'a, 'b> Term<'a, 'b> { /// use nettext::dec::decode; /// /// let term = decode(b"{ k1 = v1, k2 = v2, k3 = v3 }").unwrap(); - /// let [s1, s2] = term.dict_of([b"k1", b"k2"], true).unwrap(); - /// assert_eq!(s1.str().unwrap(), b"v1"); - /// assert_eq!(s2.str().unwrap(), b"v2"); + /// let [s1, s2] = term.dict_of(["k1", "k2"], true).unwrap(); + /// assert_eq!(s1.str().unwrap(), "v1"); + /// assert_eq!(s2.str().unwrap(), "v2"); /// /// assert!(term.dict_of([b"k1", b"k2"], false).is_err()); /// ``` - pub fn dict_of( + pub fn dict_of>( &self, - keys: [&'static [u8]; N], + keys: [T; N], allow_extra_keys: bool, ) -> Result<[Term<'a, '_>; N], TypeError> { match self.0.mkref() { AnyTerm::DictRef(_, dict) => { // Check all required keys exist in dictionnary for k in keys.iter() { - if !dict.contains_key(k) { - return Err(TypeError::MissingKey(debug(k).to_string())); + if !dict.contains_key(k.as_ref()) { + return Err(TypeError::MissingKey(debug(k.as_ref()).to_string())); } } if !allow_extra_keys { // Check that dictionnary contains no extraneous keys for k in dict.keys() { - if !keys.contains(k) { + if !keys.iter().any(|k2| k2.as_ref() == *k) { return Err(TypeError::UnexpectedKey(debug(k).to_string())); } } } - Ok(keys.map(|k| Term(dict.get(k).unwrap().mkref()))) + Ok(keys.map(|k| Term(dict.get(k.as_ref()).unwrap().mkref()))) } _ => Err(TypeError::WrongType("DICT")), } @@ -302,15 +328,15 @@ impl<'a, 'b> Term<'a, 'b> { /// /// let term = decode(b"{ k1 = v1, k2 = v2, k4 = v4 }").unwrap(); /// let [s1, s2, s3] = term.dict_of_opt([b"k1", b"k2", b"k3"], true).unwrap(); - /// assert_eq!(s1.unwrap().str().unwrap(), b"v1"); - /// assert_eq!(s2.unwrap().str().unwrap(), b"v2"); + /// assert_eq!(s1.unwrap().str().unwrap(), "v1"); + /// assert_eq!(s2.unwrap().str().unwrap(), "v2"); /// assert!(s3.is_none()); /// - /// assert!(term.dict_of_opt([b"k1", b"k2", b"k3"], false).is_err()); + /// assert!(term.dict_of_opt(["k1", "k2", "k3"], false).is_err()); /// ``` - pub fn dict_of_opt( + pub fn dict_of_opt>( &self, - keys: [&'static [u8]; N], + keys: [T; N], allow_extra_keys: bool, ) -> Result<[Option>; N], TypeError> { match self.0.mkref() { @@ -318,16 +344,123 @@ impl<'a, 'b> Term<'a, 'b> { if !allow_extra_keys { // Check that dictionnary contains no extraneous keys for k in dict.keys() { - if !keys.contains(k) { + if !keys.iter().any(|x| x.as_ref() == *k) { return Err(TypeError::UnexpectedKey(debug(k).to_string())); } } } - Ok(keys.map(|k| dict.get(k).map(|x| Term(x.mkref())))) + Ok(keys.map(|k| dict.get(k.as_ref()).map(|x| Term(x.mkref())))) } _ => Err(TypeError::WrongType("DICT")), } } + + // ---- TYPE CASTS ---- + + /// Try to interpret this str as an i64 + /// + /// Example: + /// + /// ``` + /// use nettext::dec::decode; + /// + /// let term = decode(b"42").unwrap(); + /// assert_eq!(term.int().unwrap(), 42); + /// ``` + pub fn int(&self) -> Result { + self.str()? + .parse::() + .map_err(|_| TypeError::WrongType("INT")) + } + + /// Try to interpret this string as base64-encoded bytes (uses URL-safe, no-padding encoding) + /// + /// Example: + /// + /// ``` + /// use nettext::dec::decode; + /// + /// let term = decode(b"aGVsbG8sIHdvcmxkIQ").unwrap(); + /// assert_eq!(term.bytes().unwrap(), b"hello, world!"); + /// ``` + pub fn bytes(&self) -> Result, TypeError> { + let encoded = match &self.0 { + AnyTerm::Str(s) => s, + AnyTerm::List(r, l) if l.iter().all(|x| matches!(x, NonListTerm::Str(_))) => r, + _ => return Err(TypeError::WrongType("BYTES")), + }; + base64::decode_config(encoded, base64::URL_SAFE_NO_PAD) + .map_err(|_| TypeError::WrongType("BYTES")) + } + + /// Try to interpret this string as base64-encoded bytes, + /// with an exact length. + /// + /// Example: + /// + /// ``` + /// use nettext::dec::decode; + /// + /// let term = decode(b"aGVsbG8sIHdvcmxkIQ").unwrap(); + /// assert_eq!(&term.bytes_exact::<13>().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 = "blake2")] +impl<'a, 'b> Term<'a, 'b> { + /// Try to interpret this string as a Blake2b512 digest (32-bytes base64 encoded) + /// + /// Example: + /// + /// ``` + /// use nettext::dec::decode; + /// + /// let term = decode(b"{ + /// message = hello, + /// hash = 5M-jmj03vjHFlgnoB5cHmcqmihm_qhUTXxZQheAdQaZboeGxRq62vQCStJ6sIUwQPM-jo2WVS7vlL3Sis2IMlA + /// }").unwrap(); + /// let [msg, hash] = term.dict_of(["message", "hash"], false).unwrap(); + /// assert!(hash.b2sum().unwrap().check(msg.raw()).is_ok()); + /// ``` + pub fn b2sum(&self) -> Result { + Ok(Blake2Sum::from_bytes(self.bytes_exact()?)) + } +} + +#[cfg(feature = "ed25519-dalek")] +impl<'a, 'b> Term<'a, 'b> { + /// Try to interpret this string as an ed25519 keypair (64 bytes base64 encoded) + pub fn keypair(&self) -> Result { + let bytes = self.bytes_exact::<64>()?; + ed25519_dalek::Keypair::from_bytes(&bytes).map_err(|_| TypeError::WrongType("KEYPAIR")) + } + + /// Try to interpret this string as an ed25519 public key (32 bytes base64 encoded) + pub fn public_key(&self) -> Result { + let bytes = self.bytes_exact::<32>()?; + ed25519_dalek::PublicKey::from_bytes(&bytes).map_err(|_| TypeError::WrongType("PUBLICKEY")) + } + + /// Try to interpret this string as an ed25519 secret key (32 bytes base64 encoded) + pub fn secret_key(&self) -> Result { + let bytes = self.bytes_exact::<32>()?; + ed25519_dalek::SecretKey::from_bytes(&bytes).map_err(|_| TypeError::WrongType("SECRETKEY")) + } + + /// Try to interpret this string as an ed25519 signature (64 bytes base64 encoded) + pub fn signature(&self) -> Result { + let bytes = self.bytes_exact::<64>()?; + ed25519_dalek::Signature::from_bytes(&bytes).map_err(|_| TypeError::WrongType("SIGNATURE")) + } } // ---- INTERNAL IMPLS ---- @@ -371,7 +504,7 @@ impl<'a, 'b> NonListTerm<'a, 'b> { } } -// ---- DISPLAY REPR = Raw NetText representation ---- +// ---- DISPLAY REPR = Raw nettext representation ---- impl<'a, 'b> std::fmt::Display for AnyTerm<'a, 'b> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { @@ -391,7 +524,7 @@ impl<'a, 'b> std::fmt::Display for Term<'a, 'b> { // ---- DEBUG REPR ---- -pub fn debug(x: &[u8]) -> &str { +pub(crate) fn debug(x: &[u8]) -> &str { std::str::from_utf8(x).unwrap_or("") } diff --git a/src/lib.rs b/src/lib.rs index df63aae..7ceabba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,2 @@ +pub mod crypto; pub mod dec;