#[macro_use]
extern crate log;
extern crate base64;
extern crate serde;
extern crate serde_json;
extern crate sodiumoxide;
mod caveat;
mod crypto;
mod error;
mod serialization;
mod verifier;
pub use caveat::Caveat;
pub use crypto::MacaroonKey;
pub use error::MacaroonError;
pub use serialization::Format;
pub use verifier::{Verifier, VerifyFunc};
use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
pub type Result<T> = std::result::Result<T, MacaroonError>;
pub fn initialize() -> Result<()> {
sodiumoxide::init().map_err(|_| MacaroonError::InitializationError)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct ByteString(pub Vec<u8>);
impl AsRef<[u8]> for ByteString {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for ByteString {
fn from(v: Vec<u8>) -> ByteString {
ByteString(v)
}
}
impl From<&[u8]> for ByteString {
fn from(s: &[u8]) -> ByteString {
ByteString(s.to_vec())
}
}
impl From<&str> for ByteString {
fn from(s: &str) -> ByteString {
ByteString(s.as_bytes().to_vec())
}
}
impl From<String> for ByteString {
fn from(s: String) -> ByteString {
ByteString(s.as_bytes().to_vec())
}
}
impl From<[u8; 32]> for ByteString {
fn from(b: [u8; 32]) -> ByteString {
ByteString(b.to_vec())
}
}
impl From<MacaroonKey> for ByteString {
fn from(k: MacaroonKey) -> ByteString {
ByteString(k.to_vec())
}
}
impl fmt::Display for ByteString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", base64::encode(&self.0))
}
}
impl Serialize for ByteString {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
struct ByteStringVisitor;
impl<'de> Visitor<'de> for ByteStringVisitor {
type Value = ByteString;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("base64 encoded string of bytes")
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
let raw = match base64::decode(value) {
Ok(v) => v,
Err(_) => return Err(E::custom("unable to base64 decode value")),
};
Ok(ByteString(raw))
}
}
impl<'de> Deserialize<'de> for ByteString {
fn deserialize<D>(deserializer: D) -> std::result::Result<ByteString, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(ByteStringVisitor)
}
}
fn base64_decode_flexible(b: &[u8]) -> Result<Vec<u8>> {
if b.is_empty() {
return Err(MacaroonError::DeserializationError(
"empty token to deserialize".to_string(),
));
}
if b.contains(&b'_') || b.contains(&b'-') {
Ok(base64::decode_config(b, base64::URL_SAFE)?)
} else {
Ok(base64::decode_config(b, base64::STANDARD)?)
}
}
#[test]
fn test_base64_decode_flexible() {
let val = b"Ou?T".to_vec();
assert_eq!(val, base64_decode_flexible(b"T3U/VA==").unwrap());
assert_eq!(val, base64_decode_flexible(b"T3U_VA==").unwrap());
assert_eq!(val, base64_decode_flexible(b"T3U/VA").unwrap());
assert_eq!(val, base64_decode_flexible(b"T3U_VA").unwrap());
assert!(base64_decode_flexible(b"...").is_err());
assert!(base64_decode_flexible(b"").is_err());
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Macaroon {
identifier: ByteString,
location: Option<String>,
signature: MacaroonKey,
caveats: Vec<Caveat>,
}
impl Macaroon {
pub fn create(
location: Option<String>,
key: &MacaroonKey,
identifier: ByteString,
) -> Result<Macaroon> {
let macaroon: Macaroon = Macaroon {
location,
identifier: identifier.clone(),
signature: crypto::hmac(key, &identifier),
caveats: Vec::new(),
};
debug!("Macaroon::create: {:?}", macaroon);
macaroon.validate()
}
pub fn identifier(&self) -> ByteString {
self.identifier.clone()
}
pub fn location(&self) -> Option<String> {
self.location.clone()
}
pub fn signature(&self) -> MacaroonKey {
self.signature
}
pub fn caveats(&self) -> Vec<Caveat> {
self.caveats.clone()
}
pub fn first_party_caveats(&self) -> Vec<Caveat> {
self.caveats
.iter()
.filter(|c| matches!(c, caveat::Caveat::FirstParty(_)))
.cloned()
.collect()
}
pub fn third_party_caveats(&self) -> Vec<Caveat> {
self.caveats
.iter()
.filter(|c| matches!(c, caveat::Caveat::ThirdParty(_)))
.cloned()
.collect()
}
fn validate(self) -> Result<Self> {
if self.identifier.0.is_empty() {
return Err(MacaroonError::IncompleteMacaroon("no identifier found"));
}
if self.signature.is_empty() {
return Err(MacaroonError::IncompleteMacaroon("no signature found"));
}
Ok(self)
}
pub fn add_first_party_caveat(&mut self, predicate: ByteString) {
let caveat: caveat::Caveat = caveat::new_first_party(predicate);
self.signature = caveat.sign(&self.signature);
self.caveats.push(caveat);
debug!("Macaroon::add_first_party_caveat: {:?}", self);
}
pub fn add_third_party_caveat(&mut self, location: &str, key: &MacaroonKey, id: ByteString) {
let vid: Vec<u8> = crypto::encrypt_key(&self.signature, key);
let caveat: caveat::Caveat = caveat::new_third_party(id, ByteString(vid), location);
self.signature = caveat.sign(&self.signature);
self.caveats.push(caveat);
debug!("Macaroon::add_third_party_caveat: {:?}", self);
}
pub fn bind(&self, discharge: &mut Macaroon) {
let zero_key = MacaroonKey::from([0; 32]);
discharge.signature = crypto::hmac2(&zero_key, &self.signature, &discharge.signature);
debug!(
"Macaroon::bind: original: {:?}, discharge: {:?}",
self, discharge
);
}
pub fn serialize(&self, format: serialization::Format) -> Result<String> {
match format {
serialization::Format::V1 => serialization::v1::serialize(self),
serialization::Format::V2 => serialization::v2::serialize(self),
serialization::Format::V2JSON => serialization::v2json::serialize(self),
}
}
pub fn deserialize<T: AsRef<[u8]>>(token: T) -> Result<Macaroon> {
if token.as_ref().is_empty() {
return Err(MacaroonError::DeserializationError(
"empty token provided".to_string(),
));
}
let mac: Macaroon = match token.as_ref()[0] as char {
'{' => serialization::v2json::deserialize(token.as_ref())?,
_ => {
let binary = base64_decode_flexible(token.as_ref())?;
Macaroon::deserialize_binary(&binary)?
}
};
mac.validate()
}
pub fn deserialize_binary(token: &[u8]) -> Result<Macaroon> {
if token.is_empty() {
return Err(MacaroonError::DeserializationError(
"empty macaroon token".to_string(),
));
}
let mac: Macaroon = match token[0] as char {
'\x02' => serialization::v2::deserialize(token)?,
'a'..='f' | 'A'..='Z' | '0'..='9' => serialization::v1::deserialize(token)?,
_ => {
return Err(MacaroonError::DeserializationError(
"unknown macaroon serialization format".to_string(),
))
}
};
mac.validate()
}
}
#[cfg(test)]
mod tests {
use crate::{ByteString, Caveat, Macaroon, MacaroonError, MacaroonKey, Result, Verifier};
#[test]
fn create_macaroon() {
let signature: MacaroonKey = [
20, 248, 23, 46, 70, 227, 253, 33, 123, 35, 116, 236, 130, 131, 211, 16, 41, 184, 51,
65, 213, 46, 109, 76, 49, 201, 186, 92, 114, 163, 214, 231,
]
.into();
let key = MacaroonKey::from(b"this is a super duper secret key");
let macaroon_res = Macaroon::create(Some("location".into()), &key, "identifier".into());
assert!(macaroon_res.is_ok());
let macaroon = macaroon_res.unwrap();
assert!(macaroon.location.is_some());
assert_eq!("location", macaroon.location.unwrap());
assert_eq!(ByteString::from("identifier"), macaroon.identifier);
assert_eq!(signature, macaroon.signature);
assert_eq!(0, macaroon.caveats.len());
}
#[test]
fn create_invalid_macaroon() {
let key = MacaroonKey::from(b"this is a super duper secret key");
let macaroon_res: Result<Macaroon> =
Macaroon::create(Some("location".into()), &key, "".into());
assert!(macaroon_res.is_err());
assert!(matches!(
macaroon_res,
Err(MacaroonError::IncompleteMacaroon(_))
));
println!("{}", macaroon_res.unwrap_err());
}
#[test]
fn create_macaroon_errors() {
let deser_err = Macaroon::deserialize(b"\0");
assert!(matches!(
deser_err,
Err(MacaroonError::DeserializationError(_))
));
println!("{}", deser_err.unwrap_err());
let key = MacaroonKey::generate(b"this is a super duper secret key");
let mut mac =
Macaroon::create(Some("http://mybank".into()), &key, "identifier".into()).unwrap();
let mut ver = Verifier::default();
let wrong_key = MacaroonKey::generate(b"not what was expected");
let sig_err = ver.verify(&mac, &wrong_key, Default::default());
assert!(matches!(sig_err, Err(MacaroonError::InvalidSignature)));
println!("{}", sig_err.unwrap_err());
assert!(ver.verify(&mac, &key, Default::default()).is_ok());
mac.add_first_party_caveat("account = 3735928559".into());
let cav_err = ver.verify(&mac, &key, Default::default());
assert!(matches!(cav_err, Err(MacaroonError::CaveatNotSatisfied(_))));
println!("{}", cav_err.unwrap_err());
ver.satisfy_exact("account = 3735928559".into());
assert!(ver.verify(&mac, &key, Default::default()).is_ok());
let mut mac2 = mac.clone();
let cav_key = MacaroonKey::generate(b"My key");
mac2.add_third_party_caveat("other location", &cav_key, "other ident".into());
let cav_err = ver.verify(&mac2, &key, Default::default());
assert!(matches!(cav_err, Err(MacaroonError::CaveatNotSatisfied(_))));
println!("{}", cav_err.unwrap_err());
let discharge = Macaroon::create(
Some("http://auth.mybank/".into()),
&cav_key,
"other keyid".into(),
)
.unwrap();
let disch_err = ver.verify(&mac, &key, vec![discharge]);
assert!(matches!(disch_err, Err(MacaroonError::DischargeNotUsed)));
println!("{}", disch_err.unwrap_err());
}
#[test]
fn create_macaroon_with_first_party_caveat() {
let signature: MacaroonKey = [
14, 23, 21, 148, 48, 224, 4, 143, 81, 137, 60, 25, 201, 198, 245, 250, 249, 62, 233,
94, 93, 65, 247, 88, 25, 39, 170, 203, 8, 4, 167, 187,
]
.into();
let key = MacaroonKey::from(b"this is a super duper secret key");
let mut macaroon =
Macaroon::create(Some("location".into()), &key, "identifier".into()).unwrap();
macaroon.add_first_party_caveat("predicate".into());
assert_eq!(1, macaroon.caveats.len());
let predicate = match &macaroon.caveats[0] {
Caveat::FirstParty(fp) => fp.predicate(),
_ => ByteString::default(),
};
assert_eq!(ByteString::from("predicate"), predicate);
assert_eq!(signature, macaroon.signature);
assert_eq!(&macaroon.caveats[0], &macaroon.first_party_caveats()[0]);
}
#[test]
fn create_macaroon_with_third_party_caveat() {
let key = MacaroonKey::from(b"this is a super duper secret key");
let mut macaroon =
Macaroon::create(Some("location".into()), &key, "identifier".into()).unwrap();
let location = "https://auth.mybank.com";
let cav_key = MacaroonKey::generate(b"My key");
let id = "My Caveat";
macaroon.add_third_party_caveat(location, &cav_key, id.into());
assert_eq!(1, macaroon.caveats.len());
let cav_id = match &macaroon.caveats[0] {
Caveat::ThirdParty(tp) => tp.id(),
_ => ByteString::default(),
};
let cav_location = match &macaroon.caveats[0] {
Caveat::ThirdParty(tp) => tp.location(),
_ => String::default(),
};
assert_eq!(location, cav_location);
assert_eq!(ByteString::from(id), cav_id);
assert_eq!(&macaroon.caveats[0], &macaroon.third_party_caveats()[0]);
}
#[test]
fn test_deserialize_bad_data() {
assert!(Macaroon::deserialize(b"").is_err());
assert!(Macaroon::deserialize(b"12345").is_err());
assert!(Macaroon::deserialize(b"\0").is_err());
assert!(Macaroon::deserialize(b"NDhJe_A==").is_err());
assert!(Macaroon::deserialize(&vec![10]).is_err());
assert!(Macaroon::deserialize(&vec![70, 70, 102, 70]).is_err());
assert!(Macaroon::deserialize(&vec![2, 2, 212, 212, 212, 212]).is_err());
}
}
#[cfg(doctest)]
mod test_readme {
macro_rules! external_doc_test {
($x:expr) => {
#[doc = $x]
extern "C" {}
};
}
external_doc_test!(include_str!("../README.md"));
}