From d044c9c78b191e383e4c0acd50077e3d985f013d Mon Sep 17 00:00:00 2001 From: Calvin Hill Date: Mon, 26 Nov 2018 14:09:53 +0000 Subject: [PATCH] branca: Cleanup implementation and add some error checking. --- Cargo.toml | 11 ++-- src/errors.rs | 36 ++++++++++ src/lib.rs | 178 ++++++++++++++++++++++++++++++++++---------------- test/main.rs | 21 +++--- 4 files changed, 172 insertions(+), 74 deletions(-) create mode 100644 src/errors.rs diff --git a/Cargo.toml b/Cargo.toml index efc11dc..d9aaec7 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "branca" -description = "A Rust implementation of branca, a JWT alternative." +description = "A Rust implementation of the branca specification, a JWT alternative." version = "0.1.0" authors = ["return"] keywords = ["fernet", "branca", "crypto", "jwt"] @@ -10,15 +10,12 @@ license = "MIT" documentation = "https://docs.rs/branca" [dependencies] - base-x = "0.2.3" byteorder = "1.2.7" chrono = { version = "0.4.6", features = ["serde"] } -hex = "0.3.2" -# Will change this to point a sodiumoxide release that has crypto_aead_xchacha20poly1305_ietf_* functions. -# In the meantime, use a local version instead. -sodiumoxide = { version = "0.1.0"} -serde = "^1" +# Will change this to point either sodiumoxide or ring release that supports XChaCha20-Poly1305 in the next release. +# In the meantime, use a local version of sodiumoxide-xchacha20poly1305. +sodiumoxide-xchacha20poly1305 = {version = "^0.1.0"} [[example]] name = "example" diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..df7b5d0 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,36 @@ +use std::{fmt, result}; +use std::error::Error as StdErr; + + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum Error { + InvalidBase62Token, + InvalidTokenVersion, + BadNonceLength, + BadKeyLength, + ExpiredToken, + DecryptFailed, + SodiumInitFailed, +} + +impl fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{}", self.description()) + } +} + +impl StdErr for Error { + fn description(&self) -> &str { + match *self { + Error::InvalidBase62Token => "Base62 token is invalid.", + Error::InvalidTokenVersion => "Token version is invalid.", + Error::BadNonceLength => "Bad nonce length.", + Error::BadKeyLength => "Bad key length.", + Error::ExpiredToken => "This token has expired.", + Error::SodiumInitFailed => "Libsodium initialisation failed.", + Error::DecryptFailed => "Decryption failed." + } + } +} + +pub type Result = result::Result; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index bac5023..56b05dc 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,122 +1,190 @@ -extern crate sodiumoxide; -extern crate serde; +extern crate sodiumoxide_xchacha20poly1305 as sodiumoxide; extern crate byteorder; -extern crate chrono; extern crate base_x; +extern crate chrono; + +pub mod errors; use byteorder::*; +use base_x::{encode as b62_encode, decode as b62_decode}; use chrono::prelude::*; -use base_x::*; -use sodiumoxide::randombytes::*; -use sodiumoxide::crypto::aead::xchacha20poly1305_ietf; -use std::io::*; -use std::io::Read; +use errors::Error as BrancaError; +use sodiumoxide::crypto::aead::xchacha20poly1305_ietf as xchacha20; -// Branca magic byte +// Branca magic byte. const VERSION: u8 = 0xBA; -// Branca nonce bytes +// Branca nonce bytes. const NONCE_BYTES: usize = 24; -// Base 62 alphabet +// Base 62 alphabet. const BASE62: &'static str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Branca builder #[derive(Clone, PartialEq, Debug)] pub struct Branca { - key: Option>, - nonce: Option>, - message:Option, - ttl: Option, - timestamp: Option, + key: Vec, + nonce: Vec, + ttl: u32, + timestamp: u32, } impl Branca { pub fn new () -> Branca { Branca { - key: None, - nonce: None, - message: None, - ttl: None, - timestamp: None + key: Default::default(), + nonce: Default::default(), + ttl: Default::default(), + timestamp: Default::default() } } + pub fn key(&self) -> &Vec { + &self.key + } - pub fn set_key(mut self, key: Vec) -> Self { - self.key = Some(key); - self + pub fn nonce(&self) -> &Vec { + &self.nonce } - pub fn set_nonce(mut self, nonce:Vec ) -> Self { - self.nonce = Some(nonce); + pub fn ttl(&self) -> u32 { + self.ttl + } + + pub fn timestamp(&self) -> u32 { + self.timestamp + } + + pub fn set_key(mut self, key: Vec) -> Self { + self.key = key; self } - pub fn set_message(mut self, message:String) -> Self { - self.message = Some(message); + pub fn set_nonce(mut self, nonce:Vec ) -> Self { + self.nonce = nonce; self } pub fn set_ttl(mut self, ttl: u32) -> Self { - self.ttl = Some(ttl); + self.ttl = ttl; self } pub fn set_timestamp(mut self, timestamp: u32) -> Self { - self.timestamp = Some(timestamp); + self.timestamp = timestamp; self } - pub fn build(self) -> Result { - let key = self.key.unwrap(); - let nonce = self.nonce.unwrap(); - let message = self.message.unwrap(); - let ttl = self.ttl.unwrap(); - let timestamp = self.timestamp.unwrap(); + pub fn build(self, message: String) -> Result { + let key = self.key; + let nonce = self.nonce; + let mut timestamp = self.timestamp; + + if timestamp <= 0 { + let duration = Utc::now(); + timestamp = duration.timestamp() as u32; + } + let crypted = encode(message, key, nonce, timestamp); return Ok(crypted.unwrap()); } } -pub fn encode(msg: String, key: Vec, nonce: Vec, timestamp: u32) -> Result { +pub fn encode(msg: String, key: Vec, nonce: Vec, timestamp: u32) -> Result { - sodiumoxide::init(); + // Initialise sodiumoxide before doing anything else. + sodiumoxide::init().map_err(|_e| BrancaError::SodiumInitFailed).ok(); - let k = xchacha20poly1305_ietf::Key::from_slice(key.as_slice()).unwrap(); + // Check the nonce length before going any further. - let mut nonce_n: [u8; 24] = Default::default(); - nonce_n.copy_from_slice(nonce.as_slice()); - let nonce_b = xchacha20poly1305_ietf::Nonce(nonce_n); + if nonce.len() != 24 { + return Err(BrancaError::BadNonceLength); + } - let timestamp: u32 = timestamp; + // Check the key length before going any further. + + if key.len() != 32 { + return Err(BrancaError::BadKeyLength); + } + // We now can create a Key and Nonce struct from the inputs. + let k = xchacha20::Key::from_slice(key.as_slice()).unwrap(); + + let mut nonce_n = [0u8; NONCE_BYTES]; + nonce_n.copy_from_slice(nonce.as_slice()); + let nonce_b = xchacha20::Nonce(nonce_n); + + // The nonce is now appended to the timestamp in a Vector. let mut time_bytes = vec![0x0; 4]; BigEndian::write_u32(&mut time_bytes, timestamp); time_bytes.append(&mut Vec::from(nonce)); + // We append the version header to the timestamp vector. let mut version_header = vec![VERSION]; version_header.append(&mut time_bytes); - let mut crypted = xchacha20poly1305_ietf::seal(msg.as_bytes(), Some(version_header.as_slice()), &nonce_b, &k); + // Encrypt the payload using XChaCha20-Poly1305 AEAD // + let mut crypted = xchacha20::seal(msg.as_bytes(), Some(version_header.as_slice()), &nonce_b, &k); + // The ciphertext is appended to the version header version_header.append(&mut crypted); + + // Our payload is now encoded into base62. + let b62_enc = b62_encode(BASE62, &mut version_header.as_slice()); - let b62_enc = base_x::encode(BASE62, &mut version_header.as_slice()); - + // Return the branca token as a string. return Ok(b62_enc.to_string()); } -pub fn decode(data: String, key: String) -> Result { - let g_data = base_x::decode(BASE62, &data).unwrap(); - let k = xchacha20poly1305_ietf::Key::from_slice(key.as_bytes()).unwrap(); +pub fn decode(data: String, key: String, ttl: u32) -> Result { + + // The key must be 32 bits in size. + if key.len() != 32 { + return Err(BrancaError::BadKeyLength); + } + + + let decoded_data = b62_decode(BASE62, &data).expect("Base62 token is invalid."); + + // Obtain supplied key + let key = xchacha20::Key::from_slice(key.as_bytes()).unwrap(); - let header = &g_data[0..29]; - let ciphertext = &g_data[29..]; + // After we have decoded the data, the branca token is now represented + // by the following layout below: - let mut nonce_n: [u8; 24] = Default::default(); + // Branca( header[0..29] + ciphertext[29..] ) + // Version (&u8) || Timestamp (u32) || Nonce ([u8;24]) || Ciphertext (&[u8]) || Tag ([u8:16]) + + // We then obtain the header, ciphertext, version and timestamp with this layout. + let header = &decoded_data[0..29]; + let ciphertext = &decoded_data[29..]; + let version = &header[0]; + let timestamp = BigEndian::read_u32(&header[1..5]); + + // Obtain the nonce from the header // + let mut nonce_n = [0u8; NONCE_BYTES]; nonce_n.copy_from_slice(&header[5..]); - let nonce_b = xchacha20poly1305_ietf::Nonce(nonce_n); + let nonce_b = xchacha20::Nonce(nonce_n); + + // Check the version + if version != &VERSION { + return Err(BrancaError::InvalidTokenVersion); + } + + // Retrieve plaintext using XChaCha20-Poly1305 AEAD + let decrypted_plaintext = xchacha20::open(ciphertext, Some(header), &nonce_b, &key); - let decode = xchacha20poly1305_ietf::open(ciphertext, Some(header), &nonce_b, &k).unwrap(); + if !decrypted_plaintext.is_ok() { + return Err(BrancaError::DecryptFailed); + } + + // Timestamp check for expried token // + if ttl != 0 { + let future = timestamp + ttl; + let now = Utc::now().timestamp() as u32; + if future < now { + return Err(BrancaError::ExpiredToken); + } + } - return Ok(String::from_utf8(decode).unwrap()); + // Return the decoded string // + return Ok(String::from_utf8(decrypted_plaintext.unwrap()).unwrap()); } \ No newline at end of file diff --git a/test/main.rs b/test/main.rs index 6e64410..b5685cf 100644 --- a/test/main.rs +++ b/test/main.rs @@ -1,21 +1,19 @@ extern crate branca; -use branca::{Branca, encode, decode,}; - -fn main(){ -} +fn main(){} #[cfg(test)] mod branca_unit_tests { - use super::*; use branca::{Branca, encode, decode}; + const NONCE_BYTES: [u8;24] = [0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c]; + #[test] pub fn test_encode() { let keygen = String::from("supersecretkeyyoushouldnotcommit").into_bytes(); let message = String::from("Hello world!"); - let nonce = [0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c].to_vec(); + let nonce = NONCE_BYTES.to_vec(); let timestamp = 123206400; let branca_token = encode(message,keygen,nonce,timestamp).unwrap(); assert_eq!(branca_token, "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"); @@ -25,19 +23,18 @@ mod branca_unit_tests { pub fn test_decode() { let ciphertext = String::from("875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"); let keygen = String::from("supersecretkeyyoushouldnotcommit"); - assert_eq!(decode(ciphertext, keygen).unwrap(), "Hello world!"); + let ttl = 0; + assert_eq!(decode(ciphertext, keygen, ttl).unwrap(), "Hello world!"); } #[test] pub fn test_encode_builder() { let token = Branca::new() .set_key(String::from("supersecretkeyyoushouldnotcommit").into_bytes()) - .set_message(String::from("Hello world!")) - .set_nonce([0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c].to_vec()) + .set_nonce(NONCE_BYTES.to_vec()) .set_timestamp(123206400) - .set_ttl(3600) - .build(); - + .set_ttl(0) + .build(String::from("Hello world!")); assert_eq!(token.unwrap(), "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"); } }