diff --git a/Cargo.toml b/Cargo.toml index 1d07763..5087561 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ nom = "7.1" base64 = "0.13" blake2 = { version = "0.10", optional = true } +rand = "0.7" ed25519-dalek = { version = "1.0", optional = true } [features] diff --git a/src/crypto/b2.rs b/src/crypto/b2.rs index 3026d12..48e52ec 100644 --- a/src/crypto/b2.rs +++ b/src/crypto/b2.rs @@ -1,30 +1,42 @@ use blake2::{Blake2b512, Digest}; use crate::crypto::CryptoError; +use crate::enc; /// A Blake2b512 digest #[derive(Clone, Copy, Eq, PartialEq)] pub struct Blake2Sum([u8; 64]); impl Blake2Sum { - /// Create a Blake2Sum object by passing the digest as bytes directly - pub fn from_bytes(bytes: [u8; 64]) -> Self { - Self(bytes) - } + /// Create a Blake2Sum object by passing the digest as bytes directly + pub fn from_bytes(bytes: [u8; 64]) -> Self { + Self(bytes) + } - /// Compute the Blake2b512 digest of a byte slice - pub fn compute(buf: &[u8]) -> Self { - let mut hasher = Blake2b512::new(); - hasher.update(buf); - Self(hasher.finalize()[..].try_into().unwrap()) - } + /// Compute the Blake2b512 digest of a byte slice + pub fn compute(buf: &[u8]) -> Self { + let mut hasher = Blake2b512::new(); + hasher.update(buf); + Self(hasher.finalize()[..].try_into().unwrap()) + } - /// Check that this digest corresponds to a given slice - pub fn check(&self, buf: &[u8]) -> Result<(), CryptoError> { - if Self::compute(buf) == *self { - Ok(()) - } else { - Err(CryptoError::InvalidHash) - } - } + /// Check that this digest corresponds to a given slice + pub fn verify(&self, buf: &[u8]) -> Result<(), CryptoError> { + if Self::compute(buf) == *self { + Ok(()) + } else { + Err(CryptoError::InvalidHash) + } + } + + /// Return a reference to the inner byte slice + pub fn as_bytes(&self) -> &[u8] { + &self.0[..] + } +} + +impl enc::Encode for Blake2Sum { + fn term(&self) -> enc::Term<'_> { + enc::bytes(self.as_bytes()) + } } diff --git a/src/crypto/ed25519.rs b/src/crypto/ed25519.rs index cfc4580..8c73e69 100644 --- a/src/crypto/ed25519.rs +++ b/src/crypto/ed25519.rs @@ -1 +1,34 @@ -pub use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature, Signer}; +use rand::prelude::*; + +use crate::enc; + +pub use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature, Signer, Verifier}; + +pub fn generate_keypair() -> Keypair { + let mut csprng = thread_rng(); + Keypair::generate(&mut csprng) +} + +impl enc::Encode for Keypair { + fn term(&self) -> enc::Term<'_> { + enc::bytes(&self.to_bytes()) + } +} + +impl enc::Encode for PublicKey { + fn term(&self) -> enc::Term<'_> { + enc::bytes(self.as_bytes()) + } +} + +impl enc::Encode for SecretKey { + fn term(&self) -> enc::Term<'_> { + enc::bytes(self.as_bytes()) + } +} + +impl enc::Encode for Signature { + fn term(&self) -> enc::Term<'_> { + enc::bytes(&self.to_bytes()) + } +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 0f099e4..6294b39 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -14,9 +14,8 @@ pub use ed25519::*; /// An error corresponding to a cryptographic check that failed pub enum CryptoError { - /// A hash verification failed + /// A hash verification failed InvalidHash, - /// A signature verification failed + /// A signature verification failed InvalidSignature, } - diff --git a/src/dec/mod.rs b/src/dec/mod.rs index bbede71..653e5f7 100644 --- a/src/dec/mod.rs +++ b/src/dec/mod.rs @@ -430,7 +430,7 @@ impl<'a, 'b> Term<'a, 'b> { /// 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()); + /// assert!(hash.b2sum().unwrap().verify(msg.raw()).is_ok()); /// ``` pub fn b2sum(&self) -> Result { Ok(crypto::Blake2Sum::from_bytes(self.bytes_exact()?)) diff --git a/src/enc/mod.rs b/src/enc/mod.rs index 376ed0e..9aa29e7 100644 --- a/src/enc/mod.rs +++ b/src/enc/mod.rs @@ -2,9 +2,10 @@ use std::collections::HashMap; -use crate::dec::decode; +use crate::dec::{self, decode}; use crate::{is_string_char, is_whitespace}; +/// A term meant to be encoded into a nettext representation pub struct Term<'a>(T<'a>); enum T<'a> { @@ -12,8 +13,10 @@ enum T<'a> { OwnedStr(Vec), Dict(HashMap<&'a [u8], T<'a>>), List(Vec>), + Err(Error), } +/// An error that happenned when creating a nettext encoder term #[derive(Debug)] pub enum Error { InvalidCharacter(u8), @@ -21,22 +24,35 @@ pub enum Error { NotADictionnary, } +// ---- helpers to transform datatypes into encoder terms ---- + +/// Trait for anything that can be encoded as nettext +pub trait Encode { + fn term(&self) -> Term<'_>; +} + +impl<'a, 'b> Encode for dec::Term<'a, 'b> { + fn term(&self) -> Term<'_> { + Term(T::Str(self.raw())) + } +} + // ---- helpers to build terms ---- -/// Encode a string (may contain whitespace) +/// Term corresponding to a string (that may contain whitespace) /// /// ``` /// use nettext::enc::*; /// -/// assert_eq!(encode(&string("Hello world .").unwrap()), b"Hello world ."); +/// assert_eq!(encode(string("Hello world .")).unwrap(), b"Hello world ."); /// ``` -pub fn string(s: &str) -> Result, Error> { +pub fn string(s: &str) -> Term<'_> { for c in s.as_bytes().iter() { if !(is_string_char(*c) || is_whitespace(*c)) { - return Err(Error::InvalidCharacter(*c)); + return Term(T::Err(Error::InvalidCharacter(*c))); } } - Ok(Term(T::Str(s.as_bytes()))) + Term(T::Str(s.as_bytes())) } /// Include a raw nettext value @@ -44,95 +60,133 @@ pub fn string(s: &str) -> Result, Error> { /// ``` /// use nettext::enc::*; /// -/// assert_eq!(encode(&raw(b"Hello { a = b, c = d} .").unwrap()), b"Hello { a = b, c = d} ."); +/// assert_eq!(encode(raw(b"Hello { a = b, c = d} .")).unwrap(), b"Hello { a = b, c = d} ."); /// ``` -pub fn raw(bytes: &[u8]) -> Result, Error> { +pub fn raw(bytes: &[u8]) -> Term<'_> { if decode(bytes).is_err() { - return Err(Error::InvalidRaw); + return Term(T::Err(Error::InvalidRaw)); } - Ok(Term(T::Str(bytes))) + Term(T::Str(bytes)) } -/// Encode a list of items +/// Term corresponding to a list of terms /// /// ``` /// use nettext::enc::*; /// -/// assert_eq!(encode(&list([ -/// string("Hello").unwrap(), -/// string("world").unwrap() -/// ])), b"Hello world"); +/// assert_eq!(encode(list([ +/// string("Hello"), +/// string("world") +/// ])).unwrap(), b"Hello world"); /// ``` pub fn list<'a, I: IntoIterator>>(terms: I) -> Term<'a> { - Term(T::List(terms.into_iter().map(|x| x.0).collect())) + let mut tmp = Vec::with_capacity(8); + for t in terms { + match t.0 { + T::Err(e) => return Term(T::Err(e)), + x => tmp.push(x), + } + } + Term(T::List(tmp)) } -/// Encode a list of items +/// Term corresponding to a dictionnary of items /// /// ``` /// use nettext::enc::*; /// -/// assert_eq!(encode(&dict([ -/// ("a", string("Hello").unwrap()), -/// ("b", string("world").unwrap()) -/// ])), b"{\n a = Hello,\n b = world,\n}"); +/// assert_eq!(encode(dict([ +/// ("a", string("Hello")), +/// ("b", string("world")) +/// ])).unwrap(), b"{\n a = Hello,\n b = world,\n}"); /// ``` pub fn dict<'a, I: IntoIterator)>>(pairs: I) -> Term<'a> { - Term(T::Dict( - pairs - .into_iter() - .map(|(k, v)| (k.as_bytes(), v.0)) - .collect(), + let mut tmp = HashMap::new(); + for (k, v) in pairs { + match v.0 { + T::Err(e) => return Term(T::Err(e)), + vv => { + tmp.insert(k.as_bytes(), vv); + } + } + } + Term(T::Dict(tmp)) +} + +/// Term corresponding to a byte slice, +/// encoding using base64 url-safe encoding without padding +/// +/// Example: +/// +/// ``` +/// use nettext::enc::*; +/// +/// assert_eq!(encode(bytes(b"hello, world!")).unwrap(), b"aGVsbG8sIHdvcmxkIQ"); +/// ``` +pub fn bytes(b: &[u8]) -> Term<'static> { + Term(T::OwnedStr( + base64::encode_config(b, base64::URL_SAFE_NO_PAD).into_bytes(), )) } impl<'a> Term<'a> { - pub fn push(self, t: Term<'a>) -> Term<'a> { - match self.0 { - T::List(mut v) => { - v.push(t.0); - Term(T::List(v)) - } - x => Term(T::List(vec![x, t.0])), + /// Append a term to an existing term. + /// Transforms the initial term into a list if necessary. + pub fn append(self, t: Term<'a>) -> Term<'a> { + match t.0 { + T::Err(e) => Term(T::Err(e)), + tt => match self.0 { + T::List(mut v) => { + v.push(tt); + Term(T::List(v)) + } + x => Term(T::List(vec![x, tt])), + }, } } - pub fn insert(self, k: &'a str, v: Term<'a>) -> Result, Error> { - match self.0 { - T::Dict(mut d) => { - d.insert(k.as_bytes(), v.0); - Ok(Term(T::Dict(d))) - } - _ => Err(Error::NotADictionnary), + /// Inserts a key-value pair into a term that is a dictionnary. + /// Fails if `self` is not a dictionnary. + pub fn insert(self, k: &'a str, v: Term<'a>) -> Term<'a> { + match v.0 { + T::Err(e) => Term(T::Err(e)), + vv => match self.0 { + T::Dict(mut d) => { + d.insert(k.as_bytes(), vv); + Term(T::Dict(d)) + } + _ => Term(T::Err(Error::NotADictionnary)), + }, } } } // ---- encoding function ---- -pub fn encode<'a>(t: &Term<'a>) -> Vec { +/// Generate the nettext representation of a term +pub fn encode<'a>(t: Term<'a>) -> Result, Error> { let mut buf = Vec::with_capacity(128); - encode_aux(&mut buf, &t.0, 0); - buf + encode_aux(&mut buf, t.0, 0)?; + Ok(buf) } -fn encode_aux<'a>(buf: &mut Vec, term: &T<'a>, indent: usize) { +fn encode_aux<'a>(buf: &mut Vec, term: T<'a>, indent: usize) -> Result<(), Error> { match term { T::Str(s) => buf.extend_from_slice(s), T::OwnedStr(s) => buf.extend_from_slice(&s), - T::Dict(d) => { + T::Dict(mut d) => { buf.extend_from_slice(b"{\n"); let indent2 = indent + 2; - let mut keys = d.keys().collect::>(); + let mut keys = d.keys().cloned().collect::>(); keys.sort(); for k in keys { - let v = d.get(k).unwrap(); + let v = d.remove(k).unwrap(); for _ in 0..indent2 { buf.push(b' '); } buf.extend_from_slice(k); buf.extend_from_slice(b" = "); - encode_aux(buf, v, indent2); + encode_aux(buf, v, indent2)?; buf.extend_from_slice(b",\n"); } for _ in 0..indent { @@ -142,7 +196,7 @@ fn encode_aux<'a>(buf: &mut Vec, term: &T<'a>, indent: usize) { } T::List(l) => { let indent2 = indent + 2; - for (i, v) in l.iter().enumerate() { + for (i, v) in l.into_iter().enumerate() { if buf.iter().rev().take_while(|c| **c != b'\n').count() > 80 { buf.push(b'\n'); for _ in 0..indent2 { @@ -151,10 +205,12 @@ fn encode_aux<'a>(buf: &mut Vec, term: &T<'a>, indent: usize) { } else if i > 0 { buf.push(b' '); } - encode_aux(buf, v, indent2); + encode_aux(buf, v, indent2)?; } } + T::Err(e) => return Err(e), } + Ok(()) } #[cfg(test)] @@ -164,12 +220,12 @@ mod tests { #[test] fn complex1() { let input = list([ - string("HELLO").unwrap(), - string("alexhelloworld").unwrap(), + string("HELLO"), + string("alexhelloworld"), dict([ - ("from", string("jxx").unwrap()), - ("subject", string("hello").unwrap()), - ("data", raw(b"{ f1 = plop, f2 = kuko }").unwrap()), + ("from", string("jxx")), + ("subject", string("hello")), + ("data", raw(b"{ f1 = plop, f2 = kuko }")), ]), ]); let expected = b"HELLO alexhelloworld { @@ -177,9 +233,9 @@ mod tests { from = jxx, subject = hello, }"; - let enc = encode(&input); + let enc = encode(input).unwrap(); eprintln!("{}", std::str::from_utf8(&enc).unwrap()); eprintln!("{}", std::str::from_utf8(&expected[..]).unwrap()); - assert_eq!(encode(&input), expected); + assert_eq!(&enc, &expected[..]); } } diff --git a/src/lib.rs b/src/lib.rs index d9f2167..d42c8ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,76 @@ +//! A text-based data format for cryptographic network protocols. +//! +//! ``` +//! use nettext::enc::*; +//! use nettext::dec::*; +//! use nettext::crypto::{self, Signer, Verifier}; +//! +//! let keypair = crypto::generate_keypair(); +//! +//! // Encode a fist object that represents a payload that will be hashed and signed +//! let text1 = encode(list([ +//! string("CALL"), +//! string("myfunction"), +//! dict([ +//! ("a", string("hello")), +//! ("b", string("world")), +//! ("c", raw(b"{ a = 12, b = 42 }")), +//! ]), +//! keypair.public.term(), +//! ])).unwrap(); +//! eprintln!("{}", std::str::from_utf8(&text1).unwrap()); +//! +//! let hash = crypto::Blake2Sum::compute(&text1); +//! let sign = keypair.sign(&text1); +//! +//! // Encode a second object that represents the signed and hashed payload +//! let text2 = encode(dict([ +//! ("hash", hash.term()), +//! ("signature", sign.term()), +//! ("payload", raw(&text1)), +//! ])).unwrap(); +//! eprintln!("{}", std::str::from_utf8(&text2).unwrap()); +//! +//! // Decode and check everything is fine +//! let object1 = decode(&text2).unwrap(); +//! let [hash, signature, payload] = object1.dict_of(["hash", "signature", "payload"], false).unwrap(); +//! assert!(hash.b2sum().unwrap().verify(payload.raw()).is_ok()); +//! assert_eq!(payload.raw(), text1); +//! +//! let object2 = decode(payload.raw()).unwrap(); +//! +//! let [verb, arg1, arg2, pubkey] = object2.list_of().unwrap(); +//! let pubkey = pubkey.public_key().unwrap(); +//! assert!(pubkey.verify(payload.raw(), &signature.signature().unwrap()).is_ok()); +//! +//! assert_eq!(verb.string().unwrap(), "CALL"); +//! assert_eq!(arg1.string().unwrap(), "myfunction"); +//! assert_eq!(pubkey, keypair.public); +//! ``` +//! +//! The value of `text1` would be as follows: +//! +//! ```raw +//! CALL myfunction { +//! a = hello, +//! b = world, +//! c = { a = 12, b = 42 }, +//! } gTjRjHtSX6OCwq3pdl9Bpg6M2h-2WkciKi0uNV8NQX0 +//! ``` +//! +//! And the value of `text2` would be as follows: +//! ```raw +//! { +//! hash = BEBZp98KF_d1rvBd5Ib8q1w_oGvrvIcKRXFv9kMB0ewOWH42OPd8qa0V_2ranV92z0mEdswftqvpAYebziTIew, +//! payload = CALL myfunction { +//! a = hello, +//! b = world, +//! c = { a = 12, b = 42 }, +//! } gTjRjHtSX6OCwq3pdl9Bpg6M2h-2WkciKi0uNV8NQX0, +//! signature = rAwIUTsCdoB_4eqo7r5e_J5ZHFaxHnXi99oNWi7h7y0mRfgt5u7-qXn7spIN1GcmDWYh4EPzoY34Br-sRxi0AA, +//! } +//! ``` + pub mod crypto; pub mod dec; pub mod enc;