Add crypto and documentation

This commit is contained in:
Alex 2022-11-17 16:35:06 +01:00
parent 2ae904cc8a
commit 4d4f9336d6
Signed by: lx
GPG Key ID: 0E496D15096376BE
5 changed files with 229 additions and 71 deletions

View File

@ -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" ]

36
src/crypto.rs Normal file
View File

@ -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::*;

View File

@ -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<T, DecodeError<'a>>;
impl<'a> From<nom::Err<nom::error::Error<&'a [u8]>>> for DecodeError<'a> {
fn from(e: nom::Err<nom::error::Error<&'a [u8]>>) -> DecodeError<'a> {
match e {
@ -53,8 +50,8 @@ impl<'a> From<nom::Err<nom::error::Error<&'a [u8]>>> 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<Term<'_, 'static>, 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::<HashMap<_, _>>();
@ -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)))
}

View File

@ -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<AnyTerm<'a, 'static>> 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<std::str::Utf8Error> 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<Term<'a, '_>> {
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<const N: usize>(&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<HashMap<&'a [u8], Term<'a, '_>>, TypeError> {
pub fn dict(&self) -> Result<HashMap<&'a str, Term<'a, '_>>, 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<const N: usize>(
pub fn dict_of<const N: usize, T: AsRef<[u8]>>(
&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<const N: usize>(
pub fn dict_of_opt<const N: usize, T: AsRef<[u8]>>(
&self,
keys: [&'static [u8]; N],
keys: [T; N],
allow_extra_keys: bool,
) -> Result<[Option<Term<'a, '_>>; 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<i64, TypeError> {
self.str()?
.parse::<i64>()
.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<Vec<u8>, 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<const N: usize>(&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<Blake2Sum, TypeError> {
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<ed25519_dalek::Keypair, TypeError> {
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<ed25519_dalek::PublicKey, TypeError> {
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<ed25519_dalek::SecretKey, TypeError> {
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<ed25519_dalek::Signature, TypeError> {
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("<invalid ascii>")
}

View File

@ -1 +1,2 @@
pub mod crypto;
pub mod dec;