use aes::cipher::{BlockDecryptMut, KeyIvInit, block_padding::Pkcs7};
use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::{Aes128Gcm, Aes256Gcm};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use crate::crypto::keypair::{KeyPair, OaepDigest};
use crate::error::Error;
use crate::xml::parse::{Document, Element};
use crate::xmlenc::algorithms::{DataEncryptionAlgorithm, KeyTransportAlgorithm};
const XENC_NS: &str = "http://www.w3.org/2001/04/xmlenc#";
const DS_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
const SHA1_DIGEST_URI: &str = "http://www.w3.org/2000/09/xmldsig#sha1";
const SHA256_DIGEST_URI: &str = "http://www.w3.org/2001/04/xmlenc#sha256";
const SHA384_DIGEST_URI: &str = "http://www.w3.org/2001/04/xmldsig-more#sha384";
const SHA512_DIGEST_URI: &str = "http://www.w3.org/2001/04/xmlenc#sha512";
pub(crate) fn decrypt_encrypted_assertion(
encrypted_assertion: &Element,
decryption_keys: &[&KeyPair],
allowed_data_algorithms: &[DataEncryptionAlgorithm],
allowed_key_transport_algorithms: &[KeyTransportAlgorithm],
) -> Result<Element, Error> {
let encrypted_data = encrypted_assertion
.child_element(Some(XENC_NS), "EncryptedData")
.ok_or(Error::DecryptFailed {
reason: "missing EncryptedData",
})?;
let data_em = encrypted_data
.child_element(Some(XENC_NS), "EncryptionMethod")
.ok_or(Error::DecryptFailed {
reason: "missing EncryptedData/EncryptionMethod",
})?;
let data_alg_uri = data_em
.attribute(None, "Algorithm")
.ok_or(Error::DecryptFailed {
reason: "missing EncryptionMethod/@Algorithm",
})?;
let data_algorithm = DataEncryptionAlgorithm::from_uri(data_alg_uri)?;
if !allowed_data_algorithms.contains(&data_algorithm) {
return Err(Error::DisallowedAlgorithm {
alg: data_alg_uri.to_owned(),
});
}
let key_info =
encrypted_data
.child_element(Some(DS_NS), "KeyInfo")
.ok_or(Error::DecryptFailed {
reason: "missing KeyInfo",
})?;
let encrypted_key = key_info
.child_element(Some(XENC_NS), "EncryptedKey")
.ok_or(Error::DecryptFailed {
reason: "missing EncryptedKey",
})?;
let key_em = encrypted_key
.child_element(Some(XENC_NS), "EncryptionMethod")
.ok_or(Error::DecryptFailed {
reason: "missing EncryptedKey/EncryptionMethod",
})?;
let key_alg_uri = key_em
.attribute(None, "Algorithm")
.ok_or(Error::DecryptFailed {
reason: "missing EncryptedKey/EncryptionMethod/@Algorithm",
})?;
let key_transport_algorithm = KeyTransportAlgorithm::from_uri(key_alg_uri)?;
if !allowed_key_transport_algorithms.contains(&key_transport_algorithm) {
return Err(Error::DisallowedAlgorithm {
alg: key_alg_uri.to_owned(),
});
}
let oaep_digest = match key_transport_algorithm {
KeyTransportAlgorithm::RsaOaep => oaep_digest_from_method(key_em)?,
KeyTransportAlgorithm::RsaOaepMgf1Sha1 => OaepDigest::Sha1,
#[cfg(feature = "weak-algos")]
KeyTransportAlgorithm::RsaPkcs1V15 => OaepDigest::Sha1, };
let wrapped_key_bytes = extract_cipher_value(encrypted_key)?;
let session_key = unwrap_session_key(
&wrapped_key_bytes,
decryption_keys,
key_transport_algorithm,
oaep_digest,
)?;
if session_key.len() != data_algorithm.key_size() {
return Err(Error::DecryptFailed {
reason: "key size mismatch",
});
}
let ciphertext = extract_cipher_value(encrypted_data)?;
let plaintext = decrypt_data(data_algorithm, &session_key, &ciphertext)?;
let doc = Document::parse(&plaintext)?;
Ok(doc.root().clone())
}
fn oaep_digest_from_method(encryption_method: &Element) -> Result<OaepDigest, Error> {
let Some(digest_method) = encryption_method.child_element(Some(DS_NS), "DigestMethod") else {
return Ok(OaepDigest::Sha256);
};
let uri = digest_method
.attribute(None, "Algorithm")
.ok_or(Error::DecryptFailed {
reason: "missing DigestMethod/@Algorithm",
})?;
match uri {
SHA1_DIGEST_URI => Ok(OaepDigest::Sha1),
SHA256_DIGEST_URI => Ok(OaepDigest::Sha256),
SHA384_DIGEST_URI => Ok(OaepDigest::Sha384),
SHA512_DIGEST_URI => Ok(OaepDigest::Sha512),
other => Err(Error::DisallowedAlgorithm {
alg: other.to_owned(),
}),
}
}
fn extract_cipher_value(parent: &Element) -> Result<Vec<u8>, Error> {
let cipher_data =
parent
.child_element(Some(XENC_NS), "CipherData")
.ok_or(Error::DecryptFailed {
reason: "missing CipherData",
})?;
let cipher_value = cipher_data
.child_element(Some(XENC_NS), "CipherValue")
.ok_or(Error::DecryptFailed {
reason: "missing CipherValue",
})?;
decode_base64_ws_tolerant(&cipher_value.text_content())
}
fn decode_base64_ws_tolerant(text: &str) -> Result<Vec<u8>, Error> {
let cleaned: String = text.chars().filter(|c| !c.is_whitespace()).collect();
BASE64_STANDARD
.decode(cleaned.as_bytes())
.map_err(|_err| Error::Base64Decode)
}
fn unwrap_session_key(
wrapped: &[u8],
keys: &[&KeyPair],
algorithm: KeyTransportAlgorithm,
oaep_digest: OaepDigest,
) -> Result<Vec<u8>, Error> {
for key in keys {
let attempt = match algorithm {
KeyTransportAlgorithm::RsaOaep | KeyTransportAlgorithm::RsaOaepMgf1Sha1 => {
key.decrypt_rsa_oaep(wrapped, oaep_digest)
}
#[cfg(feature = "weak-algos")]
KeyTransportAlgorithm::RsaPkcs1V15 => key.decrypt_rsa_pkcs1v15(wrapped),
};
if let Ok(session_key) = attempt {
return Ok(session_key);
}
}
Err(Error::DecryptFailed {
reason: "key transport",
})
}
fn decrypt_data(
algorithm: DataEncryptionAlgorithm,
session_key: &[u8],
ciphertext: &[u8],
) -> Result<Vec<u8>, Error> {
match algorithm {
DataEncryptionAlgorithm::Aes128Cbc => decrypt_cbc::<aes::Aes128>(session_key, ciphertext),
DataEncryptionAlgorithm::Aes256Cbc => decrypt_cbc::<aes::Aes256>(session_key, ciphertext),
DataEncryptionAlgorithm::Aes128Gcm => decrypt_gcm::<Aes128Gcm>(session_key, ciphertext),
DataEncryptionAlgorithm::Aes256Gcm => decrypt_gcm::<Aes256Gcm>(session_key, ciphertext),
}
}
fn decrypt_cbc<C>(session_key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error>
where
C: aes::cipher::BlockCipher + aes::cipher::BlockDecrypt + aes::cipher::KeyInit,
{
if ciphertext.len() < 16 {
return Err(Error::DecryptFailed { reason: "data" });
}
let (iv, ct) = ciphertext.split_at(16);
let decryptor = cbc::Decryptor::<C>::new_from_slices(session_key, iv).map_err(|_err| {
Error::DecryptFailed {
reason: "key size mismatch",
}
})?;
let mut out = vec![0u8; ct.len()];
let written = decryptor
.decrypt_padded_b2b_mut::<Pkcs7>(ct, &mut out)
.map_err(|_err| Error::DecryptFailed { reason: "data" })?
.len();
out.truncate(written);
Ok(out)
}
fn decrypt_gcm<C>(session_key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error>
where
C: KeyInit + Aead,
{
if ciphertext.len() < 12 + 16 {
return Err(Error::DecryptFailed { reason: "data" });
}
let (nonce_bytes, ct_with_tag) = ciphertext.split_at(12);
let cipher = C::new_from_slice(session_key).map_err(|_err| Error::DecryptFailed {
reason: "key size mismatch",
})?;
cipher
.decrypt(
aes_gcm::Nonce::from_slice(nonce_bytes),
Payload {
msg: ct_with_tag,
aad: &[],
},
)
.map_err(|_err| Error::DecryptFailed { reason: "data" })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::cert::X509Certificate;
use crate::crypto::cert::test_vectors::{RSA_CERT_PEM, RSA_KEY_PKCS8_PEM};
use crate::xml::emit::emit_element;
use crate::xml::parse::{Document, Node, QName};
use crate::xmlenc::encrypt::encrypt_assertion;
const SAML_NS: &str = "urn:oasis:names:tc:SAML:2.0:assertion";
fn sample_assertion() -> Element {
let subject = Element::build(QName::new(Some(SAML_NS.to_owned()), "Subject"))
.with_text("alice@example.org")
.finish();
Element::build(QName::new(Some(SAML_NS.to_owned()), "Assertion"))
.with_namespace(Some("saml".to_owned()), SAML_NS)
.with_attribute(QName::new(None, "ID"), "_a1")
.with_child(Node::Element(subject))
.finish()
}
fn rsa_keypair() -> KeyPair {
KeyPair::from_pkcs8_pem(RSA_KEY_PKCS8_PEM).unwrap()
}
fn rsa_cert() -> X509Certificate {
X509Certificate::from_pem(RSA_CERT_PEM).unwrap()
}
fn roundtrip(
data: DataEncryptionAlgorithm,
kt: KeyTransportAlgorithm,
) -> Result<Element, Error> {
let assertion = sample_assertion();
let encrypted = encrypt_assertion(&assertion, &rsa_cert(), data, kt)?;
let kp = rsa_keypair();
decrypt_encrypted_assertion(&encrypted, &[&kp], &[data], &[kt])
}
#[test]
fn round_trip_aes256_gcm_rsa_oaep_sha256() {
let assertion = sample_assertion();
let encrypted = encrypt_assertion(
&assertion,
&rsa_cert(),
DataEncryptionAlgorithm::Aes256Gcm,
KeyTransportAlgorithm::RsaOaep,
)
.expect("encrypt");
let kp = rsa_keypair();
let decrypted = decrypt_encrypted_assertion(
&encrypted,
&[&kp],
&[DataEncryptionAlgorithm::Aes256Gcm],
&[KeyTransportAlgorithm::RsaOaep],
)
.expect("decrypt");
assert_eq!(decrypted.qname().local(), "Assertion");
assert_eq!(decrypted.qname().namespace(), Some(SAML_NS));
let subject = decrypted
.child_element(Some(SAML_NS), "Subject")
.expect("Subject");
assert_eq!(subject.text_content(), "alice@example.org");
}
#[test]
fn round_trip_aes128_gcm_rsa_oaep() {
let decrypted = roundtrip(
DataEncryptionAlgorithm::Aes128Gcm,
KeyTransportAlgorithm::RsaOaep,
)
.expect("round-trip");
assert_eq!(decrypted.qname().local(), "Assertion");
}
#[test]
fn round_trip_aes256_cbc_rsa_oaep() {
let decrypted = roundtrip(
DataEncryptionAlgorithm::Aes256Cbc,
KeyTransportAlgorithm::RsaOaep,
)
.expect("CBC round-trip");
let subject = decrypted
.child_element(Some(SAML_NS), "Subject")
.expect("Subject");
assert_eq!(subject.text_content(), "alice@example.org");
}
#[cfg(feature = "weak-algos")]
#[test]
fn round_trip_aes128_cbc_rsa_oaep_mgf1_sha1() {
let decrypted = roundtrip(
DataEncryptionAlgorithm::Aes128Cbc,
KeyTransportAlgorithm::RsaOaepMgf1Sha1,
)
.expect("legacy round-trip");
assert_eq!(decrypted.qname().local(), "Assertion");
}
#[test]
fn disallowed_data_algorithm_rejected() {
let assertion = sample_assertion();
let encrypted = encrypt_assertion(
&assertion,
&rsa_cert(),
DataEncryptionAlgorithm::Aes128Cbc,
KeyTransportAlgorithm::RsaOaep,
)
.expect("encrypt");
let kp = rsa_keypair();
let err = decrypt_encrypted_assertion(
&encrypted,
&[&kp],
&[DataEncryptionAlgorithm::Aes256Gcm],
&[KeyTransportAlgorithm::RsaOaep],
)
.expect_err("CBC not in allow-list");
match err {
Error::DisallowedAlgorithm { alg } => {
assert_eq!(alg, DataEncryptionAlgorithm::Aes128Cbc.uri());
}
other => panic!("expected DisallowedAlgorithm, got {other:?}"),
}
}
#[test]
fn disallowed_key_transport_algorithm_rejected() {
let assertion = sample_assertion();
let encrypted = encrypt_assertion(
&assertion,
&rsa_cert(),
DataEncryptionAlgorithm::Aes256Gcm,
KeyTransportAlgorithm::RsaOaep,
)
.expect("encrypt");
let kp = rsa_keypair();
let err = decrypt_encrypted_assertion(
&encrypted,
&[&kp],
&[DataEncryptionAlgorithm::Aes256Gcm],
&[KeyTransportAlgorithm::RsaOaepMgf1Sha1],
)
.expect_err("RsaOaep not in allow-list");
match err {
Error::DisallowedAlgorithm { alg } => {
assert_eq!(alg, KeyTransportAlgorithm::RsaOaep.uri());
}
other => panic!("expected DisallowedAlgorithm, got {other:?}"),
}
}
#[test]
fn key_size_mismatch_rejected() {
let assertion = sample_assertion();
let mut encrypted = encrypt_assertion(
&assertion,
&rsa_cert(),
DataEncryptionAlgorithm::Aes128Gcm,
KeyTransportAlgorithm::RsaOaep,
)
.expect("encrypt 128");
let new_uri = DataEncryptionAlgorithm::Aes256Gcm.uri().to_owned();
mutate_data_em_algorithm(&mut encrypted, new_uri);
let kp = rsa_keypair();
let err = decrypt_encrypted_assertion(
&encrypted,
&[&kp],
&[
DataEncryptionAlgorithm::Aes128Gcm,
DataEncryptionAlgorithm::Aes256Gcm,
],
&[KeyTransportAlgorithm::RsaOaep],
)
.expect_err("key size mismatch should be caught");
match err {
Error::DecryptFailed { reason } => assert_eq!(reason, "key size mismatch"),
other => panic!("expected DecryptFailed, got {other:?}"),
}
}
#[test]
fn gcm_tag_tamper_rejected() {
let assertion = sample_assertion();
let mut encrypted = encrypt_assertion(
&assertion,
&rsa_cert(),
DataEncryptionAlgorithm::Aes256Gcm,
KeyTransportAlgorithm::RsaOaep,
)
.expect("encrypt");
flip_last_byte_in_data_cipher_value(&mut encrypted);
let kp = rsa_keypair();
let err = decrypt_encrypted_assertion(
&encrypted,
&[&kp],
&[DataEncryptionAlgorithm::Aes256Gcm],
&[KeyTransportAlgorithm::RsaOaep],
)
.expect_err("tag tamper");
match err {
Error::DecryptFailed { reason } => assert_eq!(reason, "data"),
other => panic!("expected DecryptFailed, got {other:?}"),
}
}
#[test]
fn cbc_payload_tamper_rejected() {
let assertion = sample_assertion();
let mut encrypted = encrypt_assertion(
&assertion,
&rsa_cert(),
DataEncryptionAlgorithm::Aes256Cbc,
KeyTransportAlgorithm::RsaOaep,
)
.expect("encrypt CBC");
flip_last_byte_in_data_cipher_value(&mut encrypted);
let kp = rsa_keypair();
let err = decrypt_encrypted_assertion(
&encrypted,
&[&kp],
&[DataEncryptionAlgorithm::Aes256Cbc],
&[KeyTransportAlgorithm::RsaOaep],
)
.expect_err("CBC tamper");
match err {
Error::DecryptFailed { reason } => assert_eq!(reason, "data"),
Error::XmlParse(_) => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn rotation_first_key_fails_second_succeeds() {
let assertion = sample_assertion();
let encrypted = encrypt_assertion(
&assertion,
&rsa_cert(),
DataEncryptionAlgorithm::Aes256Gcm,
KeyTransportAlgorithm::RsaOaep,
)
.expect("encrypt");
use crate::crypto::cert::test_vectors::EC_P256_KEY_PKCS8_PEM;
let wrong = KeyPair::from_pkcs8_pem(EC_P256_KEY_PKCS8_PEM).unwrap();
let right = rsa_keypair();
let decrypted = decrypt_encrypted_assertion(
&encrypted,
&[&wrong, &right],
&[DataEncryptionAlgorithm::Aes256Gcm],
&[KeyTransportAlgorithm::RsaOaep],
)
.expect("rotation must find the matching key");
assert_eq!(decrypted.qname().local(), "Assertion");
}
#[test]
fn missing_encrypted_data_rejected() {
let wrapper = Element::build(QName::new(Some(SAML_NS.to_owned()), "EncryptedAssertion"))
.with_namespace(Some("saml".to_owned()), SAML_NS)
.finish();
let kp = rsa_keypair();
let err = decrypt_encrypted_assertion(
&wrapper,
&[&kp],
&[DataEncryptionAlgorithm::Aes256Gcm],
&[KeyTransportAlgorithm::RsaOaep],
)
.expect_err("no EncryptedData");
match err {
Error::DecryptFailed { reason } => assert_eq!(reason, "missing EncryptedData"),
other => panic!("expected DecryptFailed, got {other:?}"),
}
}
#[test]
fn all_keys_fail_collapses_to_generic_error() {
let assertion = sample_assertion();
let encrypted = encrypt_assertion(
&assertion,
&rsa_cert(),
DataEncryptionAlgorithm::Aes256Gcm,
KeyTransportAlgorithm::RsaOaep,
)
.expect("encrypt");
use crate::crypto::cert::test_vectors::EC_P256_KEY_PKCS8_PEM;
let wrong_ec = KeyPair::from_pkcs8_pem(EC_P256_KEY_PKCS8_PEM).unwrap();
let wrong_again = KeyPair::from_pkcs8_pem(EC_P256_KEY_PKCS8_PEM).unwrap();
let err = decrypt_encrypted_assertion(
&encrypted,
&[&wrong_ec, &wrong_again],
&[DataEncryptionAlgorithm::Aes256Gcm],
&[KeyTransportAlgorithm::RsaOaep],
)
.expect_err("no key works");
match err {
Error::DecryptFailed { reason } => assert_eq!(reason, "key transport"),
other => panic!("expected DecryptFailed, got {other:?}"),
}
}
#[test]
fn emit_then_parse_then_decrypt_round_trip() {
let assertion = sample_assertion();
let encrypted = encrypt_assertion(
&assertion,
&rsa_cert(),
DataEncryptionAlgorithm::Aes256Gcm,
KeyTransportAlgorithm::RsaOaep,
)
.expect("encrypt");
let xml = emit_element(&encrypted).expect("emit");
let doc = Document::parse(xml.as_bytes()).expect("re-parse");
let kp = rsa_keypair();
let decrypted = decrypt_encrypted_assertion(
doc.root(),
&[&kp],
&[DataEncryptionAlgorithm::Aes256Gcm],
&[KeyTransportAlgorithm::RsaOaep],
)
.expect("decrypt");
assert_eq!(decrypted.qname().local(), "Assertion");
}
fn mutate_data_em_algorithm(encrypted_assertion: &mut Element, new_uri: String) {
for child in &mut encrypted_assertion.children {
if let Node::Element(enc_data) = child
&& enc_data.qname.local == "EncryptedData"
{
for grandchild in &mut enc_data.children {
if let Node::Element(em) = grandchild
&& em.qname.local == "EncryptionMethod"
{
for attr in &mut em.attributes {
if attr.qname.local == "Algorithm" {
attr.value = new_uri.clone();
return;
}
}
}
}
}
}
panic!("EncryptedData/EncryptionMethod/@Algorithm not found");
}
fn flip_last_byte_in_data_cipher_value(encrypted_assertion: &mut Element) {
for child in &mut encrypted_assertion.children {
if let Node::Element(enc_data) = child
&& enc_data.qname.local == "EncryptedData"
{
for grandchild in &mut enc_data.children {
if let Node::Element(cipher_data) = grandchild
&& cipher_data.qname.local == "CipherData"
{
for ggc in &mut cipher_data.children {
if let Node::Element(cipher_value) = ggc
&& cipher_value.qname.local == "CipherValue"
{
let mut bytes =
decode_base64_ws_tolerant(&cipher_value.text_content())
.expect("base64");
let last = bytes
.len()
.checked_sub(1)
.expect("CipherValue must be non-empty");
bytes[last] ^= 0x01;
let re_encoded = BASE64_STANDARD.encode(&bytes);
cipher_value.children.clear();
cipher_value.children.push(Node::Text(re_encoded));
return;
}
}
}
}
}
}
panic!("EncryptedData/CipherData/CipherValue not found");
}
}