extern crate base_x;
extern crate byteorder;
extern crate orion;
#[cfg(test)]
extern crate serde;
#[cfg(test)]
#[macro_use]
extern crate serde_json;
#[cfg(test)]
#[macro_use]
extern crate serde_derive;
pub mod errors;
use self::errors::Error as BrancaError;
use base_x::{decode as b62_decode, encode as b62_encode};
use byteorder::*;
use orion::errors::UnknownCryptoError;
use orion::hazardous::aead::xchacha20poly1305::*;
use orion::hazardous::stream::chacha20::CHACHA_KEYSIZE;
use orion::hazardous::stream::xchacha20::XCHACHA_NONCESIZE;
use orion::util::secure_rand_bytes;
use std::str;
use std::time::{SystemTime, UNIX_EPOCH};
const VERSION: u8 = 0xBA;
const BASE62: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
#[derive(Clone)]
pub struct Branca {
key: Vec<u8>,
nonce: Vec<u8>,
ttl: u32,
timestamp: u32,
}
impl PartialEq for Branca {
fn eq(&self, other: &Branca) -> bool {
let key_eq: bool =
orion::util::secure_cmp(self.key[..].as_ref(), other.key[..].as_ref()).is_ok();
key_eq
& (self.nonce == other.nonce)
& (self.ttl == other.ttl)
& (self.timestamp == other.timestamp)
}
}
impl core::fmt::Debug for Branca {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(
f,
"Branca {{ key: [SECRET VALUE], nonce: {:?}, ttl: {:?},
timestamp: {:?} }}",
self.nonce, self.ttl, self.timestamp
)
}
}
impl Branca {
pub fn new(key: &[u8]) -> Result<Branca, BrancaError> {
if key.len() != CHACHA_KEYSIZE {
return Err(BrancaError::BadKeyLength);
}
Ok(Branca {
key: key.to_vec(),
nonce: Vec::new(),
ttl: 0,
timestamp: 0,
})
}
pub fn key(self) -> Vec<u8> {
self.key
}
pub fn nonce(self) -> Vec<u8> {
self.nonce
}
pub fn ttl(self) -> u32 {
self.ttl
}
pub fn timestamp(self) -> u32 {
self.timestamp
}
pub fn set_key(&mut self, key: Vec<u8>) -> &mut Self {
self.key = key;
self
}
pub fn set_ttl(&mut self, ttl: u32) -> &mut Self {
self.ttl = ttl;
self
}
pub fn set_timestamp(&mut self, timestamp: u32) -> &mut Self {
self.timestamp = timestamp;
self
}
pub fn encode(&mut self, message: &[u8]) -> Result<String, BrancaError> {
let mut timestamp = self.timestamp;
if timestamp == 0 {
let ts = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("Failed to obtain timestamp from system clock.");
timestamp = ts.as_secs() as u32;
}
let mut nonce = [0; XCHACHA_NONCESIZE];
secure_rand_bytes(&mut nonce).unwrap();
self.nonce = nonce.to_vec();
encode_with_nonce(message, &self.key, &Nonce::from(nonce), timestamp)
}
pub fn decode(&self, ciphertext: &str, ttl: u32) -> Result<Vec<u8>, BrancaError> {
decode(ciphertext, &self.key, ttl)
}
}
pub fn encode(data: &[u8], key: &[u8], timestamp: u32) -> Result<String, BrancaError> {
let n = Nonce::generate();
encode_with_nonce(data, key, &n, timestamp)
}
fn encode_with_nonce(
data: &[u8],
key: &[u8],
nonce: &Nonce,
timestamp: u32,
) -> Result<String, BrancaError> {
let sk: SecretKey = match SecretKey::from_slice(key) {
Ok(key) => key,
Err(UnknownCryptoError) => return Err(BrancaError::BadKeyLength),
};
let mut header = [0u8; 29];
header[0] = VERSION;
BigEndian::write_u32(&mut header[1..5], timestamp);
header[5..29].copy_from_slice(nonce.as_ref());
let mut buf_crypt = vec![0u8; data.len() + 16 + 29]; buf_crypt[..29].copy_from_slice(header.as_ref());
match seal(
&sk,
nonce,
data,
Some(header.as_ref()),
&mut buf_crypt[29..],
) {
Ok(()) => (),
Err(UnknownCryptoError) => return Err(BrancaError::EncryptFailed),
};
Ok(b62_encode(BASE62, buf_crypt.as_ref()))
}
pub fn decode(data: &str, key: &[u8], ttl: u32) -> Result<Vec<u8>, BrancaError> {
let (timestamp, buf_crypt) = decode_with_timestamp(data, key)?;
if ttl != 0 {
let future = match timestamp.checked_add(ttl) {
Some(value) => value as u64,
None => return Err(BrancaError::OverflowingOperation),
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Failed to obtain timestamp from system clock.")
.as_secs();
if future < now {
return Err(BrancaError::ExpiredToken);
}
}
Ok(buf_crypt)
}
pub fn decode_with_timestamp(
data: &str,
key: &[u8],
) -> Result<(u32, Vec<u8>), BrancaError> {
let sk: SecretKey = match SecretKey::from_slice(key) {
Ok(key) => key,
Err(UnknownCryptoError) => return Err(BrancaError::BadKeyLength),
};
if data.len() < 61 {
return Err(BrancaError::InvalidBase62Token);
}
let decoded_data = match b62_decode(BASE62, data) {
Ok(decoded) => decoded,
Err(_) => return Err(BrancaError::InvalidBase62Token),
};
if decoded_data[0] != VERSION {
return Err(BrancaError::InvalidTokenVersion);
}
let header = &decoded_data[0..29];
let n: Nonce = Nonce::from_slice(decoded_data[5..29].as_ref()).unwrap();
let mut buf_crypt = vec![0u8; decoded_data.len() - 16 - 29];
match open(
&sk,
&n,
decoded_data[29..].as_ref(),
Some(header),
&mut buf_crypt,
) {
Ok(()) => (),
Err(orion::errors::UnknownCryptoError) => return Err(BrancaError::DecryptFailed),
};
let timestamp: u32 = BigEndian::read_u32(&decoded_data[1..5]);
Ok((timestamp, buf_crypt))
}
#[cfg(test)]
mod unit_tests {
use super::*;
mod json_test_vectors {
use super::*;
use std::fs::File;
use std::io::BufReader;
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize, Debug)]
struct TestFile {
version: String,
numberOfTests: u32,
testGroups: Vec<TestGroup>,
}
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize, Debug)]
struct TestGroup {
testType: String,
tests: Vec<TestVector>,
}
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize, Debug)]
struct TestVector {
id: u32,
comment: String,
key: String,
nonce: Option<String>,
timestamp: u32,
token: String,
msg: String,
isValid: bool,
}
fn parse_hex(data: &str) -> Vec<u8> {
match data {
"" => vec![0u8; 0],
"80" => b"\x80".to_vec(),
_ => hex::decode(data).unwrap(),
}
}
#[test]
pub fn run_test_vectors() {
let file = File::open("test_data/test_vectors.json").unwrap();
let reader = BufReader::new(file);
let tests: TestFile = serde_json::from_reader(reader).unwrap();
let mut tests_run = 0;
for test_group in tests.testGroups.iter() {
for test in test_group.tests.iter() {
if test_group.testType == "encoding" {
debug_assert!(test.nonce.is_some());
if test.isValid {
let nonce = Nonce::from_slice(&parse_hex(test.nonce.as_ref().unwrap()))
.unwrap();
let res = encode_with_nonce(
&parse_hex(&test.msg),
&parse_hex(&test.key),
&nonce,
test.timestamp,
)
.unwrap();
assert_eq!(res, test.token);
assert_eq!(
decode(&test.token, &parse_hex(&test.key), 0).unwrap(),
parse_hex(&test.msg)
);
tests_run += 1;
}
if !test.isValid {
let nonce = Nonce::from_slice(&parse_hex(test.nonce.as_ref().unwrap()));
if nonce.is_err() {
tests_run += 1;
continue;
}
let res = encode_with_nonce(
&parse_hex(&test.msg),
&parse_hex(&test.key),
&nonce.unwrap(),
test.timestamp,
);
assert!(res.is_err());
tests_run += 1;
}
}
if test_group.testType == "decoding" {
debug_assert!(test.nonce.is_none());
let res = decode(
&test.token,
&parse_hex(&test.key),
0, );
assert_eq!(test.isValid, res.is_ok());
tests_run += 1;
}
}
}
assert_eq!(tests_run, tests.numberOfTests);
}
}
#[derive(Serialize, Deserialize, Debug)]
struct JSONTest {
a: String,
b: bool,
}
#[test]
pub fn test_encode_builder() {
let key = b"supersecretkeyyoushouldnotcommit";
let mut token = Branca::new(key).unwrap();
let ciphertext = token.set_timestamp(123206400).encode(b"Test");
assert!(ciphertext.is_ok());
}
#[test]
pub fn test_decode() {
let ciphertext =
"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a";
let key = b"supersecretkeyyoushouldnotcommit";
let ttl = 0;
assert_eq!(decode(ciphertext, key, ttl).unwrap(), b"Hello world!");
}
#[test]
pub fn test_encode_and_decode() {
let message = b"Hello world!";
let timestamp = 123206400;
let branca_token = encode(message, b"supersecretkeyyoushouldnotcommit", timestamp).unwrap();
assert_eq!(
decode(
branca_token.as_str(),
b"supersecretkeyyoushouldnotcommit",
0
)
.unwrap(),
b"Hello world!"
);
}
#[test]
pub fn test_encode_and_decode_random_nonce() {
let message = b"Hello world!";
let timestamp = 123206400;
let branca_token = encode(message, b"supersecretkeyyoushouldnotcommit", timestamp).unwrap();
assert_eq!(
decode(
branca_token.as_str(),
b"supersecretkeyyoushouldnotcommit",
0
)
.unwrap(),
b"Hello world!"
);
}
#[test]
pub fn test_encode_and_decode_json() {
let literal_json = json!({ "a": "some string", "b": false });
let message = literal_json.to_string();
let timestamp = 123206400;
let branca_token = encode(
message.as_bytes(),
b"supersecretkeyyoushouldnotcommit",
timestamp,
)
.unwrap();
let json = decode(
branca_token.as_str(),
b"supersecretkeyyoushouldnotcommit",
0,
)
.unwrap();
let serialized_json: JSONTest =
serde_json::from_str(&String::from_utf8_lossy(&json)).unwrap();
assert_eq!(serialized_json.a, "some string");
assert!(!serialized_json.b);
}
#[test]
pub fn test_encode_and_decode_json_literal() {
let message = r#"{
"a":"some string",
"b":false
}"#;
let timestamp = 123206400;
let branca_token = encode(
message.as_bytes(),
b"supersecretkeyyoushouldnotcommit",
timestamp,
)
.unwrap();
let json = decode(
branca_token.as_str(),
b"supersecretkeyyoushouldnotcommit",
0,
)
.unwrap();
let serialized_json: JSONTest =
serde_json::from_str(&String::from_utf8_lossy(&json)).unwrap();
assert_eq!(serialized_json.a, "some string");
assert!(!serialized_json.b);
}
#[test]
pub fn test_encode_and_decode_builder() {
let key = b"supersecretkeyyoushouldnotcommit";
let mut token = Branca::new(key).unwrap();
let ciphertext = token.encode(b"Test").unwrap();
let payload = token.decode(ciphertext.as_str(), 0).unwrap();
assert_eq!(payload, b"Test");
}
#[test]
pub fn test_encode_and_decode_builder_with_exp_ttl() {
let key = b"supersecretkeyyoushouldnotcommit";
let mut token = Branca::new(key).unwrap();
let ciphertext = token.set_timestamp(123206400).encode(b"Test").unwrap();
let payload = token.decode(ciphertext.as_str(), 0);
if let Err(e) = payload {
assert_eq!(e, BrancaError::ExpiredToken)
}
}
#[test]
pub fn test_expired_ttl() {
let ciphertext =
"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a";
let key = b"supersecretkeyyoushouldnotcommit";
let ttl = 3600;
let message = decode(ciphertext, key, ttl);
if let Err(e) = message {
assert_eq!(e, BrancaError::ExpiredToken)
}
}
#[test]
pub fn test_decryption_fail() {
let ciphertext =
"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a";
let key = b"supersecretkeyyoushouldnotcommi.";
let ttl = 0;
let branca_token = decode(ciphertext, key, ttl);
if let Err(e) = branca_token {
assert_eq!(e, BrancaError::DecryptFailed)
}
}
#[test]
pub fn test_base62_fail() {
let ciphertext = "875GH233T7IYrxtgXxlQBYiFo";
let key = b"supersecretkeyyoushouldnotcommit";
let ttl = 0;
let branca_token = decode(ciphertext, key, ttl);
if let Err(e) = branca_token {
assert_eq!(e, BrancaError::InvalidBase62Token)
}
}
#[test]
pub fn test_bad_key() {
let key = b"supersecretkey";
let message = b"Hello world!";
let timestamp = 123206400;
let branca_token = encode(message, key, timestamp);
if let Err(e) = branca_token {
assert_eq!(e, BrancaError::BadKeyLength)
}
}
#[test]
pub fn test_version_mismatch() {
let ciphertext =
"005GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a";
let key = b"supersecretkeyyoushouldnotcommit";
let ttl = 0;
let branca_token = decode(ciphertext, key, ttl);
if let Err(e) = branca_token {
assert_eq!(e, BrancaError::InvalidTokenVersion)
}
}
#[test]
pub fn test_modified_timestamp_returns_bad_tag() {
let key = b"supersecretkeyyoushouldnotcommit";
let mut ctx = Branca::new(key).unwrap();
ctx.timestamp = 0;
let token = ctx.encode(b"Test").unwrap();
let mut decoded = b62_decode(BASE62, &token).unwrap();
BigEndian::write_u32(&mut decoded[1..5], 651323084);
assert_eq!(
decode(&b62_encode(BASE62, &decoded), key, 1000).unwrap_err(),
BrancaError::DecryptFailed
);
}
#[test]
pub fn test_no_panic_on_display() {
let _tostr = BrancaError::InvalidTokenVersion.to_string();
}
#[test]
pub fn test_empty_payload_encode_decode() {
let key = b"supersecretkeyyoushouldnotcommit";
let mut ctx = Branca::new(key).unwrap();
assert!(ctx.encode(b"").is_ok());
let decoded = ctx
.decode(
"4tGtt5wP5DCXzPhNbovMwEg9saksXSdmhvFbdrZrQjXEWf09BtuAK1wG5lpG0",
0,
)
.unwrap();
assert_eq!(b"", &decoded[..]);
}
#[test]
pub fn test_non_utf8_encode_decode() {
let key = b"supersecretkeyyoushouldnotcommit";
let mut ctx = Branca::new(key).unwrap();
let own_token = ctx.encode(b"\x80").unwrap();
assert_eq!(b"\x80", &ctx.decode(own_token.as_str(), 0).unwrap()[..]);
let decoded = ctx
.decode(
"K9i9jp23WMENUOulBifHPEnfBp67LfQBE3wYBCPSCu2uTBEeFHwGJZfH8DOTa1",
0,
)
.unwrap();
assert_eq!(b"\x80", &decoded[..]);
}
#[test]
pub fn test_correct_err_on_invalid_base62() {
let key = b"supersecretkeyyoushouldnotcommit";
let mut token = encode(b"Hello world!", key, 0).unwrap();
token.push('_');
assert_eq!(
decode(&token, key, 0).unwrap_err(),
BrancaError::InvalidBase62Token
);
}
#[test]
pub fn test_builder_nonce_is_correctly_used() {
let key = b"supersecretkeyyoushouldnotcommit";
let mut ctx = Branca::new(key).unwrap();
assert!(ctx.nonce.is_empty());
let token = ctx.encode(b"").unwrap();
assert!(!ctx.nonce.is_empty());
let raw_token = b62_decode(BASE62, &token).unwrap();
let raw_token_nonce = &raw_token[5..29];
assert_eq!(raw_token_nonce, &ctx.nonce[..]);
let token_again = ctx.encode(b"").unwrap();
let raw_token_again = b62_decode(BASE62, &token_again).unwrap();
let raw_token_nonce_again = &raw_token_again[5..29];
assert_eq!(raw_token_nonce_again, &ctx.nonce[..]);
assert_ne!(raw_token_nonce_again, raw_token_nonce);
}
#[test]
pub fn test_error_on_overflowing_timestamp() {
let key = b"supersecretkeyyoushouldnotcommit";
let token = encode(b"", key, 4294967295).unwrap();
assert_eq!(
decode(&token, key, 1).unwrap_err(),
BrancaError::OverflowingOperation
);
}
}