Skip to content

Commit

Permalink
branca: Cleanup implementation and add some error checking.
Browse files Browse the repository at this point in the history
  • Loading branch information
return committed Nov 26, 2018
1 parent d082515 commit d044c9c
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 74 deletions.
11 changes: 4 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand All @@ -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"
Expand Down
36 changes: 36 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -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<T> = result::Result<T, Error>;
178 changes: 123 additions & 55 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>>,
nonce: Option<Vec<u8>>,
message:Option<String>,
ttl: Option<u32>,
timestamp: Option<u32>,
key: Vec<u8>,
nonce: Vec<u8>,
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<u8> {
&self.key
}

pub fn set_key(mut self, key: Vec<u8>) -> Self {
self.key = Some(key);
self
pub fn nonce(&self) -> &Vec<u8> {
&self.nonce
}

pub fn set_nonce(mut self, nonce:Vec<u8> ) -> 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<u8>) -> 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<u8> ) -> 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<String> {
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<String, BrancaError> {
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<u8>, nonce: Vec<u8>, timestamp: u32) -> Result<String> {
pub fn encode(msg: String, key: Vec<u8>, nonce: Vec<u8>, timestamp: u32) -> Result<String, BrancaError> {

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<String> {
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<String, BrancaError> {

// 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());
}
21 changes: 9 additions & 12 deletions test/main.rs
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -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");
}
}

0 comments on commit d044c9c

Please sign in to comment.