use std::collections::BTreeSet;
use zeroize::Zeroizing;
use super::api::{DecryptPolicy, Decrypted, Encryptor, JsonForm, Serialization};
use super::content::{self, Cek};
use super::key::{Direction, RecipientKey, kid_matches};
use super::keymgmt;
use super::pbes2;
use super::random::RandomSource;
use super::zip;
use crate::header::Header;
use crate::json::{self, Object, Value};
use crate::jwa::{ContentEncAlg, KeyMgmtAlg};
use crate::jwk::Jwk;
use crate::util::{b64url_decode, b64url_encode};
use crate::{Error, Result};
#[derive(Debug, Clone, Default)]
pub(crate) struct RecipientParts {
pub(crate) header: Option<Header>,
pub(crate) encrypted_key: Vec<u8>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct JweParts {
pub(crate) protected_b64: String,
pub(crate) protected: Option<Header>,
pub(crate) shared: Option<Header>,
pub(crate) recipients: Vec<RecipientParts>,
pub(crate) iv: Vec<u8>,
pub(crate) ciphertext: Vec<u8>,
pub(crate) tag: Vec<u8>,
pub(crate) aad: Option<Zeroizing<Vec<u8>>>,
}
const ZIP_DEFLATE: &str = "DEF";
const SET_BY_POLYJOSE: &[&str] = &[
"alg", "enc", "zip", "epk", "apu", "apv", "iv", "tag", "p2s", "p2c",
];
pub(crate) fn parse_protected(json: &[u8]) -> Result<Header> {
let object = json::parse(json)?
.into_object()
.ok_or(Error::InvalidJson("protected header must be a JSON object"))?;
Header::from_object(object)
}
fn jose_header(
protected: Option<&Header>,
shared: Option<&Header>,
recipient: Option<&Header>,
) -> Result<Header> {
for unprotected in [shared, recipient].into_iter().flatten() {
if unprotected.crit.is_some() {
return Err(Error::InvalidHeader("crit must be in the protected header"));
}
if unprotected.zip.is_some() {
return Err(Error::InvalidHeader("zip must be in the protected header"));
}
}
let mut names = BTreeSet::new();
let mut members = Vec::new();
for part in [protected, shared, recipient].into_iter().flatten() {
for (name, value) in part.to_object()? {
if !names.insert(name.as_str().to_owned()) {
return Err(Error::InvalidHeader(
"header parameter occurs in more than one header",
));
}
members.push((name, value));
}
}
let union = Header::from_object(Object::from_unique_members(members))?;
union.validate_crit()?;
Ok(union)
}
fn check_direct_protected(union: &Header, protected: Option<&Header>) -> Result<()> {
let alg = KeyMgmtAlg::from(union.alg.as_deref().unwrap_or_default());
let direct = matches!(
keymgmt::mode(&alg),
Some(keymgmt::Mode::DirectEncryption | keymgmt::Mode::DirectAgreement)
);
let protected_alg_and_enc = protected.is_some_and(|p| p.alg.is_some() && p.enc.is_some());
if direct && !protected_alg_and_enc {
return Err(Error::InvalidHeader(
"dir and ECDH-ES need alg and enc in the protected header",
));
}
Ok(())
}
fn jose_headers(parts: &JweParts, policy: &DecryptPolicy) -> Result<Vec<Header>> {
if parts.recipients.is_empty() {
return Err(Error::InvalidFormat("JWE has no recipients"));
}
if parts.recipients.len() > policy.max_recipients {
return Err(Error::InvalidFormat("JWE exceeds max_recipients"));
}
let mut unions = Vec::with_capacity(parts.recipients.len());
for recipient in &parts.recipients {
let union = jose_header(
parts.protected.as_ref(),
parts.shared.as_ref(),
recipient.header.as_ref(),
)?;
if union.alg.is_none() {
return Err(Error::InvalidHeader("missing alg"));
}
if union.enc.is_none() {
return Err(Error::InvalidHeader("missing enc"));
}
check_direct_protected(&union, parts.protected.as_ref())?;
unions.push(union);
}
if unions.iter().any(|u| u.enc != unions[0].enc) {
return Err(Error::InvalidHeader("recipients disagree on enc"));
}
let understood: Vec<&str> = policy.understood_crit.iter().map(String::as_str).collect();
for union in &unions {
union.validate_crit_understood(&understood)?;
}
Ok(unions)
}
fn check_zip(zip: Option<&str>) -> Result<bool> {
match zip {
None => Ok(false),
Some(ZIP_DEFLATE) if cfg!(feature = "deflate") => Ok(true),
Some(_) => Err(Error::UnsupportedAlgorithm),
}
}
fn rank(error: &Error) -> u8 {
match error {
Error::AlgorithmNotAllowed => 3,
Error::UnsupportedAlgorithm => 2,
_ => 1,
}
}
fn candidate(
header: &Header,
enc: &ContentEncAlg,
key: &Jwk,
policy: &DecryptPolicy,
) -> Result<(KeyMgmtAlg, RecipientKey)> {
let alg = KeyMgmtAlg::from(header.alg.as_deref().unwrap_or_default());
if !keymgmt::supported(&alg, Direction::Decrypt) {
return Err(Error::UnsupportedAlgorithm);
}
if !policy.allowed_algs.contains(&alg) || !policy.allowed_enc.contains(enc) {
return Err(Error::AlgorithmNotAllowed);
}
let recipient_key = RecipientKey::from_jwk(key, &alg, enc, Direction::Decrypt)?;
if !kid_matches(key, header.kid.as_deref()) {
return Err(Error::KeyAlgorithmMismatch);
}
Ok((alg, recipient_key))
}
#[allow(
clippy::too_many_arguments,
reason = "one candidate recipient and the message parts it is tried against"
)]
fn decrypt_recipient(
alg: &KeyMgmtAlg,
key: &RecipientKey,
enc: &ContentEncAlg,
header: &Header,
encrypted_key: &[u8],
parts: &JweParts,
aad: &[u8],
inflate: bool,
policy: &DecryptPolicy,
pbes2_budget: &mut pbes2::Budget,
) -> Result<Zeroizing<Vec<u8>>> {
let failed = |_| Error::DecryptionFailed;
let cek = keymgmt::decrypt_key(alg, key, enc, header, encrypted_key, policy, pbes2_budget)
.map_err(failed)?;
let plaintext = content::decrypt(enc, &cek, &parts.iv, aad, &parts.ciphertext, &parts.tag)
.map_err(failed)?;
if inflate {
zip::decompress(&plaintext, policy.max_decompressed_len).map_err(failed)
} else {
Ok(plaintext)
}
}
pub(crate) fn decrypt(parts: JweParts, key: &Jwk, policy: &DecryptPolicy) -> Result<Decrypted> {
let headers = jose_headers(&parts, policy)?;
let enc = headers[0].content_enc_alg()?;
let inflate = check_zip(parts.protected.as_ref().and_then(|p| p.zip.as_deref()))?;
let mut best: Option<Error> = None;
let mut candidates = Vec::new();
for (index, header) in headers.iter().enumerate() {
match candidate(header, &enc, key, policy) {
Ok((alg, recipient_key)) => candidates.push((index, alg, recipient_key)),
Err(e) => {
if best.as_ref().is_none_or(|b| rank(&e) > rank(b)) {
best = Some(e);
}
}
}
}
if candidates.is_empty() {
return Err(best.unwrap_or(Error::DecryptionFailed));
}
let jwe_aad = parts.aad.as_deref().map(Vec::as_slice);
let aad = content::aad(&parts.protected_b64, jwe_aad);
let mut pbes2_budget = pbes2::Budget::new(policy);
for (index, alg, recipient_key) in &candidates {
let header = &headers[*index];
let recipient = &parts.recipients[*index];
let result = decrypt_recipient(
alg,
recipient_key,
&enc,
header,
&recipient.encrypted_key,
&parts,
&aad,
inflate,
policy,
&mut pbes2_budget,
);
if let Ok(plaintext) = result {
let index = *index;
let mut parts = parts;
let mut headers = headers;
return Ok(Decrypted {
plaintext,
protected_header: parts.protected.take(),
shared_header: parts.shared.take(),
recipient_header: parts.recipients.swap_remove(index).header,
header: headers.swap_remove(index),
aad: parts.aad.take(),
recipient: index,
});
}
}
Err(Error::DecryptionFailed)
}
fn check_caller_header(header: Option<&Header>) -> Result<()> {
match header {
Some(h) if SET_BY_POLYJOSE.iter().any(|name| h.contains(name)) => Err(
Error::InvalidHeader("alg, enc, zip and key management parameters are set by polyjose"),
),
_ => Ok(()),
}
}
fn with_key_params(header: &Header, alg: &KeyMgmtAlg, params: Object) -> Result<Header> {
let mut object = header.to_object()?;
for (name, value) in [(Zeroizing::new("alg".to_owned()), Value::from(alg.as_str()))]
.into_iter()
.chain(params)
{
if object.contains(&name) {
return Err(Error::InvalidHeader(
"header parameter occurs in more than one header",
));
}
object.insert(name.as_str(), value);
}
Header::from_object(object)
}
fn check_reproducible(given: &str, built: &Header) -> Result<()> {
let mismatch = Error::InvalidFormat("protected header does not match the built header");
let bytes = b64url_decode(given).map_err(|_| mismatch.clone())?;
let value = json::parse(&bytes).map_err(|_| mismatch.clone())?;
if value == Value::from(built.to_object()?) {
Ok(())
} else {
Err(mismatch)
}
}
pub(crate) fn encrypt(
encryptor: &Encryptor,
plaintext: &[u8],
form: Serialization,
rng: &mut dyn RandomSource,
protected_b64: Option<&str>,
) -> Result<JweParts> {
let recipients = &encryptor.recipients;
let enc = &encryptor.enc;
match form {
_ if recipients.is_empty() => {
return Err(Error::InvalidFormat("JWE needs at least one recipient"));
}
Serialization::Compact => {
if recipients.len() != 1 {
return Err(Error::InvalidFormat(
"compact JWE must have exactly one recipient",
));
}
if encryptor.aad.is_some() {
return Err(Error::InvalidFormat("compact JWE cannot carry a JWE AAD"));
}
if encryptor.shared.is_some() || recipients[0].header.is_some() {
return Err(Error::InvalidFormat(
"compact JWE cannot carry an unprotected header",
));
}
}
Serialization::Json(JsonForm::Flattened) if recipients.len() != 1 => {
return Err(Error::InvalidFormat(
"flattened JWE must have exactly one recipient",
));
}
Serialization::Json(_) => {}
}
enc.ensure_known()?;
let mut modes = Vec::with_capacity(recipients.len());
for recipient in recipients {
if !keymgmt::supported(&recipient.alg, Direction::Encrypt) {
return Err(Error::UnsupportedAlgorithm);
}
modes.push(keymgmt::mode(&recipient.alg).ok_or(Error::UnsupportedAlgorithm)?);
}
if encryptor.compress && !cfg!(feature = "deflate") {
return Err(Error::UnsupportedAlgorithm);
}
let random_cek = modes.iter().all(|m| m.has_random_cek());
if !random_cek && recipients.len() > 1 {
return Err(Error::InvalidFormat(
"dir and ECDH-ES allow only one recipient",
));
}
check_caller_header(encryptor.protected.as_ref())?;
check_caller_header(encryptor.shared.as_ref())?;
let mut protected = encryptor.protected.clone().unwrap_or_default();
protected.enc = Some(enc.clone());
if encryptor.compress {
protected.zip = Some(ZIP_DEFLATE.to_owned());
}
for recipient in recipients {
check_caller_header(recipient.header.as_ref())?;
jose_header(
Some(&protected),
encryptor.shared.as_ref(),
recipient.header.as_ref(),
)?;
}
let mut keys = Vec::with_capacity(recipients.len());
for recipient in recipients {
keys.push(RecipientKey::from_jwk(
&recipient.key,
&recipient.alg,
enc,
Direction::Encrypt,
)?);
}
for recipient in recipients {
pbes2::check_encrypt_options(&recipient.alg, &recipient.options)?;
}
let mut cek: Option<Cek> = if random_cek {
Some(content::generate_cek(enc, rng)?)
} else {
None
};
let general = form == Serialization::Json(JsonForm::General);
let mut out_recipients = Vec::with_capacity(recipients.len());
for ((recipient, key), mode) in recipients.iter().zip(&keys).zip(&modes) {
let given = if random_cek { cek.as_ref() } else { None };
let output =
keymgmt::encrypt_key(&recipient.alg, key, enc, given, &recipient.options, rng)?;
match (output.cek, mode.has_random_cek()) {
(Some(determined), false) => cek = Some(determined),
(None, true) => {}
_ => return Err(Error::UnsupportedAlgorithm),
}
let header = if general && mode.has_random_cek() {
let base = recipient.header.clone().unwrap_or_default();
Some(with_key_params(
&base,
&recipient.alg,
output.header_params,
)?)
} else {
protected = with_key_params(&protected, &recipient.alg, output.header_params)?;
recipient.header.clone()
};
out_recipients.push(RecipientParts {
header,
encrypted_key: output.encrypted_key,
});
}
let cek = cek.ok_or(Error::UnsupportedAlgorithm)?;
for recipient in &out_recipients {
jose_header(
Some(&protected),
encryptor.shared.as_ref(),
recipient.header.as_ref(),
)?;
}
let protected_b64 = match protected_b64 {
Some(given) => {
check_reproducible(given, &protected)?;
given.to_owned()
}
None => b64url_encode(protected.to_json()?.as_bytes()),
};
let iv = content::generate_iv(enc, rng)?;
let compressed;
let plaintext = if encryptor.compress {
compressed = zip::compress(plaintext)?;
compressed.as_slice()
} else {
plaintext
};
let aad = content::aad(&protected_b64, encryptor.aad.as_deref().map(Vec::as_slice));
let (ciphertext, tag) = content::encrypt(enc, &cek, &iv, &aad, plaintext)?;
Ok(JweParts {
protected_b64,
protected: Some(protected),
shared: encryptor.shared.clone(),
recipients: out_recipients,
iv,
ciphertext,
tag,
aad: encryptor.aad.clone(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jwe::api::Recipient;
use crate::jwe::random::OsRandom;
use crate::rng::insecure_test_vectors::FixedRng;
use KeyMgmtAlg as A;
fn oct(bytes: &[u8], extra: &str) -> Jwk {
let json = format!(r#"{{"kty":"oct","k":"{}"{extra}}}"#, b64url_encode(bytes));
Jwk::from_json(json.as_bytes()).unwrap_or_else(|e| panic!("{e}"))
}
fn header(json: &str) -> Header {
Header::from_json(json.as_bytes()).unwrap_or_else(|e| panic!("{json}: {e}"))
}
fn policy(algs: &[KeyMgmtAlg]) -> DecryptPolicy {
DecryptPolicy::new(algs, &[ContentEncAlg::A128GCM]).unwrap_or_else(|e| panic!("{e}"))
}
fn code<T>(result: Result<T>) -> &'static str {
match result {
Ok(_) => "ok",
Err(e) => e.code(),
}
}
fn general(keys: &[&Jwk]) -> JweParts {
let mut e = Encryptor::new(ContentEncAlg::A128GCM);
for key in keys {
e = e.recipient(Recipient::new(A::A128KW, key));
}
let form = Serialization::Json(JsonForm::General);
encrypt(&e, b"hello", form, &mut OsRandom, None).unwrap_or_else(|e| panic!("{e}"))
}
fn rheader(parts: &mut JweParts, index: usize) -> &mut Header {
parts.recipients[index]
.header
.as_mut()
.unwrap_or_else(|| panic!("no header"))
}
fn pheader(parts: &mut JweParts) -> &mut Header {
parts
.protected
.as_mut()
.unwrap_or_else(|| panic!("no protected header"))
}
fn reprotect(parts: &mut JweParts, json: &str) {
parts.protected = Some(header(json));
parts.protected_b64 = b64url_encode(json.as_bytes());
}
fn set_alg(parts: &mut JweParts, index: usize, alg: &str) {
rheader(parts, index).alg = Some(alg.to_owned());
}
#[test]
fn protected_header_parsing_follows_the_error_contract() {
assert_eq!(
code(parse_protected(br#"{"alg":"dir","enc":"A128GCM"}"#)),
"ok"
);
assert_eq!(code(parse_protected(b"")), "InvalidJson");
assert_eq!(code(parse_protected(br#"{"a":1,"a":2}"#)), "InvalidJson");
assert_eq!(
code(parse_protected(br#"{"x":{"a":1,"a":2}}"#)),
"InvalidJson"
);
assert_eq!(code(parse_protected(b"[]")), "InvalidJson");
assert_eq!(code(parse_protected(b"\"\"")), "InvalidJson");
assert_eq!(code(parse_protected(br#"{"enc":7}"#)), "InvalidHeader");
}
#[test]
fn the_jose_header_is_the_union_of_three_disjoint_parts() {
let p = header(r#"{"enc":"A128GCM"}"#);
let s = header(r#"{"jku":"https://example.com/k"}"#);
let r = header(r#"{"alg":"A128KW","kid":"k1"}"#);
let union = jose_header(Some(&p), Some(&s), Some(&r)).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(union.alg.as_deref(), Some("A128KW"));
assert_eq!(union.enc, Some(ContentEncAlg::A128GCM));
assert_eq!(union.kid.as_deref(), Some("k1"));
assert!(union.jku.is_some());
assert_eq!(code(jose_header(None, None, None)), "ok");
let dup = header(r#"{"kid":"k1"}"#);
for (a, b, c) in [
(Some(&r), None, Some(&dup)),
(Some(&dup), Some(&dup), None),
(None, Some(&r), Some(&r)),
] {
assert_eq!(code(jose_header(a, b, c)), "InvalidHeader");
}
let private = header(r#"{"x-a":1}"#);
assert_eq!(
code(jose_header(Some(&private), Some(&private), None)),
"InvalidHeader"
);
}
#[test]
fn crit_and_zip_must_be_protected() {
let crit = header(r#"{"crit":["x-a"],"x-a":1}"#);
let zip = header(r#"{"zip":"DEF"}"#);
assert_eq!(code(jose_header(Some(&crit), None, None)), "ok");
assert_eq!(code(jose_header(Some(&zip), None, None)), "ok");
for h in [&crit, &zip] {
assert_eq!(code(jose_header(None, Some(h), None)), "InvalidHeader");
assert_eq!(code(jose_header(None, None, Some(h))), "InvalidHeader");
}
let absent = header(r#"{"crit":["x-b"]}"#);
assert_eq!(
code(jose_header(Some(&absent), None, None)),
"InvalidHeader"
);
let elsewhere = header(r#"{"x-b":true}"#);
assert_eq!(
code(jose_header(Some(&absent), Some(&elsewhere), None)),
"ok"
);
}
#[test]
fn steps_4_and_5_run_over_every_recipient() {
let key = oct(&[1; 16], "");
let p = policy(&[A::A128KW]);
let base = general(&[&key, &key]);
let mut no_alg = base.clone();
rheader(&mut no_alg, 1).alg = None;
assert_eq!(code(decrypt(no_alg, &key, &p)), "InvalidHeader");
let mut no_enc = base.clone();
pheader(&mut no_enc).enc = None;
assert_eq!(code(decrypt(no_enc, &key, &p)), "InvalidHeader");
let mut two_encs = base.clone();
let moved = pheader(&mut two_encs).enc.take();
rheader(&mut two_encs, 0).enc = moved;
rheader(&mut two_encs, 1).enc = Some(ContentEncAlg::A256GCM);
assert_eq!(code(decrypt(two_encs, &key, &p)), "InvalidHeader");
let mut both = base.clone();
pheader(&mut both).alg = Some("A128KW".to_owned());
assert_eq!(code(decrypt(both, &key, &p)), "InvalidHeader");
let mut crit = base.clone();
reprotect(&mut crit, r#"{"enc":"A128GCM","crit":["x-a"],"x-a":1}"#);
set_alg(&mut crit, 1, "RSA1_5");
assert_eq!(code(decrypt(crit.clone(), &key, &p)), "UnknownCritical");
let understood = p.clone().understood_crit(&["x-a"]);
assert_eq!(code(decrypt(crit, &key, &understood)), "DecryptionFailed");
let mut empty = base.clone();
empty.recipients.clear();
assert_eq!(code(decrypt(empty, &key, &p)), "InvalidFormat");
assert_eq!(
code(decrypt(base.clone(), &key, &p.clone().max_recipients(1))),
"InvalidFormat"
);
assert_eq!(code(decrypt(base, &key, &p)), "ok");
}
#[test]
fn direct_modes_need_a_protected_alg_and_enc_in_step_4() {
let key = oct(&[1; 16], "");
let mut parts = general(&[&key]);
set_alg(&mut parts, 0, "dir");
parts.recipients[0].encrypted_key.clear();
let not_allowed = policy(&[A::A128KW]);
let unbound = oct(&[1; 16], r#","alg":"A128KW""#);
assert_eq!(
code(decrypt(parts.clone(), &key, ¬_allowed)),
"InvalidHeader"
);
assert_eq!(
code(decrypt(parts.clone(), &unbound, &policy(&[A::Dir]))),
"InvalidHeader"
);
let mut crit = parts.clone();
reprotect(&mut crit, r#"{"enc":"A128GCM","crit":["x-a"],"x-a":1}"#);
assert_eq!(
code(decrypt(crit, &key, &policy(&[A::Dir]))),
"InvalidHeader"
);
let mut unsupported_enc = parts.clone();
reprotect(&mut unsupported_enc, r#"{"enc":"A128CTR"}"#);
assert_eq!(
code(decrypt(unsupported_enc, &key, &policy(&[A::Dir]))),
"InvalidHeader"
);
set_alg(&mut parts, 0, "ECDH-ES");
assert_eq!(
code(decrypt(parts.clone(), &key, &policy(&[A::EcdhEs]))),
"InvalidHeader"
);
let mut enc_unprotected = general(&[&key]);
rheader(&mut enc_unprotected, 0).alg = None;
rheader(&mut enc_unprotected, 0).enc = Some(ContentEncAlg::A128GCM);
reprotect(&mut enc_unprotected, r#"{"alg":"dir"}"#);
assert_eq!(
code(decrypt(enc_unprotected, &key, &policy(&[A::Dir]))),
"InvalidHeader"
);
assert_eq!(code(decrypt(general(&[&key]), &key, ¬_allowed)), "ok");
}
#[test]
fn unsupported_enc_and_zip_fail_immediately() {
let key = oct(&[1; 16], "");
let p = policy(&[A::A128KW]);
let mut enc = general(&[&key]);
reprotect(&mut enc, r#"{"enc":"A128CTR"}"#);
assert_eq!(code(decrypt(enc, &key, &p)), "UnsupportedAlgorithm");
for zip in ["XYZ", "def", "DEF"] {
let mut parts = general(&[&key]);
reprotect(&mut parts, &format!(r#"{{"enc":"A128GCM","zip":"{zip}"}}"#));
let expected = if zip == "DEF" && cfg!(feature = "deflate") {
"DecryptionFailed" } else {
"UnsupportedAlgorithm"
};
assert_eq!(code(decrypt(parts, &key, &p)), expected, "{zip}");
}
}
#[test]
fn the_first_candidate_that_decrypts_wins() {
let k1 = oct(&[1; 16], "");
let k2 = oct(&[2; 16], "");
let k3 = oct(&[3; 16], "");
let parts = general(&[&k1, &k2, &k3]);
let p = policy(&[A::A128KW]);
for (key, index) in [(&k1, 0), (&k2, 1), (&k3, 2)] {
let d = decrypt(parts.clone(), key, &p).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(d.recipient(), index);
assert_eq!(d.plaintext(), b"hello");
assert_eq!(d.header().alg.as_deref(), Some("A128KW"));
assert_eq!(d.header().enc, Some(ContentEncAlg::A128GCM));
assert!(d.recipient_header().is_some_and(|h| h.alg.is_some()));
assert!(d.protected_header().is_some_and(|h| h.alg.is_none()));
}
let other = oct(&[9; 16], "");
assert_eq!(code(decrypt(parts, &other, &p)), "DecryptionFailed");
}
#[test]
fn non_candidates_are_skipped() {
let k = oct(&[1; 16], r#","kid":"mine""#);
let p = policy(&[A::A128KW, A::A256KW]);
let mut parts = general(&[&k, &k, &k, &k, &k]);
set_alg(&mut parts, 0, "RSA1_5"); set_alg(&mut parts, 1, "A192KW"); set_alg(&mut parts, 2, "A256KW"); rheader(&mut parts, 3).kid = Some("theirs".to_owned());
let d = decrypt(parts, &k, &p).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(d.recipient(), 4);
}
#[test]
fn with_no_candidate_the_best_ranked_error_is_reported() {
let k = oct(&[1; 16], r#","kid":"mine""#);
let p = policy(&[A::A128KW, A::A256KW]);
let unsupported = |parts: &mut JweParts, i: usize| set_alg(parts, i, "RSA-OAEP");
let not_allowed = |parts: &mut JweParts, i: usize| set_alg(parts, i, "A192KW");
let mismatch = |parts: &mut JweParts, i: usize| set_alg(parts, i, "A256KW");
let other_kid =
|parts: &mut JweParts, i: usize| rheader(parts, i).kid = Some("x".to_owned());
type Make = dyn Fn(&mut JweParts, usize);
let cases: [(&[&Make], &str); 7] = [
(&[&unsupported], "UnsupportedAlgorithm"),
(&[¬_allowed], "AlgorithmNotAllowed"),
(&[&mismatch], "KeyAlgorithmMismatch"),
(&[&other_kid], "KeyAlgorithmMismatch"),
(
&[&mismatch, &unsupported, ¬_allowed],
"AlgorithmNotAllowed",
),
(
&[¬_allowed, &unsupported, &mismatch],
"AlgorithmNotAllowed",
),
(
&[&mismatch, &other_kid, &unsupported],
"UnsupportedAlgorithm",
),
];
for (makers, expected) in cases {
let keys: Vec<&Jwk> = makers.iter().map(|_| &k).collect();
let mut parts = general(&keys);
for (i, make) in makers.iter().enumerate() {
make(&mut parts, i);
}
assert_eq!(code(decrypt(parts, &k, &p)), expected, "{expected}");
}
}
#[test]
fn rsa_oaep_and_rsa1_5_never_decrypt_even_when_allowed() {
let k = oct(&[1; 16], "");
let p = policy(&[A::RsaOaep256, A::from("RSA-OAEP"), A::from("RSA1_5")]);
for (alg, expected) in [
("RSA-OAEP-256", "KeyAlgorithmMismatch"),
("RSA-OAEP", "UnsupportedAlgorithm"),
("RSA1_5", "UnsupportedAlgorithm"),
("nope", "UnsupportedAlgorithm"),
] {
let mut parts = general(&[&k]);
set_alg(&mut parts, 0, alg);
assert_eq!(code(decrypt(parts, &k, &p)), expected, "{alg}");
}
}
#[test]
fn every_post_key_management_failure_is_the_same_error() {
let k = oct(&[1; 16], "");
let p = policy(&[A::A128KW]);
let base = general(&[&k]);
let mut cases = Vec::new();
let mut c = base.clone();
c.tag[0] ^= 1;
cases.push(c);
let mut c = base.clone();
c.ciphertext[0] ^= 1;
cases.push(c);
let mut c = base.clone();
c.iv[0] ^= 1;
cases.push(c);
let mut c = base.clone();
c.iv.pop();
cases.push(c);
let mut c = base.clone();
c.tag.truncate(15);
cases.push(c);
let mut c = base.clone();
c.recipients[0].encrypted_key[3] ^= 1;
cases.push(c);
let mut c = base.clone();
c.recipients[0].encrypted_key.truncate(16);
cases.push(c);
let mut c = base.clone();
c.protected_b64.push('A');
cases.push(c);
let mut c = base.clone();
c.aad = Some(Zeroizing::new(b"aad".to_vec()));
cases.push(c);
for parts in cases {
let e = decrypt(parts, &k, &p).map(|_| ()).unwrap_err();
assert_eq!(e, Error::DecryptionFailed);
assert_eq!(format!("{e:?}"), "DecryptionFailed");
assert_eq!(e.to_string(), "decryption failed");
assert!(std::error::Error::source(&e).is_none());
}
}
fn password(pw: &str) -> Jwk {
oct(pw.as_bytes(), "")
}
fn pbes2_general(recipients: &[(&str, u32)]) -> JweParts {
let mut e = Encryptor::new(ContentEncAlg::A128GCM);
for (pw, p2c) in recipients {
e = e.recipient(Recipient::new(A::Pbes2Hs256A128KW, &password(pw)).p2c(*p2c));
}
let form = Serialization::Json(JsonForm::General);
encrypt(&e, b"hello", form, &mut OsRandom, None).unwrap_or_else(|e| panic!("{e}"))
}
fn sixteen(correct: Option<usize>) -> Vec<(String, u32)> {
(0..16)
.map(|i| {
let pw = if correct == Some(i) {
"correct".to_owned()
} else {
format!("decoy {i}")
};
(pw, 1000)
})
.collect()
}
fn pbes2_sixteen(correct: Option<usize>) -> JweParts {
let recipients = sixteen(correct);
let refs: Vec<(&str, u32)> = recipients.iter().map(|(p, c)| (p.as_str(), *c)).collect();
pbes2_general(&refs)
}
fn decrypt_counted(parts: JweParts, key: &Jwk, p: &DecryptPolicy) -> (&'static str, u64) {
let (result, n) =
crate::test_counters::pbkdf2_derivations::count(|| decrypt(parts, key, p));
(code(result), n)
}
#[test]
fn the_pbes2_budget_is_per_token_summed_over_candidates() {
let p = policy(&[A::Pbes2Hs256A128KW]);
assert_eq!(
p.max_pbes2_count,
DecryptPolicy::DEFAULT_MAX_PBES2_COUNT,
"the default bound"
);
let none = pbes2_sixteen(None);
let key = password("correct");
assert_eq!(
decrypt_counted(none.clone(), &key, &p),
("DecryptionFailed", 10)
);
let raised = p.clone().max_pbes2_count(16_000);
assert_eq!(
decrypt_counted(none, &key, &raised),
("DecryptionFailed", 16)
);
}
#[test]
fn a_correct_recipient_within_the_budget_still_decrypts() {
let p = policy(&[A::Pbes2Hs256A128KW]);
let key = password("correct");
assert_eq!(decrypt_counted(pbes2_sixteen(Some(0)), &key, &p), ("ok", 1));
let tenth = decrypt(pbes2_sixteen(Some(9)), &key, &p).unwrap_or_else(|e| panic!("{e}"));
assert_eq!((tenth.recipient(), tenth.plaintext()), (9, &b"hello"[..]));
assert_eq!(
decrypt_counted(pbes2_sixteen(Some(9)), &key, &p),
("ok", 10)
);
let eleventh = pbes2_sixteen(Some(10));
assert_eq!(
decrypt_counted(eleventh.clone(), &key, &p),
("DecryptionFailed", 10)
);
let raised = p.clone().max_pbes2_count(11_000);
assert_eq!(decrypt_counted(eleventh, &key, &raised), ("ok", 11));
}
#[test]
fn a_candidate_over_the_remaining_budget_is_skipped_not_fatal() {
let p = policy(&[A::Pbes2Hs256A128KW]);
let key = password("correct");
let parts = pbes2_general(&[("decoy", 10_001), ("correct", 1000)]);
let d = decrypt(parts.clone(), &key, &p).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(d.recipient(), 1);
assert_eq!(decrypt_counted(parts, &key, &p), ("ok", 1));
let parts = pbes2_general(&[("decoy", 9_500), ("correct", 1000)]);
assert_eq!(
decrypt_counted(parts.clone(), &key, &p),
("DecryptionFailed", 1)
);
let parts = pbes2_general(&[("decoy", 9_000), ("correct", 1000)]);
assert_eq!(decrypt_counted(parts, &key, &p), ("ok", 2));
}
#[test]
fn the_budget_is_per_decrypt_call() {
let p = policy(&[A::Pbes2Hs256A128KW]);
let key = password("correct");
let parts = pbes2_sixteen(Some(9));
for _ in 0..3 {
assert_eq!(decrypt_counted(parts.clone(), &key, &p), ("ok", 10));
}
}
fn compact_encryptor(alg: KeyMgmtAlg, key: &Jwk) -> Encryptor {
Encryptor::new(ContentEncAlg::A128GCM).recipient(Recipient::new(alg, key))
}
fn encrypt_code(e: &Encryptor, form: Serialization) -> &'static str {
code(encrypt(e, b"x", form, &mut OsRandom, None))
}
#[test]
fn encrypt_refuses_what_the_serialization_cannot_carry() {
let k = oct(&[1; 16], "");
let compact = Serialization::Compact;
let flat = Serialization::Json(JsonForm::Flattened);
let general_form = Serialization::Json(JsonForm::General);
let one = compact_encryptor(A::A128KW, &k);
assert_eq!(encrypt_code(&one, compact), "ok");
let none = Encryptor::new(ContentEncAlg::A128GCM);
for form in [compact, flat, general_form] {
assert_eq!(encrypt_code(&none, form), "InvalidFormat");
}
let two = one.clone().recipient(Recipient::new(A::A128KW, &k));
assert_eq!(encrypt_code(&two, compact), "InvalidFormat");
assert_eq!(encrypt_code(&two, flat), "InvalidFormat");
assert_eq!(encrypt_code(&two, general_form), "ok");
assert_eq!(
encrypt_code(&one.clone().aad(*b"a"), compact),
"InvalidFormat"
);
assert_eq!(encrypt_code(&one.clone().aad(*b"a"), general_form), "ok");
let shared = one
.clone()
.shared_unprotected_header(header(r#"{"x-a":1}"#));
assert_eq!(encrypt_code(&shared, compact), "InvalidFormat");
let per = Encryptor::new(ContentEncAlg::A128GCM)
.recipient(Recipient::new(A::A128KW, &k).header(header(r#"{"kid":"a"}"#)));
assert_eq!(encrypt_code(&per, compact), "InvalidFormat");
assert_eq!(encrypt_code(&per, flat), "ok");
let d = oct(&[1; 16], "");
let dir_and_kw = compact_encryptor(A::Dir, &d).recipient(Recipient::new(A::A128KW, &k));
assert_eq!(encrypt_code(&dir_and_kw, general_form), "InvalidFormat");
let dir_twice = compact_encryptor(A::Dir, &d).recipient(Recipient::new(A::Dir, &d));
assert_eq!(encrypt_code(&dir_twice, general_form), "InvalidFormat");
}
#[test]
fn encrypt_refuses_parameters_polyjose_sets() {
let k = oct(&[1; 16], "");
for name in SET_BY_POLYJOSE {
let value = if *name == "p2c" {
"1000"
} else if *name == "epk" {
"{}"
} else {
"\"A128KW\""
};
let h = header(&format!(r#"{{"{name}":{value}}}"#));
let protected = compact_encryptor(A::A128KW, &k).protected_header(h.clone());
assert_eq!(
encrypt_code(&protected, Serialization::Compact),
"InvalidHeader",
"{name}"
);
let shared = compact_encryptor(A::A128KW, &k).shared_unprotected_header(h.clone());
let general_form = Serialization::Json(JsonForm::General);
assert_eq!(
encrypt_code(&shared, general_form),
"InvalidHeader",
"{name}"
);
let per = Encryptor::new(ContentEncAlg::A128GCM)
.recipient(Recipient::new(A::A128KW, &k).header(h));
assert_eq!(encrypt_code(&per, general_form), "InvalidHeader", "{name}");
}
let general_form = Serialization::Json(JsonForm::General);
let clash = compact_encryptor(A::A128KW, &k)
.protected_header(header(r#"{"kid":"a"}"#))
.shared_unprotected_header(header(r#"{"kid":"a"}"#));
assert_eq!(encrypt_code(&clash, general_form), "InvalidHeader");
let crit = compact_encryptor(A::A128KW, &k)
.shared_unprotected_header(header(r#"{"crit":["x-a"],"x-a":1}"#));
assert_eq!(encrypt_code(&crit, general_form), "InvalidHeader");
let crit_absent =
compact_encryptor(A::A128KW, &k).protected_header(header(r#"{"crit":["x-a"]}"#));
assert_eq!(
encrypt_code(&crit_absent, Serialization::Compact),
"InvalidHeader"
);
}
#[test]
fn encrypt_checks_support_then_keys() {
let k = oct(&[1; 16], "");
let compact = Serialization::Compact;
for alg in ["RSA-OAEP", "RSA1_5", "nope"] {
assert_eq!(
encrypt_code(&compact_encryptor(A::from(alg), &k), compact),
"UnsupportedAlgorithm",
"{alg}"
);
}
let unknown_enc =
Encryptor::new(ContentEncAlg::from("A128CTR")).recipient(Recipient::new(A::A128KW, &k));
assert_eq!(encrypt_code(&unknown_enc, compact), "UnsupportedAlgorithm");
assert_eq!(
encrypt_code(&compact_encryptor(A::A256KW, &k), compact),
"InvalidKey"
);
let dir_key = oct(&[1; 32], "");
assert_eq!(
encrypt_code(&compact_encryptor(A::Dir, &dir_key), compact),
"InvalidKey"
);
let ec = Jwk::from_json(
br#"{"kty":"EC","crv":"P-256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"}"#,
)
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(
encrypt_code(&compact_encryptor(A::A128KW, &ec), compact),
"KeyAlgorithmMismatch"
);
let bound = oct(&[1; 16], r#","alg":"A192KW""#);
assert_eq!(
encrypt_code(&compact_encryptor(A::A128KW, &bound), compact),
"KeyAlgorithmMismatch"
);
}
#[test]
fn rng_draw_order_is_cek_then_recipients_then_iv() {
let k = oct(&[1; 16], "");
let bytes: Vec<u8> = (0..28).collect();
let mut rng = FixedRng::new(bytes);
let e = compact_encryptor(A::A128KW, &k);
let parts = encrypt(&e, b"x", Serialization::Compact, &mut rng, None)
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(rng.remaining(), 0);
assert_eq!(parts.iv, (16..28).collect::<Vec<u8>>());
let unwrapped = super::super::aes_kw::unwrap(&[1; 16], &parts.recipients[0].encrypted_key)
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(unwrapped.as_slice(), (0..16).collect::<Vec<u8>>());
let mut short = FixedRng::new((0..27).collect::<Vec<u8>>());
assert_eq!(
code(encrypt(&e, b"x", Serialization::Compact, &mut short, None)),
"RandomnessUnavailable"
);
let mut iv_only = FixedRng::new([7u8; 12]);
let d = compact_encryptor(A::Dir, &k);
let parts = encrypt(&d, b"x", Serialization::Compact, &mut iv_only, None)
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(iv_only.remaining(), 0);
assert_eq!(parts.iv, [7u8; 12]);
assert!(parts.recipients[0].encrypted_key.is_empty());
}
#[test]
fn pbes2_parameters_are_refused_before_any_draw() {
let k = password("pw");
let ceiling = DecryptPolicy::MAX_PBES2_COUNT_CEILING;
let cases: [fn(Recipient) -> Recipient; 4] = [
|r| r.p2c(999),
|r| r.p2c(DecryptPolicy::MAX_PBES2_COUNT_CEILING + 1),
|r| r.p2s_len(7),
|r| r.p2s_len(257),
];
assert_eq!(ceiling, 1_000_000);
for (i, make) in cases.iter().enumerate() {
let e = Encryptor::new(ContentEncAlg::A128GCM)
.recipient(Recipient::new(A::Pbes2Hs256A128KW, &k).p2c(1000))
.recipient(make(Recipient::new(A::Pbes2Hs256A128KW, &k)));
let mut rng = FixedRng::new([0u8; 128]);
let form = Serialization::Json(JsonForm::General);
let (out, n) = crate::test_counters::pbkdf2_derivations::count(|| {
encrypt(&e, b"x", form, &mut rng, None)
});
assert_eq!((code(out), n), ("InvalidKey", 0), "case {i}");
assert_eq!(rng.remaining(), 128, "case {i}: drew before the checks");
}
}
#[cfg(not(feature = "deflate"))]
#[test]
fn compress_without_deflate_is_refused_before_any_draw() {
let k = oct(&[1; 16], "");
for alg in [A::Dir, A::A128KW] {
let e = compact_encryptor(alg.clone(), &k).compress();
for form in [
Serialization::Compact,
Serialization::Json(JsonForm::Flattened),
Serialization::Json(JsonForm::General),
] {
let mut rng = FixedRng::new([0u8; 64]);
assert_eq!(
code(encrypt(&e, b"x", form, &mut rng, None)),
"UnsupportedAlgorithm",
"{alg:?} {form:?}"
);
assert_eq!(rng.remaining(), 64, "{alg:?} {form:?}: drew first");
}
}
}
#[test]
fn header_placement_follows_the_form() {
let k = oct(&[1; 16], "");
let e = compact_encryptor(A::A128KW, &k).protected_header(header(r#"{"kid":"a"}"#));
let compact = encrypt(&e, b"x", Serialization::Compact, &mut OsRandom, None)
.unwrap_or_else(|e| panic!("{e}"));
let p = compact.protected.as_ref().unwrap_or_else(|| panic!("none"));
assert_eq!(p.alg.as_deref(), Some("A128KW"));
assert_eq!(p.kid.as_deref(), Some("a"));
assert_eq!(
compact.protected_b64,
b64url_encode(br#"{"alg":"A128KW","enc":"A128GCM","kid":"a"}"#)
);
let general_form = Serialization::Json(JsonForm::General);
let general =
encrypt(&e, b"x", general_form, &mut OsRandom, None).unwrap_or_else(|e| panic!("{e}"));
let p = general.protected.as_ref().unwrap_or_else(|| panic!("none"));
assert!(p.alg.is_none());
let r = general.recipients[0]
.header
.as_ref()
.unwrap_or_else(|| panic!("none"));
assert_eq!(r.alg.as_deref(), Some("A128KW"));
let d = Encryptor::new(ContentEncAlg::A128GCM)
.recipient(Recipient::new(A::Dir, &k).header(header(r#"{"kid":"r"}"#)));
let general =
encrypt(&d, b"x", general_form, &mut OsRandom, None).unwrap_or_else(|e| panic!("{e}"));
let p = general.protected.as_ref().unwrap_or_else(|| panic!("none"));
assert_eq!(p.alg.as_deref(), Some("dir"));
let r = general.recipients[0]
.header
.as_ref()
.unwrap_or_else(|| panic!("none"));
assert!(r.alg.is_none());
assert_eq!(r.kid.as_deref(), Some("r"));
}
#[test]
fn the_reproducible_hook_accepts_only_an_equal_header() {
let k = oct(&[1; 16], "");
let e = compact_encryptor(A::A128KW, &k).protected_header(header(r#"{"kid":"a"}"#));
let run = |given: &str| {
let mut rng = FixedRng::new([0u8; 28]);
encrypt(&e, b"x", Serialization::Compact, &mut rng, Some(given))
};
let reordered =
b64url_encode(b"{\"kid\":\"a\",\r\n \"enc\":\"A128GCM\", \"alg\":\"A128KW\"}");
let parts = run(&reordered).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(parts.protected_b64, reordered);
let policy = policy(&[A::A128KW]);
let d = decrypt(parts, &k, &policy).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(d.plaintext(), b"x");
for bad in [
br#"{"alg":"A128KW","enc":"A128GCM"}"#.as_slice(),
br#"{"alg":"A128KW","enc":"A128GCM","kid":"b"}"#,
br#"{"alg":"A128KW","enc":"A128GCM","kid":"a","x":1}"#,
br#"{"alg":"dir","enc":"A128GCM","kid":"a"}"#,
br#"{"alg":"A128KW","enc":"A128GCM","kid":"a","kid":"a"}"#,
b"[]",
] {
assert_eq!(code(run(&b64url_encode(bad))), "InvalidFormat");
}
assert_eq!(code(run("e30=")), "InvalidFormat");
}
#[test]
fn the_parts_hold_the_aad_in_a_buffer_wiped_on_drop() {
fn is_zeroizing(_: &Option<Zeroizing<Vec<u8>>>) {}
let k = oct(&[1; 16], "");
let e = compact_encryptor(A::A128KW, &k).aad(*b"MARKERaad");
let form = Serialization::Json(JsonForm::Flattened);
let parts = encrypt(&e, b"x", form, &mut OsRandom, None).unwrap_or_else(|e| panic!("{e}"));
is_zeroizing(&parts.aad);
assert_eq!(
parts.aad.as_deref().map(Vec::as_slice),
Some(&b"MARKERaad"[..])
);
let d = decrypt(parts, &k, &policy(&[A::A128KW])).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(
(d.plaintext(), d.aad()),
(&b"x"[..], Some(&b"MARKERaad"[..]))
);
}
}