use super::api::DecryptPolicy;
use super::core::{self, JweParts, RecipientParts};
use crate::util::{b64url_decode, b64url_encode};
use crate::{Error, Result};
pub(crate) fn parse(token: &str, policy: &DecryptPolicy) -> Result<JweParts> {
if token.len() > policy.max_input_len {
return Err(Error::InvalidFormat("compact JWE exceeds max_input_len"));
}
let [protected_b64, encrypted_key, iv, ciphertext, tag] = split(token)?;
let protected_json = b64url_decode(protected_b64)?;
let encrypted_key = b64url_decode(encrypted_key)?;
let iv = b64url_decode(iv)?;
let ciphertext = b64url_decode(ciphertext)?;
let tag = b64url_decode(tag)?;
let protected = core::parse_protected(&protected_json)?;
Ok(JweParts {
protected_b64: protected_b64.to_owned(),
protected: Some(protected),
shared: None,
recipients: vec![RecipientParts {
header: None,
encrypted_key,
}],
iv,
ciphertext,
tag,
aad: None,
})
}
pub(crate) fn serialize(parts: &JweParts) -> Result<String> {
let [recipient] = parts.recipients.as_slice() else {
return Err(Error::InvalidFormat(
"compact JWE must have exactly one recipient",
));
};
if parts.protected_b64.is_empty() {
return Err(Error::InvalidFormat("compact JWE needs a protected header"));
}
if parts.shared.is_some() || recipient.header.is_some() {
return Err(Error::InvalidFormat(
"compact JWE cannot carry an unprotected header",
));
}
if parts.aad.is_some() {
return Err(Error::InvalidFormat("compact JWE cannot carry a JWE AAD"));
}
Ok([
parts.protected_b64.clone(),
b64url_encode(&recipient.encrypted_key),
b64url_encode(&parts.iv),
b64url_encode(&parts.ciphertext),
b64url_encode(&parts.tag),
]
.join("."))
}
fn split(token: &str) -> Result<[&str; 5]> {
let mut parts = token.split('.');
let mut out = [""; 5];
for slot in &mut out {
*slot = parts
.next()
.ok_or(Error::InvalidFormat("compact JWE must have five parts"))?;
}
if parts.next().is_some() {
return Err(Error::InvalidFormat("compact JWE must have five parts"));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::header::Header;
use crate::jwa::{ContentEncAlg, KeyMgmtAlg};
use zeroize::Zeroizing;
const A3: &str = "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.\
6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ.\
AxY8DCtDaGlsbGljb3RoZQ.\
KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.\
U0m_YmjN04DJvceFICbCVQ";
fn policy() -> DecryptPolicy {
DecryptPolicy::new(&[KeyMgmtAlg::A128KW], &[ContentEncAlg::A128CbcHs256])
.unwrap_or_else(|e| panic!("{e}"))
}
fn parse_code(token: &str) -> &'static str {
match parse(token, &policy()) {
Ok(_) => "ok",
Err(e) => e.code(),
}
}
#[test]
fn parses_rfc_7516_a3_and_serializes_it_back() {
let parts = parse(A3, &policy()).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(
parts.protected_b64,
"eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0"
);
let protected = parts.protected.as_ref().unwrap_or_else(|| panic!("none"));
assert_eq!(protected.alg.as_deref(), Some("A128KW"));
assert_eq!(protected.enc, Some(ContentEncAlg::A128CbcHs256));
assert_eq!(parts.recipients.len(), 1);
assert_eq!(parts.recipients[0].encrypted_key.len(), 40);
assert!(parts.recipients[0].header.is_none());
assert_eq!(parts.iv.len(), 16);
assert_eq!(parts.ciphertext.len(), 32);
assert_eq!(parts.tag.len(), 16);
assert!(parts.shared.is_none() && parts.aad.is_none());
assert_eq!(serialize(&parts).as_deref(), Ok(A3));
}
#[test]
fn exactly_five_parts() {
let parts: Vec<&str> = A3.split('.').collect();
for n in [1, 2, 3, 4] {
assert_eq!(parse_code(&parts[..n].join(".")), "InvalidFormat", "{n}");
}
assert_eq!(parse_code(&format!("{A3}.")), "InvalidFormat");
assert_eq!(parse_code(&format!("{A3}.AA")), "InvalidFormat");
assert_eq!(parse_code(""), "InvalidFormat");
assert_eq!(parse_code("...."), "InvalidJson"); }
#[test]
fn the_length_limit_comes_before_anything_else() {
let at_limit = policy().max_input_len(A3.len());
assert!(parse(A3, &at_limit).is_ok());
let under = policy().max_input_len(A3.len() - 1);
assert_eq!(
parse(A3, &under).map(|_| ()),
Err(Error::InvalidFormat("compact JWE exceeds max_input_len"))
);
let garbage = "=".repeat(64);
let tiny = policy().max_input_len(10);
assert_eq!(
parse(&garbage, &tiny).map(|_| ()).unwrap_err().code(),
"InvalidFormat"
);
}
#[test]
fn every_part_must_be_strict_base64url() {
let parts: Vec<&str> = A3.split('.').collect();
for i in 0..5 {
for bad in ["AA==", "A+/A", "AA AA", "Zh", "A\nAA"] {
let mut p = parts.clone();
p[i] = bad;
assert_eq!(
parse_code(&p.join(".")),
"InvalidBase64",
"part {i}: {bad:?}"
);
}
}
}
#[test]
fn base64url_is_checked_in_every_part_before_the_header() {
let not_json = b64url_encode(b"not json");
let token = format!("{not_json}..AA==..");
assert_eq!(parse_code(&token), "InvalidBase64");
}
#[test]
fn the_protected_header_must_be_one_json_object() {
let enc = |json: &str| format!("{}..AAAA.AAAA.AAAA", b64url_encode(json.as_bytes()));
assert_eq!(parse_code(&enc("")), "InvalidJson");
assert_eq!(parse_code(&enc("{")), "InvalidJson");
assert_eq!(parse_code(&enc(r#"{"alg":"dir"} x"#)), "InvalidJson");
assert_eq!(
parse_code(&enc(r#"{"alg":"dir","alg":"dir","enc":"A128GCM"}"#)),
"InvalidJson"
);
for json in ["[]", "[1]", "\"x\"", "\"\"", "1", "null"] {
assert_eq!(parse_code(&enc(json)), "InvalidJson", "{json}");
}
assert_eq!(
parse_code(&enc(r#"{"alg":1,"enc":"A128GCM"}"#)),
"InvalidHeader"
);
assert_eq!(parse_code(&enc("{}")), "ok");
}
#[test]
fn the_received_header_encoding_is_kept() {
let json = "{ \"enc\" : \"A128GCM\",\r\n \"alg\":\"dir\" }";
let encoded = b64url_encode(json.as_bytes());
let parts = parse(&format!("{encoded}..AAAA.AAAA.AAAA"), &policy())
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(parts.protected_b64, encoded);
assert_eq!(serialize(&parts), Ok(format!("{encoded}..AAAA.AAAA.AAAA")));
}
#[test]
fn serialize_refuses_what_compact_cannot_carry() {
let base = || parse(A3, &policy()).unwrap_or_else(|e| panic!("{e}"));
let mut two = base();
two.recipients.push(two.recipients[0].clone());
let mut none = base();
none.recipients.clear();
let mut shared = base();
shared.shared = Some(Header::new());
let mut per_recipient = base();
per_recipient.recipients[0].header = Some(Header::new());
let mut aad = base();
aad.aad = Some(Zeroizing::new(Vec::new()));
let mut no_protected = base();
no_protected.protected_b64.clear();
for parts in [two, none, shared, per_recipient, aad, no_protected] {
assert_eq!(serialize(&parts).unwrap_err().code(), "InvalidFormat");
}
}
}