libmacaroon 0.2.1

Macaroons (bearer credentials with contextual caveats) in pure Rust, with first-party and third-party caveats, WASM support, and cross-language interop.
Documentation
use crate::caveat;
use crate::caveat::CaveatBuilder;
use crate::error::MacaroonError;
use crate::serialization::macaroon_builder::MacaroonBuilder;
use crate::{base64_decode_flexible, ByteString, Macaroon, Result, URL_SAFE_NO_PAD};
use base64::Engine as _;
use serde::{Deserialize, Serialize};
use serde_json;
use std::str;

#[derive(Debug, Default, Deserialize, Serialize)]
struct Caveat {
    i: Option<String>,
    i64: Option<ByteString>,
    l: Option<String>,
    l64: Option<String>,
    v: Option<String>,
    v64: Option<ByteString>,
}

#[derive(Debug, Default, Deserialize, Serialize)]
struct Serialization {
    v: u8,
    i: Option<String>,
    i64: Option<ByteString>,
    l: Option<String>,
    l64: Option<String>,
    c: Vec<Caveat>,
    s: Option<Vec<u8>>,
    s64: Option<String>,
}

impl Serialization {
    fn from_macaroon(macaroon: &Macaroon) -> Result<Serialization> {
        let mut serialized: Serialization = Serialization {
            v: 2,
            i: None,
            i64: Some(ByteString(macaroon.identifier().to_vec())),
            l: macaroon.location().map(|s| s.to_string()),
            l64: None,
            c: Vec::new(),
            s: None,
            // URL-safe, no padding — matches libmacaroons / pymacaroons
            // wire format for `s64`.
            s64: Some(URL_SAFE_NO_PAD.encode(macaroon.signature())),
        };
        for c in macaroon.caveats() {
            match c {
                caveat::Caveat::FirstParty(fp) => {
                    let serialized_caveat: Caveat = Caveat {
                        i: None,
                        i64: Some(ByteString(fp.predicate().to_vec())),
                        l: None,
                        l64: None,
                        v: None,
                        v64: None,
                    };
                    serialized.c.push(serialized_caveat);
                }
                caveat::Caveat::ThirdParty(tp) => {
                    let serialized_caveat: Caveat = Caveat {
                        i: None,
                        i64: Some(ByteString(tp.id().to_vec())),
                        l: Some(tp.location().to_string()),
                        l64: None,
                        v: None,
                        v64: Some(ByteString(tp.verifier_id().to_vec())),
                    };
                    serialized.c.push(serialized_caveat);
                }
            }
        }

        Ok(serialized)
    }
}

fn reject_both<A, B>(a: &Option<A>, b: &Option<B>, pair: &str) -> Result<()> {
    if a.is_some() && b.is_some() {
        return Err(MacaroonError::DeserializationError(format!(
            "Found {} fields",
            pair
        )));
    }
    Ok(())
}

impl Macaroon {
    fn from_json(ser: Serialization) -> Result<Macaroon> {
        if ser.v != 2 {
            return Err(MacaroonError::DeserializationError(format!(
                "Unsupported V2JSON version field: {} (expected 2)",
                ser.v
            )));
        }
        reject_both(&ser.i, &ser.i64, "i and i64")?;
        reject_both(&ser.l, &ser.l64, "l and l64")?;
        reject_both(&ser.s, &ser.s64, "s and s64")?;

        let mut builder: MacaroonBuilder = MacaroonBuilder::new();
        builder.set_identifier(match ser.i {
            Some(id) => id.into(),
            None => match ser.i64 {
                Some(id) => id,
                None => {
                    return Err(MacaroonError::DeserializationError(String::from(
                        "No identifier \
                         found",
                    )))
                }
            },
        });

        match ser.l {
            Some(loc) => builder.set_location(&loc),
            None => {
                if let Some(loc) = ser.l64 {
                    builder
                        .set_location(&String::from_utf8(base64_decode_flexible(loc.as_bytes())?)?)
                }
            }
        };

        let raw_sig = match ser.s {
            Some(sig) => sig,
            None => match ser.s64 {
                Some(sig) => base64_decode_flexible(sig.as_bytes())?,
                None => {
                    return Err(MacaroonError::DeserializationError(
                        "No signature found".into(),
                    ))
                }
            },
        };
        if raw_sig.len() != 32 {
            return Err(MacaroonError::DeserializationError(
                "Illegal signature length".into(),
            ));
        }

        builder.set_signature(&raw_sig);

        let mut caveat_builder: CaveatBuilder = CaveatBuilder::new();
        for c in ser.c {
            // Mirror the top-level exclusion checks at the caveat level so a
            // token cannot silently prefer one encoding over another. Two
            // serializers that read different fields must not see different
            // content.
            reject_both(&c.i, &c.i64, "caveat i and i64")?;
            reject_both(&c.l, &c.l64, "caveat l and l64")?;
            reject_both(&c.v, &c.v64, "caveat v and v64")?;

            caveat_builder.add_id(match c.i {
                Some(id) => id.into(),
                None => match c.i64 {
                    Some(id64) => id64,
                    None => {
                        return Err(MacaroonError::DeserializationError(String::from(
                            "No caveat ID found",
                        )))
                    }
                },
            });
            match c.l {
                Some(loc) => caveat_builder.add_location(loc),
                None => {
                    if let Some(loc64) = c.l64 {
                        caveat_builder.add_location(String::from_utf8(base64_decode_flexible(
                            loc64.as_bytes(),
                        )?)?)
                    }
                }
            };
            match c.v {
                Some(vid) => caveat_builder.add_verifier_id(vid.into()),
                None => {
                    if let Some(vid64) = c.v64 {
                        caveat_builder.add_verifier_id(vid64)
                    }
                }
            };
            builder.add_caveat(caveat_builder.build()?)?;
            caveat_builder = CaveatBuilder::new();
        }

        builder.build()
    }
}

pub fn serialize(macaroon: &Macaroon) -> Result<String> {
    let serialized: String = serde_json::to_string(&Serialization::from_macaroon(macaroon)?)?;
    Ok(serialized)
}

pub fn deserialize(data: &[u8]) -> Result<Macaroon> {
    let v2j: Serialization = serde_json::from_slice(data)?;
    Macaroon::from_json(v2j)
}

#[cfg(test)]
mod tests {
    use super::super::Format;
    use crate::{Caveat, Macaroon, MacaroonKey};

    const SERIALIZED_JSON: &str = "{\"v\":2,\"l\":\"http://example.org/\",\"i\":\"keyid\",\
                                   \"c\":[{\"i\":\"account = 3735928559\"},{\"i\":\"user = \
                                   alice\"}],\"s64\":\
                                   \"S-lnzR6gxrJrr2pKlO6bBbFYhtoLqF6MQqk8jQ4SXvw\"}";
    const SIGNATURE: [u8; 32] = [
        75, 233, 103, 205, 30, 160, 198, 178, 107, 175, 106, 74, 148, 238, 155, 5, 177, 88, 134,
        218, 11, 168, 94, 140, 66, 169, 60, 141, 14, 18, 94, 252,
    ];

    #[test]
    fn test_deserialize() {
        let serialized_json: Vec<u8> = SERIALIZED_JSON.as_bytes().to_vec();
        let macaroon = super::deserialize(&serialized_json).unwrap();
        assert_eq!("http://example.org/", macaroon.location().unwrap());
        assert_eq!(b"keyid", macaroon.identifier());
        assert_eq!(2, macaroon.caveats().len());
        let predicate = match &macaroon.caveats()[0] {
            Caveat::FirstParty(fp) => fp.predicate().to_vec(),
            _ => vec![],
        };
        assert_eq!(b"account = 3735928559".to_vec(), predicate);
        let predicate = match &macaroon.caveats()[1] {
            Caveat::FirstParty(fp) => fp.predicate().to_vec(),
            _ => vec![],
        };
        assert_eq!(b"user = alice".to_vec(), predicate);
        assert_eq!(&MacaroonKey::from(SIGNATURE), macaroon.signature());
    }

    #[test]
    fn test_serialize_deserialize() {
        let mut macaroon =
            Macaroon::create(Some("http://example.org/"), &SIGNATURE.into(), "keyid").unwrap();
        macaroon.add_first_party_caveat("user = alice").unwrap();
        macaroon
            .add_third_party_caveat(
                "https://auth.mybank.com/",
                &MacaroonKey::generate(b"my key"),
                "keyid",
            )
            .unwrap();
        let serialized = macaroon.serialize(Format::V2JSON).unwrap();
        let other = Macaroon::deserialize(&serialized).unwrap();
        assert_eq!(macaroon, other);
    }

    #[test]
    fn test_reject_wrong_version() {
        let bad =
            r#"{"v":1,"i":"keyid","c":[],"s64":"S-lnzR6gxrJrr2pKlO6bBbFYhtoLqF6MQqk8jQ4SXvw"}"#;
        let err = Macaroon::deserialize(bad).unwrap_err();
        assert!(matches!(err, crate::MacaroonError::DeserializationError(_)));
    }

    #[test]
    fn test_reject_caveat_with_both_i_and_i64() {
        let bad = r#"{"v":2,"i":"keyid","c":[{"i":"x","i64":"eA"}],"s64":"S-lnzR6gxrJrr2pKlO6bBbFYhtoLqF6MQqk8jQ4SXvw"}"#;
        let err = Macaroon::deserialize(bad).unwrap_err();
        match err {
            crate::MacaroonError::DeserializationError(s) => {
                assert!(s.contains("i and i64"), "unexpected error: {}", s);
            }
            other => panic!("expected DeserializationError, got {:?}", other),
        }
    }
}