use base64::{
alphabet,
engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
Engine as _,
};
use log::debug;
pub(crate) const STANDARD: GeneralPurpose = GeneralPurpose::new(
&alphabet::STANDARD,
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
pub(crate) const URL_SAFE: GeneralPurpose = GeneralPurpose::new(
&alphabet::URL_SAFE,
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
pub(crate) const URL_SAFE_NO_PAD: GeneralPurpose = GeneralPurpose::new(
&alphabet::URL_SAFE,
GeneralPurposeConfig::new()
.with_encode_padding(false)
.with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
mod caveat;
mod crypto;
mod error;
mod serialization;
mod verifier;
pub use caveat::{Caveat, FirstParty, ThirdParty};
pub use crypto::MacaroonKey;
pub use error::MacaroonError;
pub use serialization::Format;
pub use verifier::Verifier;
use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
pub type Result<T> = std::result::Result<T, MacaroonError>;
pub const MAX_CAVEATS: usize = 1000;
pub const MAX_FIELD_SIZE_BYTES: usize = 65535;
pub(crate) fn check_field_size(field: &'static str, size: usize) -> Result<()> {
if size > MAX_FIELD_SIZE_BYTES {
return Err(MacaroonError::FieldTooLarge { field, size });
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub(crate) struct ByteString(pub(crate) Vec<u8>);
impl AsRef<[u8]> for ByteString {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl std::borrow::Borrow<[u8]> for ByteString {
fn borrow(&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 fmt::Display for ByteString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", URL_SAFE_NO_PAD.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 = base64_decode_flexible(value.as_bytes())
.map_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(URL_SAFE.decode(b)?)
} else {
Ok(STANDARD.decode(b)?)
}
}
#[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: impl AsRef<[u8]>,
) -> Result<Macaroon> {
let identifier_bytes = identifier.as_ref();
check_field_size("identifier", identifier_bytes.len())?;
if let Some(loc) = &location {
check_field_size("location", loc.len())?;
}
let identifier = ByteString(identifier_bytes.to_vec());
let signature = crypto::hmac(key, &identifier);
let macaroon = Macaroon {
location,
identifier,
signature,
caveats: Vec::new(),
};
debug!("Macaroon::create: {:?}", macaroon);
macaroon.validate()
}
pub fn identifier(&self) -> &[u8] {
&self.identifier.0
}
pub fn location(&self) -> Option<&str> {
self.location.as_deref()
}
pub fn signature(&self) -> &MacaroonKey {
&self.signature
}
pub fn caveats(&self) -> &[Caveat] {
&self.caveats
}
pub fn first_party_caveats(&self) -> Vec<&Caveat> {
self.caveats
.iter()
.filter(|c| matches!(c, caveat::Caveat::FirstParty(_)))
.collect()
}
pub fn third_party_caveats(&self) -> Vec<&Caveat> {
self.caveats
.iter()
.filter(|c| matches!(c, caveat::Caveat::ThirdParty(_)))
.collect()
}
fn validate(self) -> Result<Self> {
if self.identifier.0.is_empty() {
return Err(MacaroonError::IncompleteMacaroon("no identifier found"));
}
Ok(self)
}
pub fn add_first_party_caveat(&mut self, predicate: impl AsRef<[u8]>) -> Result<()> {
let predicate_bytes = predicate.as_ref();
check_field_size("predicate", predicate_bytes.len())?;
self.check_caveat_capacity()?;
let caveat: caveat::Caveat = caveat::new_first_party(ByteString(predicate_bytes.to_vec()));
self.signature = caveat.sign(&self.signature);
self.caveats.push(caveat);
debug!("Macaroon::add_first_party_caveat: {:?}", self);
Ok(())
}
pub fn add_third_party_caveat(
&mut self,
location: &str,
key: &MacaroonKey,
id: impl AsRef<[u8]>,
) -> Result<()> {
let id_bytes = id.as_ref();
check_field_size("caveat id", id_bytes.len())?;
check_field_size("caveat location", location.len())?;
self.check_caveat_capacity()?;
let vid: Vec<u8> = crypto::encrypt_key(&self.signature, key)?;
let caveat: caveat::Caveat =
caveat::new_third_party(ByteString(id_bytes.to_vec()), ByteString(vid), location);
self.signature = caveat.sign(&self.signature);
self.caveats.push(caveat);
debug!("Macaroon::add_third_party_caveat: {:?}", self);
Ok(())
}
fn check_caveat_capacity(&self) -> Result<()> {
if self.caveats.len() >= MAX_CAVEATS {
return Err(MacaroonError::TooManyCaveats);
}
Ok(())
}
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)?,
'0'..='9' | 'a'..='f' | 'A'..='F' => serialization::v1::deserialize(token)?,
_ => {
return Err(MacaroonError::DeserializationError(
"unknown macaroon serialization format".to_string(),
))
}
};
mac.validate()
}
}
#[cfg(test)]
mod tests {
use crate::{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");
assert!(macaroon_res.is_ok());
let macaroon = macaroon_res.unwrap();
assert!(macaroon.location.is_some());
assert_eq!("location", macaroon.location.as_deref().unwrap());
assert_eq!(macaroon.identifier(), b"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, "");
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").unwrap();
let mut ver = Verifier::default();
let wrong_key = MacaroonKey::generate(b"not what was expected");
let sig_err = ver.verify(&mac, &wrong_key, &[]);
assert!(matches!(sig_err, Err(MacaroonError::InvalidSignature)));
println!("{}", sig_err.unwrap_err());
assert!(ver.verify(&mac, &key, &[]).is_ok());
mac.add_first_party_caveat("account = 3735928559").unwrap();
let cav_err = ver.verify(&mac, &key, &[]);
assert!(matches!(cav_err, Err(MacaroonError::CaveatNotSatisfied(_))));
println!("{}", cav_err.unwrap_err());
ver.satisfy_exact("account = 3735928559");
assert!(ver.verify(&mac, &key, &[]).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")
.unwrap();
let cav_err = ver.verify(&mac2, &key, &[]);
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").unwrap();
let disch_err = ver.verify(&mac, &key, &[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").unwrap();
macaroon.add_first_party_caveat("predicate").unwrap();
assert_eq!(1, macaroon.caveats.len());
let predicate = match &macaroon.caveats[0] {
Caveat::FirstParty(fp) => fp.predicate().to_vec(),
_ => Vec::new(),
};
assert_eq!(b"predicate".to_vec(), 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").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)
.unwrap();
assert_eq!(1, macaroon.caveats.len());
let cav_id = match &macaroon.caveats[0] {
Caveat::ThirdParty(tp) => tp.id().to_vec(),
_ => Vec::new(),
};
let cav_location = match &macaroon.caveats[0] {
Caveat::ThirdParty(tp) => tp.location().to_string(),
_ => String::default(),
};
assert_eq!(location, cav_location);
assert_eq!(id.as_bytes().to_vec(), 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"));
}