use aes::cipher::{BlockEncryptMut, 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 rsa::RsaPublicKey;
use rsa::pkcs1::DecodeRsaPublicKey as _;
use rsa::rand_core::{OsRng, RngCore as _};
#[cfg(feature = "weak-algos")]
use sha1::Sha1;
use sha2::Sha256;
use x509_cert::Certificate;
use x509_cert::der::Decode as _;
use crate::crypto::cert::{PublicKeyAlgorithm, X509Certificate};
use crate::error::Error;
use crate::xml::emit::emit_element;
use crate::xml::parse::{Element, Node, QName};
use crate::xmlenc::algorithms::{DataEncryptionAlgorithm, KeyTransportAlgorithm};
const SAML_NS: &str = "urn:oasis:names:tc:SAML:2.0:assertion";
const XENC_NS: &str = "http://www.w3.org/2001/04/xmlenc#";
const XENC11_NS: &str = "http://www.w3.org/2009/xmlenc11#";
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 MGF1_SHA1_URI: &str = "http://www.w3.org/2009/xmlenc11#mgf1sha1";
const ENCRYPTED_DATA_TYPE_ELEMENT: &str = "http://www.w3.org/2001/04/xmlenc#Element";
pub(crate) fn encrypt_assertion(
assertion: &Element,
recipient_encryption_cert: &X509Certificate,
data_algorithm: DataEncryptionAlgorithm,
key_transport_algorithm: KeyTransportAlgorithm,
) -> Result<Element, Error> {
if recipient_encryption_cert.public_key().algorithm_family() != PublicKeyAlgorithm::Rsa {
return Err(Error::DisallowedAlgorithm {
alg: "key transport requires RSA cert".into(),
});
}
let rsa_public = rsa_public_key_from_cert(recipient_encryption_cert)?;
let plaintext = emit_element(assertion)?.into_bytes();
let mut session_key = vec![0u8; data_algorithm.key_size()];
fill_random(&mut session_key)?;
let (mut cipher_value, ciphertext_bytes) =
encrypt_data(data_algorithm, &session_key, &plaintext)?;
cipher_value.reserve_exact(ciphertext_bytes.len());
cipher_value.extend_from_slice(&ciphertext_bytes);
let data_cipher_b64 = BASE64_STANDARD.encode(&cipher_value);
let wrapped_key_bytes = wrap_session_key(&rsa_public, key_transport_algorithm, &session_key)?;
let wrapped_key_b64 = BASE64_STANDARD.encode(&wrapped_key_bytes);
Ok(build_encrypted_assertion_element(
data_algorithm,
key_transport_algorithm,
&wrapped_key_b64,
&data_cipher_b64,
))
}
fn rsa_public_key_from_cert(cert: &X509Certificate) -> Result<RsaPublicKey, Error> {
let parsed = Certificate::from_der(cert.to_der()).map_err(|_err| Error::X509Parse)?;
let key_bytes = parsed
.tbs_certificate
.subject_public_key_info
.subject_public_key
.as_bytes()
.ok_or(Error::X509Parse)?;
RsaPublicKey::from_pkcs1_der(key_bytes).map_err(|_err| Error::X509Parse)
}
fn fill_random(buf: &mut [u8]) -> Result<(), Error> {
let mut rng = OsRng;
rng.try_fill_bytes(buf)
.map_err(|_err| Error::DecryptFailed { reason: "rng" })
}
fn encrypt_data(
algorithm: DataEncryptionAlgorithm,
session_key: &[u8],
plaintext: &[u8],
) -> Result<(Vec<u8>, Vec<u8>), Error> {
match algorithm {
DataEncryptionAlgorithm::Aes128Cbc => {
encrypt_cbc_with_random_iv::<aes::Aes128>(session_key, plaintext)
}
DataEncryptionAlgorithm::Aes256Cbc => {
encrypt_cbc_with_random_iv::<aes::Aes256>(session_key, plaintext)
}
DataEncryptionAlgorithm::Aes128Gcm => encrypt_gcm::<Aes128Gcm>(session_key, plaintext),
DataEncryptionAlgorithm::Aes256Gcm => encrypt_gcm::<Aes256Gcm>(session_key, plaintext),
}
}
fn encrypt_cbc_with_random_iv<C>(
session_key: &[u8],
plaintext: &[u8],
) -> Result<(Vec<u8>, Vec<u8>), Error>
where
C: aes::cipher::BlockCipher + aes::cipher::BlockEncrypt + aes::cipher::KeyInit,
{
let mut iv = vec![0u8; 16];
fill_random(&mut iv)?;
let ct = encrypt_cbc::<C>(session_key, &iv, plaintext)?;
Ok((iv, ct))
}
fn encrypt_gcm<C>(session_key: &[u8], plaintext: &[u8]) -> Result<(Vec<u8>, Vec<u8>), Error>
where
C: KeyInit + Aead,
{
let mut nonce = vec![0u8; 12];
fill_random(&mut nonce)?;
let cipher = C::new_from_slice(session_key).map_err(|_err| Error::DecryptFailed {
reason: "key size mismatch",
})?;
let ct = cipher
.encrypt(
aes_gcm::Nonce::from_slice(&nonce),
Payload {
msg: plaintext,
aad: &[],
},
)
.map_err(|_err| Error::DecryptFailed { reason: "data" })?;
Ok((nonce, ct))
}
fn encrypt_cbc<C>(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, Error>
where
C: aes::cipher::BlockCipher + aes::cipher::BlockEncrypt + aes::cipher::KeyInit,
{
let encryptor =
cbc::Encryptor::<C>::new_from_slices(key, iv).map_err(|_err| Error::DecryptFailed {
reason: "key size mismatch",
})?;
let mut out = vec![
0u8;
plaintext
.len()
.checked_add(16)
.ok_or(Error::DecryptFailed { reason: "data" })?
];
let written = encryptor
.encrypt_padded_b2b_mut::<Pkcs7>(plaintext, &mut out)
.map_err(|_err| Error::DecryptFailed { reason: "data" })?
.len();
out.truncate(written);
Ok(out)
}
fn wrap_session_key(
public: &RsaPublicKey,
algorithm: KeyTransportAlgorithm,
session_key: &[u8],
) -> Result<Vec<u8>, Error> {
let mut rng = OsRng;
match algorithm {
KeyTransportAlgorithm::RsaOaep => {
let padding = rsa::Oaep::new::<Sha256>();
public
.encrypt(&mut rng, padding, session_key)
.map_err(|_err| Error::DecryptFailed {
reason: "key transport",
})
}
KeyTransportAlgorithm::RsaOaepMgf1Sha1 => {
#[cfg(feature = "weak-algos")]
{
let padding = rsa::Oaep::new::<Sha1>();
public
.encrypt(&mut rng, padding, session_key)
.map_err(|_err| Error::DecryptFailed {
reason: "key transport",
})
}
#[cfg(not(feature = "weak-algos"))]
{
Err(Error::DisallowedAlgorithm {
alg: "RSA-OAEP-SHA1 outbound requires the weak-algos feature".into(),
})
}
}
#[cfg(feature = "weak-algos")]
KeyTransportAlgorithm::RsaPkcs1V15 => {
Err(Error::DisallowedAlgorithm {
alg: "RSA-PKCS1-v1.5 outbound key transport is not supported".into(),
})
}
}
}
fn build_encrypted_assertion_element(
data_algorithm: DataEncryptionAlgorithm,
key_transport_algorithm: KeyTransportAlgorithm,
wrapped_key_b64: &str,
data_cipher_b64: &str,
) -> Element {
let key_cipher_value = Element::build(QName::new(Some(XENC_NS.to_owned()), "CipherValue"))
.with_text(wrapped_key_b64.to_owned())
.finish();
let key_cipher_data = Element::build(QName::new(Some(XENC_NS.to_owned()), "CipherData"))
.with_child(Node::Element(key_cipher_value))
.finish();
let key_em = build_key_transport_encryption_method(key_transport_algorithm);
let encrypted_key = Element::build(QName::new(Some(XENC_NS.to_owned()), "EncryptedKey"))
.with_child(Node::Element(key_em))
.with_child(Node::Element(key_cipher_data))
.finish();
let key_info = Element::build(QName::new(Some(DS_NS.to_owned()), "KeyInfo"))
.with_child(Node::Element(encrypted_key))
.finish();
let data_em = Element::build(QName::new(Some(XENC_NS.to_owned()), "EncryptionMethod"))
.with_attribute(
QName::new(None, "Algorithm"),
data_algorithm.uri().to_owned(),
)
.finish();
let data_cipher_value = Element::build(QName::new(Some(XENC_NS.to_owned()), "CipherValue"))
.with_text(data_cipher_b64.to_owned())
.finish();
let data_cipher_data = Element::build(QName::new(Some(XENC_NS.to_owned()), "CipherData"))
.with_child(Node::Element(data_cipher_value))
.finish();
let encrypted_data = Element::build(QName::new(Some(XENC_NS.to_owned()), "EncryptedData"))
.with_attribute(
QName::new(None, "Type"),
ENCRYPTED_DATA_TYPE_ELEMENT.to_owned(),
)
.with_child(Node::Element(data_em))
.with_child(Node::Element(key_info))
.with_child(Node::Element(data_cipher_data))
.finish();
let mut wrapper = Element::build(QName::new(Some(SAML_NS.to_owned()), "EncryptedAssertion"))
.with_namespace(Some("saml".to_owned()), SAML_NS)
.with_namespace(Some("xenc".to_owned()), XENC_NS)
.with_namespace(Some("ds".to_owned()), DS_NS);
if matches!(key_transport_algorithm, KeyTransportAlgorithm::RsaOaep) {
wrapper = wrapper.with_namespace(Some("xenc11".to_owned()), XENC11_NS);
}
wrapper.with_child(Node::Element(encrypted_data)).finish()
}
fn build_key_transport_encryption_method(algorithm: KeyTransportAlgorithm) -> Element {
match algorithm {
KeyTransportAlgorithm::RsaOaep => {
let digest = Element::build(QName::new(Some(DS_NS.to_owned()), "DigestMethod"))
.with_attribute(QName::new(None, "Algorithm"), SHA256_DIGEST_URI.to_owned())
.finish();
let mgf = Element::build(QName::new(Some(XENC11_NS.to_owned()), "MGF"))
.with_attribute(QName::new(None, "Algorithm"), MGF1_SHA1_URI.to_owned())
.finish();
Element::build(QName::new(Some(XENC_NS.to_owned()), "EncryptionMethod"))
.with_attribute(QName::new(None, "Algorithm"), algorithm.uri().to_owned())
.with_child(Node::Element(digest))
.with_child(Node::Element(mgf))
.finish()
}
KeyTransportAlgorithm::RsaOaepMgf1Sha1 => {
let digest = Element::build(QName::new(Some(DS_NS.to_owned()), "DigestMethod"))
.with_attribute(QName::new(None, "Algorithm"), SHA1_DIGEST_URI.to_owned())
.finish();
Element::build(QName::new(Some(XENC_NS.to_owned()), "EncryptionMethod"))
.with_attribute(QName::new(None, "Algorithm"), algorithm.uri().to_owned())
.with_child(Node::Element(digest))
.finish()
}
#[cfg(feature = "weak-algos")]
KeyTransportAlgorithm::RsaPkcs1V15 => {
Element::build(QName::new(Some(XENC_NS.to_owned()), "EncryptionMethod"))
.with_attribute(QName::new(None, "Algorithm"), algorithm.uri().to_owned())
.finish()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::cert::X509Certificate;
use crate::crypto::cert::test_vectors::RSA_CERT_PEM;
use crate::xml::parse::Document;
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()
}
#[test]
fn emits_well_formed_encrypted_assertion_aes256_gcm_rsa_oaep() {
let assertion = sample_assertion();
let cert = X509Certificate::from_pem(RSA_CERT_PEM).unwrap();
let encrypted = encrypt_assertion(
&assertion,
&cert,
DataEncryptionAlgorithm::Aes256Gcm,
KeyTransportAlgorithm::RsaOaep,
)
.expect("encrypt");
assert_eq!(encrypted.qname().namespace(), Some(SAML_NS));
assert_eq!(encrypted.qname().local(), "EncryptedAssertion");
let xml = emit_element(&encrypted).expect("emit");
let doc = Document::parse(xml.as_bytes()).expect("re-parse");
let enc_data = doc
.root()
.child_element(Some(XENC_NS), "EncryptedData")
.expect("EncryptedData");
assert_eq!(
enc_data.attribute(None, "Type"),
Some(ENCRYPTED_DATA_TYPE_ELEMENT)
);
let em = enc_data
.child_element(Some(XENC_NS), "EncryptionMethod")
.expect("EncryptionMethod");
assert_eq!(
em.attribute(None, "Algorithm"),
Some(DataEncryptionAlgorithm::Aes256Gcm.uri())
);
let key_info = enc_data
.child_element(Some(DS_NS), "KeyInfo")
.expect("KeyInfo");
let enc_key = key_info
.child_element(Some(XENC_NS), "EncryptedKey")
.expect("EncryptedKey");
let _ = enc_key
.child_element(Some(XENC_NS), "EncryptionMethod")
.expect("EncryptedKey/EncryptionMethod");
}
#[cfg(feature = "weak-algos")]
#[test]
fn emits_well_formed_encrypted_assertion_aes128_cbc_rsa_oaep_mgf1sha1() {
let assertion = sample_assertion();
let cert = X509Certificate::from_pem(RSA_CERT_PEM).unwrap();
let encrypted = encrypt_assertion(
&assertion,
&cert,
DataEncryptionAlgorithm::Aes128Cbc,
KeyTransportAlgorithm::RsaOaepMgf1Sha1,
)
.expect("encrypt CBC + OAEP-MGF1-SHA1");
let xml = emit_element(&encrypted).expect("emit");
let doc = Document::parse(xml.as_bytes()).expect("re-parse");
let em = doc
.root()
.child_element(Some(XENC_NS), "EncryptedData")
.and_then(|e| e.child_element(Some(XENC_NS), "EncryptionMethod"))
.unwrap();
assert_eq!(
em.attribute(None, "Algorithm"),
Some(DataEncryptionAlgorithm::Aes128Cbc.uri())
);
}
}