use crate::xml_input as xml_sec_xml_input;
use std::{fmt, io::Write, sync::Arc};
use crate::xml::dom::{Document, Node};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use rsa::RsaPublicKey;
use xml_sec_xml_input::lexical::Writer;
use crate::document::{
DocumentParseSettings, XmlDocument, XmlDocumentError, XmlParseWorkBudget,
parse_borrowed_with_settings_and_budget,
};
use crate::operation::{
OperationExecutionContext, OperationNodeId, OperationNodeKind, OperationPlanError,
OperationResourceIdentity, OperationStage,
};
use crate::xml::{is_xml_1_0_character, is_xml_ncname};
use super::types::{XMLDSIG_NS, XMLENC_NS, XMLENC11_NS};
use super::{
DataEncryptionAlgorithm, DocumentEncryptionOptions, EncryptedDataType, EncryptionRecipient,
EncryptionResult, KeyWrapAlgorithm, ReplacementMode, RsaOaepParameters, XmlEncError,
has_single_element_with_boundary_trivia, map_document_error,
};
const XML_WHITESPACE: &[char] = &[' ', '\t', '\n', '\r'];
pub fn validate_rsa_recipient_key(
key: &RsaPublicKey,
policy: &crate::policy::EncryptionPolicy,
) -> Result<(), XmlEncError> {
validate_key_transport_recipient(key, policy)
}
pub fn validate_key_transport_recipient(
key: &dyn crate::provider::KeyTransportKey,
policy: &crate::policy::EncryptionPolicy,
) -> Result<(), XmlEncError> {
let modulus = key.rsa_modulus();
let exponent = key.rsa_exponent();
policy
.rsa_keys
.validate_components("encryption", &modulus, &exponent)?;
Ok(())
}
#[derive(Clone)]
pub struct EncryptedDataBuilder {
algorithm: DataEncryptionAlgorithm,
chacha: Option<super::ChaChaParameters>,
encrypted_type: EncryptedDataType,
id: Option<String>,
direct_key: Option<DirectEncryptionKey>,
direct_key_name: Option<String>,
recipients: Vec<EncryptionRecipient>,
policy: crate::policy::EncryptionPolicy,
provider: Arc<dyn crate::provider::CryptoProvider>,
xml_backend: crate::XmlBackend,
}
#[derive(Clone)]
enum DirectEncryptionKey {
Encapsulation(Arc<dyn crate::provider::KeyEncapsulationKey>),
Raw(zeroize::Zeroizing<Vec<u8>>),
Derived {
method: super::KeyDerivationMethod,
secret: zeroize::Zeroizing<Vec<u8>>,
},
Agreement {
method: super::KeyDerivationMethod,
key: Arc<dyn crate::provider::KeyAgreementKey>,
algorithm: crate::policy::KeyAgreementAlgorithm,
peer_public_key: Vec<u8>,
},
}
struct EphemeralSenderKey {
binding: Option<crate::provider::ProviderBinding>,
key: std::sync::Mutex<Option<Box<dyn crate::provider::KeyAgreementKey>>>,
}
impl crate::provider::KeyAgreementKey for EphemeralSenderKey {
fn provider_binding(&self) -> Option<&crate::provider::ProviderBinding> {
self.binding.as_ref()
}
fn agree(
&self,
parameters: &crate::provider::KeyAgreementParameters<'_>,
) -> Result<Vec<u8>, crate::provider::ProviderError> {
let key = self
.key
.lock()
.ok()
.and_then(|mut slot| slot.take())
.ok_or(crate::provider::ProviderError::InvalidInput(
crate::provider::ProviderInputError::ConsumedEphemeralKey,
))?;
key.agree(parameters)
}
}
enum OperationContentKey<'a> {
Established {
secret: zeroize::Zeroizing<[u8; 32]>,
width: usize,
},
Borrowed(&'a [u8]),
Owned(zeroize::Zeroizing<Vec<u8>>),
}
impl AsRef<[u8]> for OperationContentKey<'_> {
fn as_ref(&self) -> &[u8] {
match self {
Self::Established { secret, width } => &secret[..*width],
Self::Borrowed(bytes) => bytes,
Self::Owned(bytes) => bytes,
}
}
}
struct GeneratedEncryption {
result: EncryptionResult,
xml_nodes: usize,
mutation: Option<OperationNodeId>,
}
struct EncryptionPlanNodes {
document: OperationNodeId,
keys: Vec<OperationNodeId>,
crypto: OperationNodeId,
evidence: OperationNodeId,
mutation: Option<OperationNodeId>,
}
struct EncryptionOperationBudgets {
xml_parse: XmlParseWorkBudget,
key_establishment: std::cell::RefCell<super::key_establishment_budget::KeyEstablishmentUsage>,
}
impl EncryptionOperationBudgets {
fn from_policy(policy: &crate::policy::EncryptionPolicy) -> Self {
Self {
xml_parse: XmlParseWorkBudget::from_resources(&policy.resources),
key_establishment: Default::default(),
}
}
}
fn compile_encryption_plan(
operation: &mut OperationExecutionContext<
crate::policy::EncryptionPolicy,
EncryptionOperationBudgets,
>,
key_count: usize,
mutation: bool,
input_resource: OperationResourceIdentity,
) -> Result<EncryptionPlanNodes, XmlEncError> {
let document = operation.add_node(
OperationNodeKind::Document,
OperationStage::Parse,
Some(input_resource),
);
let mut keys = Vec::with_capacity(key_count.max(1));
for index in 0..key_count.max(1) {
let key = operation.add_node(
OperationNodeKind::Key { index },
OperationStage::Resolve,
None,
);
operation
.add_dependency(key, document)
.map_err(map_encryption_plan_error)?;
keys.push(key);
}
let crypto = operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
for key in &keys {
operation
.add_dependency(crypto, *key)
.map_err(map_encryption_plan_error)?;
}
let evidence = operation.add_node(OperationNodeKind::Evidence, OperationStage::Evidence, None);
operation
.add_dependency(evidence, crypto)
.map_err(map_encryption_plan_error)?;
let mutation = mutation.then(|| {
let node = operation.add_node(OperationNodeKind::Mutation, OperationStage::Mutation, None);
operation
.add_dependency(node, evidence)
.expect("evidence-to-mutation stage order is fixed");
node
});
operation.compile().map_err(map_encryption_plan_error)?;
Ok(EncryptionPlanNodes {
document,
keys,
crypto,
evidence,
mutation,
})
}
fn map_encryption_plan_error(error: OperationPlanError) -> XmlEncError {
XmlEncError::from(error)
}
fn require_encryption_mutation(
mutation: Option<OperationNodeId>,
) -> Result<OperationNodeId, XmlEncError> {
mutation
.ok_or_else(|| XmlEncError::OperationPlan("encryption mutation node is unavailable".into()))
}
impl fmt::Debug for EncryptedDataBuilder {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("EncryptedDataBuilder")
.field("algorithm", &self.algorithm)
.field("encrypted_type", &self.encrypted_type)
.field("id", &self.id)
.field(
"direct_key",
&self.direct_key.as_ref().map(|_| "[REDACTED]"),
)
.field("direct_key_name", &self.direct_key_name)
.field("recipients", &self.recipients)
.field("policy", &self.policy)
.field("provider", &self.provider.name())
.finish()
}
}
impl EncryptedDataBuilder {
pub fn new(algorithm: DataEncryptionAlgorithm) -> Self {
Self {
algorithm,
chacha: if algorithm == DataEncryptionAlgorithm::ChaCha20Poly1305 {
Some(super::ChaChaParameters::default())
} else {
None
},
encrypted_type: EncryptedDataType::Element,
id: None,
direct_key: None,
direct_key_name: None,
recipients: Vec::new(),
policy: crate::policy::EncryptionPolicy::default(),
provider: Arc::new(crate::provider::RustCryptoProvider),
xml_backend: crate::XmlBackend::default(),
}
}
pub fn policy(mut self, policy: crate::policy::EncryptionPolicy) -> Self {
self.policy = policy;
self
}
pub fn chacha_parameters(mut self, parameters: super::ChaChaParameters) -> Self {
self.chacha = Some(parameters);
self
}
pub fn provider(mut self, provider: Arc<dyn crate::provider::CryptoProvider>) -> Self {
self.provider = provider;
self
}
pub fn xml_backend(mut self, backend: crate::XmlBackend) -> Self {
self.xml_backend = backend;
self
}
fn document_parse_settings(&self) -> DocumentParseSettings {
DocumentParseSettings::from_policy(&self.policy.xml, &self.policy.resources)
.with_backend(self.xml_backend)
}
pub fn encryption_type(mut self, encrypted_type: EncryptedDataType) -> Self {
self.encrypted_type = encrypted_type;
self
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn direct_key(mut self, key: impl Into<Vec<u8>>) -> Self {
self.direct_key = Some(DirectEncryptionKey::Raw(zeroize::Zeroizing::new(
key.into(),
)));
self
}
pub fn encapsulation_key(mut self, key: Arc<dyn crate::provider::KeyEncapsulationKey>) -> Self {
self.direct_key = Some(DirectEncryptionKey::Encapsulation(key));
self
}
pub fn derived_key(
mut self,
method: super::KeyDerivationMethod,
secret: impl Into<Vec<u8>>,
) -> Self {
self.direct_key = Some(DirectEncryptionKey::Derived {
method,
secret: zeroize::Zeroizing::new(secret.into()),
});
self
}
pub fn agreement_key(
mut self,
method: super::KeyDerivationMethod,
key: Box<dyn crate::provider::KeyAgreementKey>,
algorithm: crate::policy::KeyAgreementAlgorithm,
peer_public_key: impl Into<Vec<u8>>,
) -> Self {
let key: Arc<dyn crate::provider::KeyAgreementKey> =
if algorithm == crate::policy::KeyAgreementAlgorithm::EcdhEs {
Arc::new(EphemeralSenderKey {
binding: key.provider_binding().cloned(),
key: std::sync::Mutex::new(Some(key)),
})
} else {
Arc::from(key)
};
self.direct_key = Some(DirectEncryptionKey::Agreement {
method,
key,
algorithm,
peer_public_key: peer_public_key.into(),
});
self
}
pub fn direct_key_name(mut self, key_name: impl Into<String>) -> Self {
self.direct_key_name = Some(key_name.into());
self
}
pub fn add_recipient(mut self, recipient: EncryptionRecipient) -> Self {
self.recipients.push(recipient);
self
}
pub fn recipient_rsa_oaep(self, public_key: RsaPublicKey) -> Self {
self.add_recipient(EncryptionRecipient::rsa_oaep(public_key))
}
pub fn recipient_key_transport(
self,
public_key: Arc<dyn crate::provider::KeyTransportKey>,
) -> Self {
self.add_recipient(EncryptionRecipient::provider_key_transport(public_key))
}
pub fn recipient_aes_kw(self, kek: impl Into<Vec<u8>>, algorithm: KeyWrapAlgorithm) -> Self {
self.add_recipient(EncryptionRecipient::aes_key_wrap(kek, algorithm))
}
pub fn encrypt_xml(&self, xml: &str) -> Result<EncryptionResult, XmlEncError> {
self.policy.validate()?;
self.validate_plaintext_len(xml.len())?;
let mut operation = OperationExecutionContext::new(
self.policy.clone(),
EncryptionOperationBudgets::from_policy(&self.policy),
None,
);
validate_xml_plaintext(
xml,
&self.encrypted_type,
operation.policy(),
&operation.budgets().xml_parse,
self.xml_backend,
)?;
let generated = self.encrypt_payload_with_operation(
xml.as_bytes(),
Some(self.encrypted_type.clone()),
&mut operation,
false,
)?;
validate_standalone_encrypted_data_nodes(
generated.xml_nodes,
operation.policy().resources.effective_xml_nodes() as usize,
)?;
Ok(generated.result)
}
pub fn encrypt_binary(&self, data: &[u8]) -> Result<EncryptionResult, XmlEncError> {
self.policy.validate()?;
let mut operation = OperationExecutionContext::new(
self.policy.clone(),
EncryptionOperationBudgets::from_policy(&self.policy),
None,
);
let encrypted_type = match &self.encrypted_type {
EncryptedDataType::Other(uri) => Some(EncryptedDataType::Other(uri.clone())),
EncryptedDataType::Element | EncryptedDataType::Content => None,
};
let generated =
self.encrypt_payload_with_operation(data, encrypted_type, &mut operation, false)?;
validate_standalone_encrypted_data_nodes(
generated.xml_nodes,
operation.policy().resources.effective_xml_nodes() as usize,
)?;
Ok(generated.result)
}
pub fn encrypt_document(
&self,
xml: &str,
options: DocumentEncryptionOptions<'_>,
) -> Result<String, XmlEncError> {
self.policy.validate()?;
self.validate_document_len(xml.len())?;
let budgets = EncryptionOperationBudgets::from_policy(&self.policy);
let settings = self.document_parse_settings();
let mut document = XmlDocument::parse_with_settings_and_budget(
xml.to_owned(),
settings,
&budgets.xml_parse,
)
.map_err(|error| map_document_error(error, settings))?;
self.encrypt_owned_document_with_budgets(&mut document, options, budgets)?;
Ok(document.into_xml())
}
pub fn encrypt_owned_document(
&self,
document: &mut XmlDocument,
options: DocumentEncryptionOptions<'_>,
) -> Result<(), XmlEncError> {
self.encrypt_owned_document_with_budgets(
document,
options,
EncryptionOperationBudgets::from_policy(&self.policy),
)
}
fn encrypt_owned_document_with_budgets(
&self,
document: &mut XmlDocument,
options: DocumentEncryptionOptions<'_>,
budgets: EncryptionOperationBudgets,
) -> Result<(), XmlEncError> {
self.policy.validate()?;
document.validate_operation_policy(&self.policy.xml, &self.policy.resources)?;
let document_nodes = document.with_view(|view| view.node_count());
let (target, source, content_boundaries, selected_nodes) = document.with_view(|view| {
let selected = select_encryption_target(view.document(), options.element_id)?;
let source = &view.xml()[selected.range()];
let content_boundaries = match self.encrypted_type {
EncryptedDataType::Element => {
self.validate_plaintext_len(source.len())?;
None
}
EncryptedDataType::Content => {
let boundaries = element_content_boundaries(source)?;
self.validate_plaintext_len(boundaries.content.len())?;
Some(boundaries)
}
EncryptedDataType::Other(_) => None,
};
Ok::<_, XmlEncError>((
view.node_identity(selected),
source.to_owned(),
content_boundaries,
selected.descendants().count(),
))
})?;
let mut operation = OperationExecutionContext::new(
self.policy.clone(),
budgets,
Some((document.identity(), document.generation())),
);
document
.with_view(|view| operation.validate_document_view(view))
.map_err(map_encryption_plan_error)?;
match self.encrypted_type {
EncryptedDataType::Element => {
let generated = self.encrypt_payload_with_operation(
source.as_bytes(),
Some(EncryptedDataType::Element),
&mut operation,
true,
)?;
let result = generated.result;
validate_replacement_document_len(
document.as_xml().len(),
source.len(),
result.encrypted_data_xml.len(),
operation.policy().resources.max_xml_document_bytes,
)?;
validate_replacement_node_counts(
document_nodes,
selected_nodes,
generated.xml_nodes,
ReplacementMode::ReplaceElement,
operation.policy().resources.effective_xml_nodes() as usize,
)?;
let settings = self.document_parse_settings();
let mutation = require_encryption_mutation(generated.mutation)?;
operation.run_document_transition(mutation, document, |document, budgets| {
document
.replace_element_with_budget(
target,
&result.encrypted_data_xml,
settings,
&budgets.xml_parse,
)
.map_err(|error| map_document_error(error, settings))
})?;
Ok(())
}
EncryptedDataType::Content => {
let boundaries = content_boundaries.ok_or_else(|| {
XmlEncError::InvalidStructure(
"content encryption target boundaries are unavailable".into(),
)
})?;
let plaintext = &source[boundaries.content.clone()];
let generated = self.encrypt_payload_with_operation(
plaintext.as_bytes(),
Some(EncryptedDataType::Content),
&mut operation,
true,
)?;
let result = generated.result;
let (removed, inserted) = if boundaries.self_closing {
let slash = source[..boundaries.start_tag_end]
.rfind('/')
.ok_or_else(|| {
XmlEncError::InvalidStructure("self-closing tag has no slash".into())
})?;
(
source.len(),
slash
.saturating_add(result.encrypted_data_xml.len())
.saturating_add(boundaries.qualified_name.len())
.saturating_add(4),
)
} else {
(boundaries.content.len(), result.encrypted_data_xml.len())
};
validate_replacement_document_len(
document.as_xml().len(),
removed,
inserted,
operation.policy().resources.max_xml_document_bytes,
)?;
validate_replacement_node_counts(
document_nodes,
selected_nodes,
generated.xml_nodes,
ReplacementMode::ReplaceContent,
operation.policy().resources.effective_xml_nodes() as usize,
)?;
let settings = self.document_parse_settings();
let mutation = require_encryption_mutation(generated.mutation)?;
operation.run_document_transition(mutation, document, |document, budgets| {
document
.replace_content_with_budget(
target,
&result.encrypted_data_xml,
settings,
&budgets.xml_parse,
)
.map_err(|error| map_document_error(error, settings))
})?;
Ok(())
}
EncryptedDataType::Other(_) => Err(XmlEncError::InvalidEncryptionConfig(
"document encryption requires Element or Content Type".into(),
)),
}
}
fn encrypt_payload_with_operation(
&self,
plaintext: &[u8],
encrypted_type: Option<EncryptedDataType>,
operation: &mut OperationExecutionContext<
crate::policy::EncryptionPolicy,
EncryptionOperationBudgets,
>,
mutates_document: bool,
) -> Result<GeneratedEncryption, XmlEncError> {
validate_plaintext_len(
plaintext.len(),
operation.policy().resources.max_encryption_plaintext_bytes,
)?;
self.validate_configuration_with_policy(operation.policy())?;
let input_resource = OperationResourceIdentity::external("encryption-input", plaintext);
let plan = compile_encryption_plan(
operation,
self.recipients.len() + usize::from(self.direct_key.is_some()),
mutates_document,
input_resource.clone(),
)?;
let derivation_xml = operation.run_with_resource(plan.document, &input_resource, || {
match &self.direct_key {
Some(DirectEncryptionKey::Derived { method, .. }) => {
method.to_xml(&operation.policy().resources).map(Some)
}
Some(DirectEncryptionKey::Agreement { method, .. }) => {
method.to_xml(&operation.policy().resources).map(Some)
}
_ => Ok(None),
}
})?;
let (content_key, encrypted_keys, encapsulated_ciphertext) =
operation.run_batch(&plan.keys, || {
let mut encapsulated_ciphertext = None;
let content_key = match &self.direct_key {
Some(DirectEncryptionKey::Encapsulation(key)) => {
if self.algorithm.key_len() > 32 {
return Err(
crate::key_establishment::KeyEstablishmentError::Structure(
"consuming key exceeds KEM secret width",
)
.into(),
);
}
let result = operation
.budgets()
.key_establishment
.borrow_mut()
.encapsulate(
&operation.policy().key_establishment,
self.provider.as_ref(),
key.as_ref(),
)?;
encapsulated_ciphertext = Some(result.ciphertext);
OperationContentKey::Established {
secret: result.shared_secret,
width: self.algorithm.key_len(),
}
}
Some(DirectEncryptionKey::Raw(key)) => {
validate_content_key(self.algorithm, key)?;
OperationContentKey::Borrowed(key)
}
Some(DirectEncryptionKey::Derived { method, secret }) => {
let parameters = method.parameters(self.algorithm.key_len())?;
let key = operation
.budgets()
.key_establishment
.borrow_mut()
.derive_key(
&operation.policy().key_establishment,
self.provider.as_ref(),
¶meters,
secret,
)?;
OperationContentKey::Owned(key)
}
Some(DirectEncryptionKey::Agreement {
method,
key,
algorithm,
peer_public_key,
}) => {
let parameters = method.parameters(self.algorithm.key_len())?;
let agreement = crate::provider::KeyAgreementParameters {
algorithm: algorithm.uri(),
peer_public_key,
};
let content_key = operation
.budgets()
.key_establishment
.borrow_mut()
.agree_and_derive(
&operation.policy().key_establishment,
self.provider.as_ref(),
key.as_ref(),
&agreement,
¶meters,
)?;
OperationContentKey::Owned(content_key)
}
None => {
let key = random_bytes(self.provider.as_ref(), self.algorithm.key_len())?;
#[cfg(feature = "legacy-algorithms")]
let mut key = key;
#[cfg(feature = "legacy-algorithms")]
if self.algorithm == DataEncryptionAlgorithm::TripleDesCbc {
for octet in &mut key {
*octet = (*octet & 0xfe)
| (((*octet & 0xfe).count_ones() as u8 & 1) ^ 1);
}
}
OperationContentKey::Owned(zeroize::Zeroizing::new(key))
}
};
let encrypted_keys = self
.recipients
.iter()
.map(|recipient| {
wrap_content_key(self.provider.as_ref(), recipient, content_key.as_ref())
})
.collect::<Result<Vec<_>, _>>()?;
Ok::<_, XmlEncError>((content_key, encrypted_keys, encapsulated_ciphertext))
})?;
let mut generated_nonce = [0; 12];
let ciphertext = operation.run(plan.crypto, || {
let parameters = match self.chacha.as_ref() {
Some(parameters) => {
let mut parameters = parameters.borrowed();
if parameters.nonce.is_none() {
self.provider
.require_capability(crate::provider::ProviderCapability::Random)?;
self.provider.fill_random(&mut generated_nonce)?;
parameters.nonce = Some(&generated_nonce);
}
Some(parameters)
}
None => None,
};
encrypt_content(
self.provider.as_ref(),
self.algorithm,
content_key.as_ref(),
plaintext,
parameters,
)
})?;
let (encrypted_data_xml, xml_nodes) = operation.run(plan.evidence, || {
let encrypted_data_xml = render_encrypted_data(
&EncryptionXml {
encapsulation: match (&self.direct_key, encapsulated_ciphertext.as_deref()) {
(Some(DirectEncryptionKey::Encapsulation(key)), Some(ciphertext)) => {
Some((key.algorithm(), ciphertext))
}
_ => None,
},
algorithm: self.algorithm,
chacha: self.chacha.as_ref().map(|parameters| {
let mut parameters = parameters.borrowed();
if parameters.nonce.is_none() {
parameters.nonce = Some(&generated_nonce);
}
parameters
}),
encrypted_type: encrypted_type.as_ref(),
id: self.id.as_deref(),
direct_key_name: self.direct_key_name.as_deref(),
derivation_xml: derivation_xml.as_deref(),
agreement_algorithm: match &self.direct_key {
Some(DirectEncryptionKey::Agreement { algorithm, .. }) => Some(*algorithm),
_ => None,
},
encrypted_keys: &encrypted_keys,
ciphertext: &ciphertext,
},
operation.policy().resources.max_xml_document_bytes,
)?;
validate_document_len(
encrypted_data_xml.len(),
operation.policy().resources.max_xml_document_bytes,
)?;
let xml_nodes = count_generated_encrypted_data_nodes(
&encrypted_data_xml,
operation.policy(),
&operation.budgets().xml_parse,
self.xml_backend,
)?;
Ok::<_, XmlEncError>((encrypted_data_xml, xml_nodes))
})?;
let replacement = match encrypted_type {
Some(EncryptedDataType::Content) => ReplacementMode::ReplaceContent,
Some(EncryptedDataType::Element | EncryptedDataType::Other(_)) | None => {
ReplacementMode::ReplaceElement
}
};
Ok(GeneratedEncryption {
result: EncryptionResult {
encrypted_data_xml,
replacement,
},
xml_nodes,
mutation: plan.mutation,
})
}
fn validate_configuration_with_policy(
&self,
policy: &crate::policy::EncryptionPolicy,
) -> Result<(), XmlEncError> {
policy.validate()?;
if matches!(self.direct_key, Some(DirectEncryptionKey::Encapsulation(_))) {
policy.key_establishment.check_kem_content(self.algorithm)?;
}
let metadata_limit = policy.resources.max_encryption_metadata_bytes;
if let Some(parameters) = &self.chacha {
parameters.validate(self.algorithm)?;
validate_metadata("ChaCha AAD", parameters.aad.as_deref(), metadata_limit)?;
} else if matches!(
self.algorithm,
DataEncryptionAlgorithm::ChaCha20 | DataEncryptionAlgorithm::ChaCha20Poly1305
) {
return Err(XmlEncError::MissingRequired("ChaCha parameters"));
}
if let EncryptedDataType::Other(uri) = &self.encrypted_type {
validate_metadata("EncryptedData Type", Some(uri), metadata_limit)?;
}
crate::policy::check_content_algorithm(
policy.data_algorithms.as_ref(),
self.algorithm,
"encryption",
)?;
if self.recipients.len() > policy.resources.max_encryption_recipients {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::ENCRYPTION_RECIPIENTS,
maximum: policy.resources.max_encryption_recipients,
actual: self.recipients.len(),
}
.into());
}
let key_candidates = self.recipients.len() + usize::from(self.direct_key.is_some());
policy.resources.validate_key_candidates(key_candidates)?;
validate_metadata("EncryptedData Id", self.id.as_deref(), metadata_limit)?;
if self.id.as_deref().is_some_and(|id| !is_xml_ncname(id)) {
return Err(XmlEncError::InvalidEncryptionConfig(
"EncryptedData Id must be an XML NCName".into(),
));
}
validate_key_name(
"direct KeyName",
self.direct_key_name.as_deref(),
metadata_limit,
)?;
for recipient in &self.recipients {
match recipient {
#[cfg(feature = "legacy-algorithms")]
EncryptionRecipient::RsaPkcs1v15 {
public_key,
recipient,
key_name,
} => {
let algorithm = super::KeyTransportAlgorithm::RsaPkcs1v15;
if !policy
.key_transport_algorithms
.as_ref()
.is_some_and(|allowed| allowed.contains(&algorithm))
{
return Err(crate::policy::PolicyViolation::Algorithm {
operation: "encryption",
algorithm: algorithm.uri().into(),
}
.into());
}
validate_key_transport_recipient(public_key.as_ref(), policy)?;
validate_metadata(
"EncryptedKey Recipient",
recipient.as_deref(),
metadata_limit,
)?;
validate_key_name("EncryptedKey KeyName", key_name.as_deref(), metadata_limit)?;
}
EncryptionRecipient::RsaOaep {
public_key,
parameters,
recipient,
key_name,
} => {
if parameters.algorithm.requires_explicit_permission() {
return Err(XmlEncError::InvalidEncryptionConfig(
"RSA-OAEP recipient requires an OAEP algorithm".into(),
));
}
let mgf_uri = parameters.mgf_digest.mgf_uri().ok_or_else(|| {
XmlEncError::InvalidEncryptionConfig(
"OAEP mask digest has no XML Encryption identifier".into(),
)
})?;
validate_key_transport_recipient(public_key.as_ref(), policy)?;
if policy
.key_transport_algorithms
.as_ref()
.is_some_and(|allowed| !allowed.contains(¶meters.algorithm))
{
return Err(crate::policy::PolicyViolation::Algorithm {
operation: "encryption",
algorithm: parameters.algorithm.uri().to_string(),
}
.into());
}
if parameters.digest.requires_explicit_permission()
&& !policy
.oaep_digests
.as_ref()
.is_some_and(|allowed| allowed.contains(¶meters.digest))
{
return Err(crate::policy::PolicyViolation::Algorithm {
operation: "encryption",
algorithm: parameters.digest.uri().to_owned(),
}
.into());
}
if let Some(allowed) = &policy.oaep_digests {
let rejected_uri = if !allowed.contains(¶meters.digest) {
Some(parameters.digest.uri())
} else if !allowed.contains(¶meters.mgf_digest) {
Some(mgf_uri)
} else {
None
};
if let Some(algorithm) = rejected_uri {
return Err(crate::policy::PolicyViolation::Algorithm {
operation: "encryption",
algorithm: algorithm.to_owned(),
}
.into());
}
}
validate_metadata(
"EncryptedKey Recipient",
recipient.as_deref(),
metadata_limit,
)?;
validate_key_name("EncryptedKey KeyName", key_name.as_deref(), metadata_limit)?;
validate_metadata_len(parameters.label.len(), metadata_limit)?;
}
EncryptionRecipient::AesKeyWrap {
kek,
algorithm,
recipient,
key_name,
} => {
if policy
.key_wrap_algorithms
.as_ref()
.map_or(algorithm.requires_explicit_permission(), |allowed| {
!allowed.contains(algorithm)
})
{
return Err(crate::policy::PolicyViolation::Algorithm {
operation: "encryption",
algorithm: algorithm.uri().to_string(),
}
.into());
}
if kek.len() != algorithm.key_len() {
return Err(XmlEncError::InvalidEncryptionConfig(format!(
"{} requires a {}-byte key-encryption key, got {} bytes",
algorithm.uri(),
algorithm.key_len(),
kek.len()
)));
}
validate_metadata(
"EncryptedKey Recipient",
recipient.as_deref(),
metadata_limit,
)?;
validate_key_name("EncryptedKey KeyName", key_name.as_deref(), metadata_limit)?;
}
}
}
match (self.direct_key.is_some(), self.recipients.is_empty()) {
(false, true) => Err(XmlEncError::InvalidEncryptionConfig(
"configure a direct content key or at least one wrapped recipient".into(),
)),
(true, false) => Err(XmlEncError::InvalidEncryptionConfig(
"a direct content key cannot be combined with wrapped recipients".into(),
)),
_ if self.direct_key_name.is_some() && self.direct_key.is_none() => {
Err(XmlEncError::InvalidEncryptionConfig(
"direct KeyName requires a direct content key".into(),
))
}
_ => Ok(()),
}
}
fn validate_plaintext_len(&self, actual: usize) -> Result<(), XmlEncError> {
validate_plaintext_len(actual, self.policy.resources.max_encryption_plaintext_bytes)
}
fn validate_document_len(&self, actual: usize) -> Result<(), XmlEncError> {
validate_document_len(actual, self.policy.resources.max_xml_document_bytes)
}
}
fn validate_metadata(
field: &'static str,
value: Option<&str>,
maximum: usize,
) -> Result<(), XmlEncError> {
if value.is_some_and(|value| !value.chars().all(is_xml_1_0_character)) {
return Err(XmlEncError::InvalidEncryptionConfig(format!(
"{field} contains a character forbidden by XML 1.0"
)));
}
validate_metadata_len(value.map_or(0, str::len), maximum)
}
fn validate_key_name(
field: &'static str,
value: Option<&str>,
maximum: usize,
) -> Result<(), XmlEncError> {
if value.is_some_and(str::is_empty) {
return Err(XmlEncError::InvalidEncryptionConfig(format!(
"{field} must not be empty"
)));
}
validate_metadata(field, value, maximum)
}
fn validate_metadata_len(actual: usize, maximum: usize) -> Result<(), XmlEncError> {
if actual <= maximum {
Ok(())
} else {
Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
maximum,
actual,
}
.into())
}
}
#[derive(Debug)]
struct WrappedKey {
algorithm_uri: &'static str,
oaep: Option<RsaOaepParameters>,
recipient: Option<String>,
key_name: Option<String>,
ciphertext: Vec<u8>,
}
#[derive(Debug)]
struct ContentBoundaries {
content: std::ops::Range<usize>,
self_closing: bool,
qualified_name: String,
start_tag_end: usize,
}
fn validate_plaintext_len(actual: usize, maximum: usize) -> Result<(), XmlEncError> {
if actual <= maximum {
Ok(())
} else {
Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::ENCRYPTION_PLAINTEXT_BYTES,
maximum,
actual,
}
.into())
}
}
fn validate_document_len(actual: usize, maximum: usize) -> Result<(), XmlEncError> {
if actual > maximum {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DOCUMENT,
maximum,
actual,
}
.into());
}
Ok(())
}
fn validate_replacement_document_len(
document_len: usize,
removed_len: usize,
inserted_len: usize,
maximum: usize,
) -> Result<(), XmlEncError> {
let actual = document_len
.saturating_sub(removed_len)
.saturating_add(inserted_len);
validate_document_len(actual, maximum)
}
fn validate_replacement_node_counts(
document_nodes: usize,
selected_nodes: usize,
inserted_nodes: usize,
replacement: ReplacementMode,
maximum: usize,
) -> Result<(), XmlEncError> {
let removed_nodes = match replacement {
ReplacementMode::ReplaceElement => selected_nodes,
ReplacementMode::ReplaceContent => selected_nodes.saturating_sub(1),
};
let actual = document_nodes
.saturating_sub(removed_nodes)
.saturating_add(inserted_nodes);
if actual > maximum {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_NODES,
maximum,
actual,
}
.into());
}
Ok(())
}
fn count_generated_encrypted_data_nodes(
encrypted_data_xml: &str,
policy: &crate::policy::EncryptionPolicy,
parse_budget: &XmlParseWorkBudget,
backend: crate::XmlBackend,
) -> Result<usize, XmlEncError> {
let maximum_nodes = policy.resources.effective_xml_nodes();
let settings = DocumentParseSettings {
nodes_limit: maximum_nodes.saturating_add(1),
..DocumentParseSettings::from_policy(&policy.xml, &policy.resources).with_backend(backend)
};
let generated =
parse_borrowed_with_settings_and_budget(encrypted_data_xml, settings, Some(parse_budget))
.map_err(|error| match error {
XmlDocumentError::Parse(crate::xml::dom::ParseError::NodesLimitReached) => {
XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_NODES,
maximum: maximum_nodes as usize,
actual: maximum_nodes as usize + 1,
})
}
error => map_document_error(error, settings),
})?;
Ok(generated.root_element().descendants().count())
}
fn validate_standalone_encrypted_data_nodes(
encrypted_data_subtree_nodes: usize,
maximum: usize,
) -> Result<(), XmlEncError> {
let actual = encrypted_data_subtree_nodes.saturating_add(1);
if actual > maximum {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_NODES,
maximum,
actual,
}
.into());
}
Ok(())
}
fn validate_content_key(algorithm: DataEncryptionAlgorithm, key: &[u8]) -> Result<(), XmlEncError> {
if key.len() == algorithm.key_len() {
Ok(())
} else {
Err(XmlEncError::InvalidKeySize {
algorithm,
expected: algorithm.key_len(),
actual: key.len(),
})
}
}
fn random_bytes(
provider: &dyn crate::provider::CryptoProvider,
len: usize,
) -> Result<Vec<u8>, XmlEncError> {
provider.require_capability(crate::provider::ProviderCapability::Random)?;
let mut bytes = vec![0_u8; len];
provider.fill_random(&mut bytes)?;
Ok(bytes)
}
fn encrypt_content(
provider: &dyn crate::provider::CryptoProvider,
algorithm: DataEncryptionAlgorithm,
key: &[u8],
plaintext: &[u8],
parameters: Option<super::ChaChaParametersRef<'_>>,
) -> Result<Vec<u8>, XmlEncError> {
provider.require_capability(crate::provider::ProviderCapability::Encrypt(algorithm))?;
let ciphertext =
provider.encrypt_data_with_parameters(algorithm, key, plaintext, parameters)?;
super::types::validate_ciphertext_framing(algorithm, ciphertext.len())?;
let expected = algorithm
.ciphertext_len_for_plaintext(plaintext.len())
.ok_or(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::ENCRYPTION_PLAINTEXT_BYTES,
maximum: crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING,
actual: plaintext.len(),
})?;
if ciphertext.len() != expected {
return Err(crate::provider::ProviderError::InvalidOutputSize {
operation: crate::provider::ProviderOperation::Encrypt,
expected,
actual: ciphertext.len(),
}
.into());
}
Ok(ciphertext)
}
fn wrap_content_key(
provider: &dyn crate::provider::CryptoProvider,
recipient: &EncryptionRecipient,
content_key: &[u8],
) -> Result<WrappedKey, XmlEncError> {
match recipient {
#[cfg(feature = "legacy-algorithms")]
EncryptionRecipient::RsaPkcs1v15 {
public_key,
recipient,
key_name,
} => {
provider.require_capability(crate::provider::ProviderCapability::Pkcs1v15Transport)?;
let ciphertext = provider.transport_pkcs1v15(public_key.as_ref(), content_key)?;
let expected = public_key.rsa_modulus().len();
if ciphertext.len() != expected {
return Err(XmlEncError::InvalidWrappedKeyLength {
expected,
actual: ciphertext.len(),
});
}
Ok(WrappedKey {
algorithm_uri: super::KeyTransportAlgorithm::RsaPkcs1v15.uri(),
oaep: None,
recipient: recipient.clone(),
key_name: key_name.clone(),
ciphertext,
})
}
EncryptionRecipient::RsaOaep {
public_key,
parameters,
recipient,
key_name,
} => Ok(WrappedKey {
algorithm_uri: parameters.algorithm.uri(),
oaep: Some(parameters.clone()),
recipient: recipient.clone(),
key_name: key_name.clone(),
ciphertext: wrap_rsa_oaep(provider, public_key.as_ref(), parameters, content_key)?,
}),
EncryptionRecipient::AesKeyWrap {
kek,
algorithm,
recipient,
key_name,
} => {
provider
.require_capability(crate::provider::ProviderCapability::KeyWrap(*algorithm))?;
let wrapped = provider.wrap_key(*algorithm, kek, content_key)?;
let expected = algorithm
.wrapped_len(content_key.len())
.ok_or_else(|| XmlEncError::UnsupportedAlgorithm(algorithm.uri().into()))?;
if wrapped.len() != expected {
return Err(XmlEncError::InvalidWrappedKeyLength {
expected,
actual: wrapped.len(),
});
}
Ok(WrappedKey {
algorithm_uri: algorithm.uri(),
oaep: None,
recipient: recipient.clone(),
key_name: key_name.clone(),
ciphertext: wrapped,
})
}
}
}
fn wrap_rsa_oaep(
provider: &dyn crate::provider::CryptoProvider,
public_key: &dyn crate::provider::KeyTransportKey,
parameters: &RsaOaepParameters,
content_key: &[u8],
) -> Result<Vec<u8>, XmlEncError> {
provider.require_capability(crate::provider::ProviderCapability::KeyTransport(
parameters,
))?;
let ciphertext = provider
.transport_key(public_key, parameters, content_key)
.map_err(|error| match error {
crate::provider::ProviderError::Random(message) => XmlEncError::Rng(message),
crate::provider::ProviderError::InvalidInput(reason) => {
XmlEncError::InvalidEncryptionConfig(reason.to_string())
}
error => XmlEncError::RsaEncrypt(error.to_string()),
})?;
let expected = public_key.rsa_modulus().len();
if ciphertext.len() != expected {
return Err(XmlEncError::InvalidWrappedKeyLength {
expected,
actual: ciphertext.len(),
});
}
Ok(ciphertext)
}
struct EncryptionXml<'a> {
encapsulation: Option<(crate::provider::KeyEncapsulationAlgorithm, &'a [u8])>,
algorithm: DataEncryptionAlgorithm,
chacha: Option<super::ChaChaParametersRef<'a>>,
encrypted_type: Option<&'a EncryptedDataType>,
id: Option<&'a str>,
direct_key_name: Option<&'a str>,
derivation_xml: Option<&'a str>,
agreement_algorithm: Option<crate::policy::KeyAgreementAlgorithm>,
encrypted_keys: &'a [WrappedKey],
ciphertext: &'a [u8],
}
fn render_encrypted_data(
output: &EncryptionXml<'_>,
maximum: usize,
) -> Result<String, XmlEncError> {
let mut counter = Writer::new(EncryptionXmlSize { length: 0 });
write_encrypted_data(&mut counter, output)?;
let length = counter.into_inner().length;
validate_document_len(length, maximum)?;
let mut writer = Writer::new(Vec::with_capacity(length));
write_encrypted_data(&mut writer, output)?;
String::from_utf8(writer.into_inner())
.map_err(|error| XmlEncError::XmlSerialize(error.to_string()))
}
struct EncryptionXmlSize {
length: usize,
}
impl Write for EncryptionXmlSize {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.length = self
.length
.checked_add(bytes.len())
.ok_or_else(|| std::io::Error::other("encrypted XML size overflow"))?;
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn write_hex_element<W: Write>(
writer: &mut Writer<W>,
name: &str,
bytes: &[u8],
) -> Result<(), XmlEncError> {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut text = [0; 24];
for (pair, byte) in text.as_chunks_mut::<2>().0.iter_mut().zip(bytes) {
pair[0] = HEX[(byte >> 4) as usize];
pair[1] = HEX[(byte & 15) as usize];
}
write_text_element(
writer,
name,
core::str::from_utf8(&text[..bytes.len() * 2]).expect("ASCII hex"),
)
}
fn write_encrypted_data<W: Write>(
writer: &mut Writer<W>,
output: &EncryptionXml<'_>,
) -> Result<(), XmlEncError> {
let EncryptionXml {
encapsulation,
algorithm,
chacha,
encrypted_type,
id,
direct_key_name,
derivation_xml,
agreement_algorithm,
encrypted_keys,
ciphertext,
} = *output;
let encrypted_type_uri = encrypted_type.map(|value| match value {
EncryptedDataType::Element => "http://www.w3.org/2001/04/xmlenc#Element",
EncryptedDataType::Content => "http://www.w3.org/2001/04/xmlenc#Content",
EncryptedDataType::Other(uri) => uri.as_str(),
});
let root_attributes = [
("xmlns:xenc", XMLENC_NS),
("xmlns:xenc11", XMLENC11_NS),
("xmlns:ds", XMLDSIG_NS),
]
.into_iter()
.chain(id.map(|value| ("Id", value)))
.chain(encrypted_type_uri.map(|value| ("Type", value)));
write_start(writer, "xenc:EncryptedData", root_attributes)?;
if let Some(parameters) = chacha {
write_start(
writer,
"xenc:EncryptionMethod",
[
("Algorithm", algorithm.uri()),
("xmlns:more", "http://www.w3.org/2021/04/xmldsig-more#"),
],
)?;
write_hex_element(
writer,
"more:Nonce",
parameters
.nonce
.ok_or(XmlEncError::MissingRequired("ChaCha Nonce"))?,
)?;
if let Some(counter) = parameters.counter {
write_hex_element(writer, "more:Counter", &counter)?;
}
if let Some(aad) = parameters.aad {
write_text_element(writer, "more:AAD", aad)?;
}
write_end(writer, "xenc:EncryptionMethod")?;
} else {
write_empty_with_algorithm(writer, "xenc:EncryptionMethod", algorithm.uri())?;
}
if direct_key_name.is_some()
|| !encrypted_keys.is_empty()
|| derivation_xml.is_some()
|| encapsulation.is_some()
{
write_start(writer, "ds:KeyInfo", [])?;
if let Some((algorithm, ciphertext)) = encapsulation {
write_start(
writer,
"kem:EncapsulationMechanism",
[
("xmlns:kem", crate::key_establishment::ENCAPSULATION_NS),
("Algorithm", algorithm.uri()),
],
)?;
write_start(writer, "ds:KeyInfo", [])?;
write_end(writer, "ds:KeyInfo")?;
write_cipher_data(writer, ciphertext)?;
write_end(writer, "kem:EncapsulationMechanism")?;
}
if let Some(key_name) = direct_key_name {
write_text_element(writer, "ds:KeyName", key_name)?;
}
if let Some(derivation_xml) = derivation_xml {
if let Some(algorithm) = agreement_algorithm {
write_start(
writer,
"xenc:AgreementMethod",
[("Algorithm", algorithm.uri())],
)?;
} else {
write_start(writer, "xenc11:DerivedKey", [])?;
}
writer
.raw(derivation_xml)
.map_err(|error| XmlEncError::XmlSerialize(error.to_string()))?;
if let Some(name) = direct_key_name
&& agreement_algorithm.is_none()
{
write_text_element(writer, "xenc11:DerivedKeyName", name)?;
}
write_end(
writer,
if agreement_algorithm.is_some() {
"xenc:AgreementMethod"
} else {
"xenc11:DerivedKey"
},
)?;
}
for encrypted_key in encrypted_keys {
write_encrypted_key(writer, encrypted_key)?;
}
write_end(writer, "ds:KeyInfo")?;
}
write_cipher_data(writer, ciphertext)?;
write_end(writer, "xenc:EncryptedData")
}
fn write_encrypted_key<W: Write>(
writer: &mut Writer<W>,
encrypted_key: &WrappedKey,
) -> Result<(), XmlEncError> {
write_start(
writer,
"xenc:EncryptedKey",
encrypted_key
.recipient
.as_deref()
.map(|recipient| ("Recipient", recipient)),
)?;
if let Some(parameters) = encrypted_key.oaep.as_ref() {
write_start(
writer,
"xenc:EncryptionMethod",
[("Algorithm", encrypted_key.algorithm_uri)],
)?;
if !parameters.label.is_empty() {
write_text_element(
writer,
"xenc:OAEPparams",
&STANDARD.encode(¶meters.label),
)?;
}
write_empty_with_algorithm(writer, "ds:DigestMethod", parameters.digest.uri())?;
if parameters.algorithm == super::KeyTransportAlgorithm::RsaOaep11
|| parameters.mgf_digest != super::OaepDigestAlgorithm::Sha1
{
let uri = parameters.mgf_digest.mgf_uri().ok_or_else(|| {
XmlEncError::InvalidEncryptionConfig(
"OAEP mask digest has no XML Encryption identifier".into(),
)
})?;
write_empty_with_algorithm(writer, "xenc11:MGF", uri)?;
}
write_end(writer, "xenc:EncryptionMethod")?;
} else {
write_empty_with_algorithm(writer, "xenc:EncryptionMethod", encrypted_key.algorithm_uri)?;
}
if let Some(key_name) = encrypted_key.key_name.as_deref() {
write_start(writer, "ds:KeyInfo", [])?;
write_text_element(writer, "ds:KeyName", key_name)?;
write_end(writer, "ds:KeyInfo")?;
}
write_cipher_data(writer, &encrypted_key.ciphertext)?;
write_end(writer, "xenc:EncryptedKey")
}
fn write_cipher_data<W: Write>(writer: &mut Writer<W>, value: &[u8]) -> Result<(), XmlEncError> {
write_start(writer, "xenc:CipherData", [])?;
write_start(writer, "xenc:CipherValue", [])?;
let mut encoded = [0_u8; 4096];
for chunk in value.chunks(3072) {
let length = STANDARD
.encode_slice(chunk, &mut encoded)
.map_err(|error| XmlEncError::XmlSerialize(error.to_string()))?;
let text = std::str::from_utf8(&encoded[..length])
.map_err(|error| XmlEncError::XmlSerialize(error.to_string()))?;
writer
.raw(text)
.map_err(|error| XmlEncError::XmlSerialize(error.to_string()))?;
}
write_end(writer, "xenc:CipherValue")?;
write_end(writer, "xenc:CipherData")
}
fn write_empty_with_algorithm<W: Write>(
writer: &mut Writer<W>,
name: &str,
algorithm: &str,
) -> Result<(), XmlEncError> {
writer
.empty(name, [("Algorithm", algorithm)])
.map_err(|error| XmlEncError::XmlSerialize(error.to_string()))
}
fn write_text_element<W: Write>(
writer: &mut Writer<W>,
name: &str,
text: &str,
) -> Result<(), XmlEncError> {
write_start(writer, name, [])?;
writer
.text(text)
.map_err(|error| XmlEncError::XmlSerialize(error.to_string()))?;
write_end(writer, name)
}
fn write_start<'a, W: Write>(
writer: &mut Writer<W>,
name: &str,
attributes: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> Result<(), XmlEncError> {
writer
.start(name, attributes)
.map_err(|error| XmlEncError::XmlSerialize(error.to_string()))
}
fn write_end<W: Write>(writer: &mut Writer<W>, name: &str) -> Result<(), XmlEncError> {
writer
.end(name)
.map_err(|error| XmlEncError::XmlSerialize(error.to_string()))
}
fn validate_xml_plaintext(
xml: &str,
encrypted_type: &EncryptedDataType,
policy: &crate::policy::EncryptionPolicy,
parse_budget: &XmlParseWorkBudget,
backend: crate::XmlBackend,
) -> Result<(), XmlEncError> {
let settings =
DocumentParseSettings::from_policy(&policy.xml, &policy.resources).with_backend(backend);
match encrypted_type {
EncryptedDataType::Element => {
let document =
parse_borrowed_with_settings_and_budget(xml, settings, Some(parse_budget))
.map_err(|error| map_document_error(error, settings))?;
if !has_single_element_with_boundary_trivia(document.root()) {
return Err(XmlEncError::InvalidStructure(
"Element plaintext must contain exactly one element".into(),
));
}
Ok(())
}
EncryptedDataType::Content => {
const WRAPPER_START: &str = "<xmlsec-content>";
const WRAPPER_END: &str = "</xmlsec-content>";
policy.resources.validate_xml_document_len(xml.len())?;
let wrapped = format!("{WRAPPER_START}{xml}{WRAPPER_END}");
let wrapper_bytes = WRAPPER_START.len() + WRAPPER_END.len();
let wrapped_settings = DocumentParseSettings {
nodes_limit: settings.nodes_limit.saturating_add(1),
depth_limit: settings.depth_limit.saturating_add(1),
max_bytes: settings.max_bytes.saturating_add(wrapper_bytes),
..settings
};
parse_borrowed_with_settings_and_budget(&wrapped, wrapped_settings, Some(parse_budget))
.map_err(|error| match error {
XmlDocumentError::DocumentTooLarge { actual, .. } => {
XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DOCUMENT,
maximum: settings.max_bytes,
actual: actual.saturating_sub(wrapper_bytes),
})
}
XmlDocumentError::Parse(crate::xml::dom::ParseError::NodesLimitReached) => {
XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_NODES,
maximum: settings.nodes_limit as usize,
actual: settings.nodes_limit as usize + 1,
})
}
XmlDocumentError::DocumentTooDeep { actual, .. } => {
XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DEPTH,
maximum: settings.depth_limit,
actual: actual.saturating_sub(1),
})
}
error => map_document_error(error, wrapped_settings),
})?;
Ok(())
}
EncryptedDataType::Other(_) => Err(XmlEncError::InvalidEncryptionConfig(
"encrypt_xml requires Element or Content Type".into(),
)),
}
}
fn select_encryption_target<'a, 'input>(
document: &'a Document<'input>,
id: Option<&str>,
) -> Result<Node<'a, 'input>, XmlEncError> {
let Some(id) = id else {
return Ok(document.root_element());
};
let mut matches = document.descendants().filter(|node| {
node.is_element()
&& ["Id", "ID", "id"]
.iter()
.any(|name| node.attribute(*name) == Some(id))
});
let selected = matches
.next()
.ok_or(XmlEncError::EncryptionTargetNotFound)?;
if matches.next().is_some() {
return Err(XmlEncError::AmbiguousEncryptionTarget);
}
Ok(selected)
}
fn element_content_boundaries(source: &str) -> Result<ContentBoundaries, XmlEncError> {
let tag_end = find_start_tag_end(source)?;
let before_end = source[..tag_end].trim_end_matches(XML_WHITESPACE);
let self_closing = before_end.ends_with('/');
let name_end = source[1..]
.find(|character: char| character.is_ascii_whitespace() || matches!(character, '/' | '>'))
.map(|index| index + 1)
.ok_or_else(|| XmlEncError::InvalidStructure("source element has no name".into()))?;
let qualified_name = source[1..name_end].to_owned();
if self_closing {
return Ok(ContentBoundaries {
content: tag_end..tag_end,
self_closing: true,
qualified_name,
start_tag_end: tag_end,
});
}
let closing_start = source
.rfind("</")
.ok_or_else(|| XmlEncError::InvalidStructure("source element has no closing tag".into()))?;
Ok(ContentBoundaries {
content: tag_end + 1..closing_start,
self_closing: false,
qualified_name,
start_tag_end: tag_end,
})
}
fn find_start_tag_end(source: &str) -> Result<usize, XmlEncError> {
let mut quote = None;
for (index, character) in source.char_indices() {
match (quote, character) {
(Some(expected), actual) if expected == actual => quote = None,
(None, '\'' | '"') => quote = Some(character),
(None, '>') => return Ok(index),
_ => {}
}
}
Err(XmlEncError::InvalidStructure(
"source element start tag is unterminated".into(),
))
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use crate::rsa_encoding::RsaPublicKeyEncoding as _;
use getrandom::SysRng;
use getrandom::rand_core::UnwrapErr;
use rsa::{RsaPrivateKey, RsaPublicKey};
use super::*;
use crate::hard_limits::{
ENCRYPTION_METADATA_BYTE_CEILING as MAX_ENCRYPTION_METADATA_LEN,
ENCRYPTION_PLAINTEXT_BYTE_CEILING as MAX_ENCRYPTION_PLAINTEXT_LEN,
ENCRYPTION_RECIPIENT_CEILING as MAX_ENCRYPTION_RECIPIENTS,
XML_DOCUMENT_BYTE_CEILING as MAX_ENCRYPTION_DOCUMENT_LEN,
};
use crate::xmlenc::{
KekDecryptor, OaepDigestAlgorithm, PrivateKeyDecryptor, SymmetricKeyDecryptor, decrypt,
decrypt_document, parse_encrypted_data,
};
#[test]
fn streamed_ciphertext_preserves_chunk_boundaries_and_padding() {
for length in [0, 1, 2, 3, 3071, 3072, 3073, 3074, 6144, 6145] {
let bytes: Vec<_> = (0..length).map(|index| index as u8).collect();
let mut writer = Writer::new(Vec::new());
writer
.start("root", [("xmlns:xenc", XMLENC_NS)])
.expect("bound namespace");
write_cipher_data(&mut writer, &bytes).expect("streamed ciphertext");
writer.end("root").expect("close wrapper");
let output = String::from_utf8(writer.into_inner()).expect("base64 XML is UTF-8");
assert_eq!(
output,
format!(
"<root xmlns:xenc=\"{XMLENC_NS}\"><xenc:CipherData><xenc:CipherValue>{}</xenc:CipherValue></xenc:CipherData></root>",
STANDARD.encode(bytes),
)
);
}
}
#[test]
fn generated_xml_size_counts_escaping_and_checks_exact_boundary() {
let render = |maximum| {
render_encrypted_data(
&EncryptionXml {
encapsulation: None,
chacha: None,
algorithm: DataEncryptionAlgorithm::Aes128Gcm,
encrypted_type: Some(&EncryptedDataType::Element),
id: Some("id&value"),
direct_key_name: Some("key<&\r"),
derivation_xml: None,
agreement_algorithm: None,
encrypted_keys: &[],
ciphertext: &[7; 28],
},
maximum,
)
};
let output = render(MAX_ENCRYPTION_DOCUMENT_LEN).expect("generous output ceiling");
assert_eq!(render(output.len()).expect("exact output ceiling"), output);
assert!(matches!(render(output.len() - 1),
Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DOCUMENT,
maximum,
actual,
})) if maximum == output.len() - 1 && actual == output.len()
));
let document = Document::parse(&output).expect("serialized XML");
assert_eq!(document.root_element().attribute("Id"), Some("id&value"));
let name = document
.descendants()
.find(|node| node.has_tag_name((XMLDSIG_NS, "KeyName")))
.expect("serialized KeyName");
assert_eq!(name.text(), Some("key<&\r"));
}
#[test]
fn document_encryption_requires_a_compiled_mutation_node() {
assert!(matches!(
require_encryption_mutation(None),
Err(XmlEncError::OperationPlan(message))
if message == "encryption mutation node is unavailable"
));
}
#[derive(Debug)]
struct OverridingOutputProvider {
ciphertext: Option<Vec<u8>>,
wrapped_key: Option<Vec<u8>>,
transported_key: Option<Vec<u8>>,
transport_calls: AtomicUsize,
}
struct OpaqueTransportKey {
modulus: Vec<u8>,
exponent: Vec<u8>,
}
impl crate::provider::KeyTransportKey for OpaqueTransportKey {
fn rsa_modulus(&self) -> std::borrow::Cow<'_, [u8]> {
std::borrow::Cow::Borrowed(&self.modulus)
}
fn rsa_exponent(&self) -> std::borrow::Cow<'_, [u8]> {
std::borrow::Cow::Borrowed(&self.exponent)
}
fn transport_with_provider(
&self,
_provider: &dyn crate::provider::CryptoProvider,
_parameters: &RsaOaepParameters,
_plaintext: &[u8],
) -> Result<Vec<u8>, crate::provider::ProviderError> {
panic!("custom provider must own transport for its opaque key")
}
}
impl crate::provider::CryptoProvider for OverridingOutputProvider {
fn name(&self) -> &'static str {
"overriding-output-test"
}
fn supports(&self, capability: crate::provider::ProviderCapability<'_>) -> bool {
crate::provider::CryptoProvider::supports(
&crate::provider::RustCryptoProvider,
capability,
)
}
fn fill_random(&self, output: &mut [u8]) -> Result<(), crate::provider::ProviderError> {
crate::provider::CryptoProvider::fill_random(
&crate::provider::RustCryptoProvider,
output,
)
}
fn derive_key(
&self,
parameters: &crate::provider::KdfParameters<'_>,
secret: &[u8],
) -> Result<Vec<u8>, crate::provider::ProviderError> {
crate::provider::RustCryptoProvider.derive_key(parameters, secret)
}
#[cfg(feature = "xmldsig")]
fn digest(
&self,
algorithm: crate::xmldsig::DigestAlgorithm,
data: &[u8],
) -> Result<Vec<u8>, crate::provider::ProviderError> {
crate::provider::CryptoProvider::digest(
&crate::provider::RustCryptoProvider,
algorithm,
data,
)
}
#[cfg(feature = "xmldsig")]
fn sign(
&self,
key: &dyn crate::xmldsig::SigningKey,
algorithm: crate::xmldsig::SignatureAlgorithm,
data: &[u8],
) -> Result<Vec<u8>, crate::xmldsig::SigningKeyError> {
crate::provider::CryptoProvider::sign(
&crate::provider::RustCryptoProvider,
key,
algorithm,
data,
)
}
#[cfg(feature = "xmldsig")]
fn verify(
&self,
key: &dyn crate::xmldsig::VerifyingKey,
algorithm: crate::xmldsig::SignatureAlgorithm,
data: &[u8],
signature: &[u8],
) -> Result<bool, crate::xmldsig::DsigError> {
crate::provider::CryptoProvider::verify(
&crate::provider::RustCryptoProvider,
key,
algorithm,
data,
signature,
)
}
fn encrypt_data(
&self,
algorithm: DataEncryptionAlgorithm,
key: &[u8],
plaintext: &[u8],
) -> Result<Vec<u8>, crate::provider::ProviderError> {
if let Some(ciphertext) = &self.ciphertext {
return Ok(ciphertext.clone());
}
crate::provider::CryptoProvider::encrypt_data(
&crate::provider::RustCryptoProvider,
algorithm,
key,
plaintext,
)
}
fn decrypt_data(
&self,
algorithm: DataEncryptionAlgorithm,
key: &[u8],
ciphertext: &[u8],
) -> Result<Vec<u8>, crate::provider::ProviderError> {
crate::provider::CryptoProvider::decrypt_data(
&crate::provider::RustCryptoProvider,
algorithm,
key,
ciphertext,
)
}
fn wrap_key(
&self,
algorithm: KeyWrapAlgorithm,
kek: &[u8],
key: &[u8],
) -> Result<Vec<u8>, crate::provider::ProviderError> {
if let Some(wrapped_key) = &self.wrapped_key {
return Ok(wrapped_key.clone());
}
crate::provider::CryptoProvider::wrap_key(
&crate::provider::RustCryptoProvider,
algorithm,
kek,
key,
)
}
fn unwrap_key(
&self,
algorithm: KeyWrapAlgorithm,
kek: &[u8],
wrapped: &[u8],
) -> Result<Vec<u8>, crate::provider::ProviderError> {
crate::provider::CryptoProvider::unwrap_key(
&crate::provider::RustCryptoProvider,
algorithm,
kek,
wrapped,
)
}
fn transport_key(
&self,
key: &dyn crate::provider::KeyTransportKey,
parameters: &RsaOaepParameters,
plaintext: &[u8],
) -> Result<Vec<u8>, crate::provider::ProviderError> {
self.transport_calls.fetch_add(1, Ordering::Relaxed);
if let Some(transported_key) = &self.transported_key {
return Ok(transported_key.clone());
}
crate::provider::CryptoProvider::transport_key(
&crate::provider::RustCryptoProvider,
key,
parameters,
plaintext,
)
}
fn recover_key(
&self,
key: &dyn crate::provider::KeyRecoveryKey,
parameters: &RsaOaepParameters,
ciphertext: &[u8],
) -> Result<Vec<u8>, crate::provider::ProviderError> {
crate::provider::CryptoProvider::recover_key(
&crate::provider::RustCryptoProvider,
key,
parameters,
ciphertext,
)
}
}
#[test]
fn direct_key_round_trips_every_content_algorithm() {
for algorithm in [
DataEncryptionAlgorithm::Aes128Cbc,
DataEncryptionAlgorithm::Aes256Cbc,
DataEncryptionAlgorithm::Aes128Gcm,
DataEncryptionAlgorithm::Aes256Gcm,
] {
for plaintext in [b"".as_slice(), b"sixteen-byte-msg", b"not aligned"] {
let key = vec![0x31; algorithm.key_len()];
let encrypted = EncryptedDataBuilder::new(algorithm)
.direct_key(key.clone())
.direct_key_name("content-key")
.encrypt_binary(plaintext)
.expect("supported direct encryption must succeed");
assert_eq!(
decrypt(
&encrypted.encrypted_data_xml,
&SymmetricKeyDecryptor::new(key)
)
.expect("generated ciphertext must decrypt"),
super::super::DecryptedContent::Bytes(plaintext.to_vec())
);
}
}
}
#[test]
fn aes_key_wrap_round_trips_and_preserves_recipient_metadata() {
let kek = [0x44; 32];
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(
EncryptionRecipient::aes_key_wrap(kek, KeyWrapAlgorithm::AesKw256)
.recipient("service-a")
.key_name("shared-kek"),
)
.encrypt_xml("<secret>value</secret>")
.expect("AES-KW encryption must succeed");
let parsed = parse_encrypted_data(&encrypted.encrypted_data_xml)
.expect("generated EncryptedData must parse");
assert_eq!(
parsed.encrypted_keys[0].recipient.as_deref(),
Some("service-a")
);
assert_eq!(
parsed.encrypted_keys[0].key_name.as_deref(),
Some("shared-kek")
);
assert_eq!(
decrypt(&encrypted.encrypted_data_xml, &KekDecryptor::new(kek))
.expect("wrapped key must decrypt"),
super::super::DecryptedContent::Xml("<secret>value</secret>".into())
);
}
#[test]
fn aes_key_wrap_rejects_mismatched_kek_before_provider_dispatch() {
let builder = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.recipient_aes_kw([0x44; 32], KeyWrapAlgorithm::AesKw128);
assert!(matches!(
builder.validate_configuration_with_policy(&builder.policy),
Err(XmlEncError::InvalidEncryptionConfig(_))
));
}
#[test]
fn rsa_oaep_round_trips_configurable_parameters() {
let private = RsaPrivateKey::new(&mut UnwrapErr(SysRng), 2048)
.expect("test RSA key generation must succeed");
let public = RsaPublicKey::from(&private);
let parameters =
RsaOaepParameters::xmlenc11(OaepDigestAlgorithm::Sha256, OaepDigestAlgorithm::Sha512)
.label(b"recipient-label".to_vec());
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes256Gcm)
.add_recipient(
EncryptionRecipient::rsa_oaep(public)
.oaep_parameters(parameters)
.recipient("rsa-recipient"),
)
.encrypt_xml("<secret/>")
.expect("RSA-OAEP encryption must succeed");
assert_eq!(
decrypt(
&encrypted.encrypted_data_xml,
&PrivateKeyDecryptor::new(private)
)
.expect("RSA recipient must recover content key"),
super::super::DecryptedContent::Xml("<secret/>".into())
);
}
#[test]
fn legacy_oaep_serializes_explicit_non_default_mgf() {
let public = RsaPublicKey::from_public_key_pem(include_str!(
"../../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"
))
.expect("tracked RSA public key must parse");
let parameters = RsaOaepParameters {
algorithm: super::super::KeyTransportAlgorithm::RsaOaepMgf1p,
digest: OaepDigestAlgorithm::Sha256,
mgf_digest: OaepDigestAlgorithm::Sha256,
label: Vec::new(),
};
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(EncryptionRecipient::rsa_oaep(public).oaep_parameters(parameters))
.encrypt_binary(b"legacy OAEP with explicit MGF")
.expect("libxmlsec1-compatible legacy OAEP parameters must encrypt");
assert!(encrypted.encrypted_data_xml.contains("xenc11:MGF"));
}
#[test]
fn encryption_policy_reports_the_rejected_mgf_uri() {
let public = RsaPublicKey::from_public_key_pem(include_str!(
"../../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"
))
.expect("tracked RSA public key must parse");
let parameters = RsaOaepParameters {
algorithm: super::super::KeyTransportAlgorithm::RsaOaepMgf1p,
digest: OaepDigestAlgorithm::Sha256,
mgf_digest: OaepDigestAlgorithm::Sha384,
label: Vec::new(),
};
let policy = crate::policy::EncryptionPolicy {
key_transport_algorithms: Some(std::collections::HashSet::from([parameters.algorithm])),
oaep_digests: Some(std::collections::HashSet::from([parameters.digest])),
..crate::policy::EncryptionPolicy::default()
};
let builder = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.policy(policy)
.add_recipient(EncryptionRecipient::rsa_oaep(public).oaep_parameters(parameters));
assert!(matches!(
builder.validate_configuration_with_policy(&builder.policy),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::Algorithm { algorithm, .. }
)) if algorithm == OaepDigestAlgorithm::Sha384.mgf_uri().expect("MGF1-SHA384 URI")
));
}
#[test]
fn encrypt_document_replaces_element_and_self_closing_content() {
let key = [0x55; 16];
let document =
"<root><target ID=\"element\"><child/></target><empty ID=\"content\"/></root>";
let encrypted_element = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key(key)
.encrypt_document(
document,
DocumentEncryptionOptions {
element_id: Some("element"),
},
)
.expect("element replacement must succeed");
let decrypted_element =
decrypt_document(&encrypted_element, None, &SymmetricKeyDecryptor::new(key))
.expect("element replacement must round-trip");
assert_eq!(decrypted_element, document);
let encrypted_content = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.encryption_type(EncryptedDataType::Content)
.direct_key(key)
.encrypt_document(
document,
DocumentEncryptionOptions {
element_id: Some("content"),
},
)
.expect("self-closing content replacement must expand the element");
assert!(encrypted_content.contains("<empty ID=\"content\"><xenc:EncryptedData"));
let decrypted_content =
decrypt_document(&encrypted_content, None, &SymmetricKeyDecryptor::new(key))
.expect("empty content must decrypt");
assert_eq!(
decrypted_content,
"<root><target ID=\"element\"><child/></target><empty ID=\"content\"></empty></root>"
);
}
#[test]
fn invalid_configuration_and_bounds_fail_before_encryption() {
let no_key = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.encrypt_binary(b"data")
.expect_err("missing key source must fail");
assert!(matches!(no_key, XmlEncError::InvalidEncryptionConfig(_)));
assert!(
validate_plaintext_len(MAX_ENCRYPTION_PLAINTEXT_LEN, MAX_ENCRYPTION_PLAINTEXT_LEN,)
.is_ok()
);
assert!(matches!(
validate_plaintext_len(
MAX_ENCRYPTION_PLAINTEXT_LEN + 1,
MAX_ENCRYPTION_PLAINTEXT_LEN,
),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::ENCRYPTION_PLAINTEXT_BYTES,
..
}
))
));
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 15])
.encrypt_binary(b"data"),
Err(XmlEncError::InvalidKeySize { .. })
));
let too_many_recipients = (0..=MAX_ENCRYPTION_RECIPIENTS).fold(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm),
|builder, _| builder.recipient_aes_kw([0_u8; 16], KeyWrapAlgorithm::AesKw128),
);
assert!(matches!(
too_many_recipients.encrypt_binary(b"data"),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::ENCRYPTION_RECIPIENTS,
..
}
))
));
let oversized_metadata = "x".repeat(MAX_ENCRYPTION_METADATA_LEN + 1);
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.id(oversized_metadata)
.encrypt_binary(b"data"),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
..
}
))
));
}
#[test]
fn oversized_xml_is_rejected_before_parsing() {
let oversized_malformed = format!(
"<child>{}</unclosed>",
"x".repeat(MAX_ENCRYPTION_PLAINTEXT_LEN)
);
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.encryption_type(EncryptedDataType::Content)
.direct_key([0_u8; 16])
.encrypt_xml(&oversized_malformed),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::ENCRYPTION_PLAINTEXT_BYTES,
..
}
))
));
}
#[test]
fn oversized_document_is_rejected_before_parsing() {
let oversized_malformed = format!(
"<root>{}</unclosed>",
"x".repeat(MAX_ENCRYPTION_DOCUMENT_LEN)
);
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.encrypt_document(&oversized_malformed, DocumentEncryptionOptions::default()),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DOCUMENT,
..
}
))
));
}
#[test]
fn encrypted_replacement_must_fit_document_policy() {
for encrypted_type in [EncryptedDataType::Element, EncryptedDataType::Content] {
let document = "<root><target ID=\"selected\">x</target></root>";
let policy = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_document_bytes: document.len(),
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.encryption_type(encrypted_type)
.direct_key([0_u8; 16])
.policy(policy)
.encrypt_document(
document,
DocumentEncryptionOptions {
element_id: Some("selected"),
},
),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DOCUMENT,
..
}
))
));
}
}
#[test]
fn binary_encryption_preserves_an_opaque_type_hint() {
let result = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.encryption_type(EncryptedDataType::Other("urn:example:binary".into()))
.direct_key([0x42_u8; 16])
.encrypt_binary(b"opaque payload")
.expect("opaque binary Type must be accepted");
assert!(
result
.encrypted_data_xml
.contains("Type=\"urn:example:binary\"")
);
}
#[test]
fn binary_encryption_bounds_an_opaque_type_hint() {
let maximum = 64;
let policy = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_encryption_metadata_bytes: maximum,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let encrypt = |uri: String| {
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.encryption_type(EncryptedDataType::Other(uri))
.direct_key([0x42_u8; 16])
.policy(policy.clone())
.encrypt_binary(b"opaque payload")
};
encrypt(format!("urn:{}", "x".repeat(maximum - 4)))
.expect("metadata at the configured boundary must remain accepted");
assert!(matches!(
encrypt(format!("urn:{}", "x".repeat(maximum - 3))),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
maximum: 64,
actual: 65,
}
))
));
}
#[test]
fn encryption_policy_bounds_xml_nodes_at_both_parse_entry_points() {
let policy = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_nodes: 4,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let builder = || {
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy.clone())
};
let xml = "<root><a/><b/><c/><d/></root>";
assert!(matches!(
builder().encrypt_xml(xml),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_NODES,
maximum: 4,
actual: 5,
}
))
));
assert!(matches!(
builder().encrypt_document(xml, DocumentEncryptionOptions::default()),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_NODES,
maximum: 4,
actual: 5,
}
))
));
}
#[test]
fn encryption_entry_points_enforce_policy_depth() {
let xml = "<root><child><leaf/></child></root>";
let policy = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_depth: 2,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let builder = || {
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy.clone())
};
let mut document = XmlDocument::parse(xml).expect("wide retained fixture must parse");
assert!(matches!(
builder().encrypt_document(xml, DocumentEncryptionOptions::default()),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DEPTH,
maximum: 2,
actual: 3,
}
))
));
assert!(matches!(
builder().encrypt_owned_document(&mut document, DocumentEncryptionOptions::default()),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DEPTH,
maximum: 2,
actual: 3,
}
))
));
}
#[test]
fn encryption_plaintext_and_owned_mutations_use_the_active_depth() {
let policy = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_depth: 1,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let builder = || {
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy.clone())
};
assert!(matches!(
builder().encrypt_xml("<root><child/></root>"),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DEPTH,
maximum: 1,
actual: 2,
}
))
));
let generated = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.encryption_type(EncryptedDataType::Content)
.encrypt_xml("")
.expect("generated content replacement must parse");
let generated_document = Document::parse(&generated.encrypted_data_xml)
.expect("generated replacement must be XML");
let generated_depth = generated_document
.descendants()
.filter(|node| node.is_element())
.map(|node| {
node.ancestors()
.filter(|ancestor| ancestor.is_element())
.count()
})
.max()
.expect("generated replacement has elements");
let mutation_policy = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_depth: generated_depth,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let mut document = XmlDocument::parse("<root/>").expect("shallow fixture must parse");
let original = document.as_xml().to_owned();
let generation = document.generation();
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.encryption_type(EncryptedDataType::Content)
.policy(mutation_policy)
.encrypt_owned_document(&mut document, DocumentEncryptionOptions::default()),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DEPTH,
maximum,
actual,
}
)) if maximum == generated_depth && actual == generated_depth + 1
));
assert_eq!(document.as_xml(), original);
assert_eq!(document.generation(), generation);
}
#[test]
fn owned_encryption_checks_active_node_limit_before_target_selection() {
let policy = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_nodes: 1,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let mut document = XmlDocument::parse("<root><first/><second/></root>")
.expect("retained fixture must parse");
let before = document.as_xml().to_owned();
let error = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy)
.encrypt_owned_document(
&mut document,
DocumentEncryptionOptions {
element_id: Some("missing"),
},
)
.expect_err("active node ceiling must precede target selection");
assert!(matches!(
error,
XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_NODES,
maximum: 1,
actual,
}) if actual > 1
));
assert_eq!(document.as_xml(), before);
assert_eq!(document.generation(), 0);
}
#[test]
fn document_encryption_initial_parse_uses_the_policy_work_budget() {
let xml = "<root/>";
let policy = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_parse_work_bytes: 0,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let error = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy)
.encrypt_document(xml, DocumentEncryptionOptions::default())
.expect_err("a zero parse-work budget must reject the input parse");
assert!(matches!(
error,
XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
maximum: 0,
actual,
}) if actual == xml.len()
));
}
#[test]
fn content_plaintext_node_limit_excludes_the_internal_wrapper() {
let policy = |max_xml_nodes| crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_nodes,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let policy_three = policy(3);
let budget_three = XmlParseWorkBudget::from_resources(&policy_three.resources);
validate_xml_plaintext(
"<first/><second/>",
&EncryptedDataType::Content,
&policy_three,
&budget_three,
crate::XmlBackend::default(),
)
.expect("the caller root and two elements must fit a three-node policy");
let policy_two = policy(2);
let budget_two = XmlParseWorkBudget::from_resources(&policy_two.resources);
assert!(matches!(
validate_xml_plaintext(
"<first/><second/>",
&EncryptedDataType::Content,
&policy_two,
&budget_two,
crate::XmlBackend::default(),
),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_NODES,
maximum: 2,
actual: 3,
}
))
));
}
#[test]
fn content_plaintext_byte_limit_excludes_only_the_internal_wrapper() {
let xml = "<first/><second/>";
let maximum = xml.len() - 1;
let policy = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_document_bytes: maximum,
max_encryption_plaintext_bytes: xml.len(),
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let budget = XmlParseWorkBudget::from_resources(&policy.resources);
assert!(matches!(
validate_xml_plaintext(
xml,
&EncryptedDataType::Content,
&policy,
&budget,
crate::XmlBackend::default(),
),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DOCUMENT,
maximum: observed_maximum,
actual,
}
)) if observed_maximum == maximum && actual == xml.len()
));
assert_eq!(
budget.consumed(),
0,
"oversized XML must fail before parsing"
);
}
#[test]
fn generated_encrypted_data_parse_uses_the_active_node_limit() {
let xml = "<EncryptedData><CipherData><CipherValue>AA==</CipherValue></CipherData></EncryptedData>";
let policy = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_nodes: 1,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let budget = XmlParseWorkBudget::from_resources(&policy.resources);
assert!(matches!(
count_generated_encrypted_data_nodes(
xml,
&policy,
&budget,
crate::XmlBackend::default(),
),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_NODES,
maximum: 1,
actual: 2,
}
))
));
}
#[test]
fn document_encryption_bounds_projected_replacement_nodes() {
fn policy(max_xml_nodes: usize) -> crate::policy::EncryptionPolicy {
crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_nodes,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
}
}
let generated_nodes = {
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.encrypt_binary(b"payload")
.expect("default policy must permit generated EncryptedData");
Document::parse(&encrypted.encrypted_data_xml)
.expect("generated EncryptedData must parse")
.root_element()
.descendants()
.count()
};
let element_actual = match EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy(generated_nodes))
.encrypt_document("<root/>", DocumentEncryptionOptions::default())
{
Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: "XML nodes",
maximum,
actual,
})) if maximum == generated_nodes && actual > maximum => actual,
result => panic!("expected projected element node bound, got {result:?}"),
};
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy(element_actual))
.encrypt_document("<root/>", DocumentEncryptionOptions::default())
.expect("the exact projected element node limit must be accepted");
let content_actual = match EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.encryption_type(EncryptedDataType::Content)
.direct_key([0_u8; 16])
.policy(policy(generated_nodes))
.encrypt_document("<root/>", DocumentEncryptionOptions::default())
{
Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: "XML nodes",
maximum,
actual,
})) if maximum == generated_nodes && actual > maximum => actual,
result => panic!("expected projected content node bound, got {result:?}"),
};
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.encryption_type(EncryptedDataType::Content)
.direct_key([0_u8; 16])
.policy(policy(content_actual))
.encrypt_document("<root/>", DocumentEncryptionOptions::default())
.expect("the exact projected content node limit must be accepted");
}
#[test]
fn standalone_encryption_bounds_generated_xml_nodes() {
fn policy(max_xml_nodes: usize) -> crate::policy::EncryptionPolicy {
crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_nodes,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
}
}
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy(1))
.encrypt_binary(b"payload"),
Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: "XML nodes",
maximum: 1,
actual,
})) if actual > 1
));
let generated = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.encrypt_binary(b"payload")
.expect("default policy must permit generated EncryptedData");
let document = Document::parse(&generated.encrypted_data_xml)
.expect("generated EncryptedData must parse");
let actual = document.root().descendants().count();
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy(actual - 1))
.encrypt_binary(b"payload"),
Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: "XML nodes",
maximum,
actual: reported,
})) if maximum == actual - 1 && reported == actual
));
let exact = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy(actual))
.encrypt_binary(b"payload")
.expect("the exact generated node limit must be accepted");
let exact_document =
Document::parse(&exact.encrypted_data_xml).expect("exact-boundary output must parse");
let decryption_policy = crate::policy::DecryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_nodes: actual,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::DecryptionPolicy::default()
};
super::super::parse::parse_encrypted_data_node_with_policy(
exact_document.root_element(),
&decryption_policy,
)
.expect("output accepted at the exact limit must be consumable at the same limit");
}
#[test]
fn document_dtd_is_controlled_only_by_operation_policy() {
let document = "<!DOCTYPE root [<!ELEMENT root ANY>]><root/>";
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.encrypt_document(document, DocumentEncryptionOptions { element_id: None },),
Err(XmlEncError::XmlParse(_))
));
let mut policy = crate::policy::EncryptionPolicy::default();
policy.xml.allow_internal_dtd = true;
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy.clone())
.encrypt_document(document, DocumentEncryptionOptions { element_id: None })
.expect("the explicit operation policy should permit parsing");
}
#[test]
fn invalid_resource_policy_is_rejected_at_every_entry_point() {
let mut policy = crate::policy::EncryptionPolicy::default();
policy.resources.max_encryption_plaintext_bytes =
crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING + 1;
let builder = || {
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy.clone())
};
assert!(matches!(
builder().encrypt_xml("<broken>"),
Err(XmlEncError::Policy(_))
));
assert!(matches!(
builder().encrypt_binary(b"x"),
Err(XmlEncError::Policy(_))
));
assert!(matches!(
builder().encrypt_document("<broken>", DocumentEncryptionOptions::default()),
Err(XmlEncError::Policy(_))
));
}
#[test]
fn standalone_encrypted_output_obeys_document_byte_ceiling() {
let policy = |maximum| crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_xml_document_bytes: maximum,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let binary_len = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.encrypt_binary(b"bounded binary")
.expect("baseline binary encryption must succeed")
.encrypted_data_xml
.len();
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy(binary_len - 1))
.encrypt_binary(b"bounded binary"),
Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DOCUMENT,
maximum,
actual,
}))
if maximum == binary_len - 1 && actual == binary_len
));
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy(binary_len))
.encrypt_binary(b"bounded binary")
.expect("the exact standalone binary output bound must be accepted");
let xml_len = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.encrypt_xml("<secret>bounded XML</secret>")
.expect("baseline XML encryption must succeed")
.encrypted_data_xml
.len();
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy(xml_len - 1))
.encrypt_xml("<secret>bounded XML</secret>"),
Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XML_DOCUMENT,
maximum,
actual,
}))
if maximum == xml_len - 1 && actual == xml_len
));
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy(xml_len))
.encrypt_xml("<secret>bounded XML</secret>")
.expect("the exact standalone XML output bound must be accepted");
}
#[test]
fn zero_resource_ceilings_allow_operations_that_consume_none() {
let policy = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_encryption_plaintext_bytes: 0,
max_encryption_recipients: 0,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy.clone())
.encrypt_binary(&[])
.expect("zero ceilings must allow resources the operation does not consume");
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.policy(policy.clone())
.encrypt_binary(b"x"),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::ENCRYPTION_PLAINTEXT_BYTES,
maximum: 0,
actual: 1
}
))
));
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.recipient_aes_kw([0_u8; 16], KeyWrapAlgorithm::AesKw128)
.policy(policy)
.encrypt_binary(&[]),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::ENCRYPTION_RECIPIENTS,
maximum: 0,
actual: 1,
}
))
));
}
#[test]
fn encryption_enforces_key_candidate_budget_before_inspection() {
let deny_keys = crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_key_candidates: 0,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
for error in [
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 15])
.policy(deny_keys.clone())
.encrypt_binary(b"data")
.expect_err("a direct key must consume one candidate"),
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.recipient_aes_kw([0_u8; 15], KeyWrapAlgorithm::AesKw128)
.policy(deny_keys)
.encrypt_binary(b"data")
.expect_err("a recipient key must consume one candidate"),
] {
assert!(matches!(
error,
XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_CANDIDATES,
maximum: 0,
actual: 1,
})
));
}
let recipient =
|| EncryptionRecipient::aes_key_wrap([0_u8; 16], KeyWrapAlgorithm::AesKw128);
let policy_with_candidate_limit = |maximum| crate::policy::EncryptionPolicy {
resources: crate::policy::ResourcePolicy {
max_key_candidates: maximum,
..crate::policy::ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
let builder = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(recipient())
.add_recipient(recipient());
assert!(matches!(
builder
.clone()
.policy(policy_with_candidate_limit(1))
.encrypt_binary(b"data"),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_CANDIDATES,
maximum: 1,
actual: 2,
}
))
));
builder
.policy(policy_with_candidate_limit(2))
.encrypt_binary(b"data")
.expect("the exact key-candidate boundary must be accepted");
}
#[test]
fn element_plaintext_enforces_replacement_node_contract() {
let builder =
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm).direct_key([0_u8; 16]);
builder
.encrypt_xml("\n<!--before--><secret/><!--after-->\n")
.expect("one element with boundary trivia must be accepted");
assert!(matches!(
builder.encrypt_xml("<?target value?><secret/>"),
Err(XmlEncError::InvalidStructure(_))
));
}
#[test]
fn empty_key_names_are_rejected_before_serialization() {
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.direct_key_name("")
.encrypt_binary(b"data"),
Err(XmlEncError::InvalidEncryptionConfig(_))
));
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(
EncryptionRecipient::aes_key_wrap([0_u8; 16], KeyWrapAlgorithm::AesKw128)
.key_name("")
)
.encrypt_binary(b"data"),
Err(XmlEncError::InvalidEncryptionConfig(_))
));
}
#[test]
fn xml_forbidden_metadata_characters_are_rejected() {
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.id("invalid\0id")
.encrypt_binary(b"data"),
Err(XmlEncError::InvalidEncryptionConfig(_))
));
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.direct_key_name("invalid\0name")
.encrypt_binary(b"data"),
Err(XmlEncError::InvalidEncryptionConfig(_))
));
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(
EncryptionRecipient::aes_key_wrap([0_u8; 16], KeyWrapAlgorithm::AesKw128)
.recipient("invalid\0recipient")
)
.encrypt_binary(b"data"),
Err(XmlEncError::InvalidEncryptionConfig(_))
));
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(
EncryptionRecipient::aes_key_wrap([0_u8; 16], KeyWrapAlgorithm::AesKw128)
.key_name("invalid\0name")
)
.encrypt_binary(b"data"),
Err(XmlEncError::InvalidEncryptionConfig(_))
));
}
#[test]
fn encrypted_data_id_must_be_an_xml_ncname() {
for invalid in ["bad id", "1leading", "qualified:name", ""] {
assert!(matches!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.id(invalid)
.encrypt_binary(b"data"),
Err(XmlEncError::InvalidEncryptionConfig(_))
));
}
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key([0_u8; 16])
.id("Δοκιμή")
.encrypt_binary(b"data")
.expect("Unicode XML NCNames must remain valid identifiers");
}
#[test]
fn rejects_malformed_custom_provider_ciphertext_before_serialization() {
for (algorithm, ciphertext) in [
(DataEncryptionAlgorithm::Aes128Gcm, vec![0_u8; 27]),
(DataEncryptionAlgorithm::Aes128Cbc, vec![0_u8; 31]),
(DataEncryptionAlgorithm::Aes256Cbc, vec![0_u8; 33]),
] {
let error = EncryptedDataBuilder::new(algorithm)
.provider(Arc::new(OverridingOutputProvider {
ciphertext: Some(ciphertext),
wrapped_key: None,
transported_key: None,
transport_calls: AtomicUsize::new(0),
}))
.direct_key(vec![0_u8; algorithm.key_len()])
.encrypt_binary(b"data")
.expect_err("malformed provider output must fail before XML serialization");
assert!(matches!(
error,
XmlEncError::DataTooShort { .. } | XmlEncError::InvalidCbcCiphertextLength { .. }
));
}
}
#[test]
fn rejects_overlong_custom_provider_ciphertext_before_serialization() {
for (algorithm, expected, actual) in [
(DataEncryptionAlgorithm::Aes128Gcm, 32, 33),
(DataEncryptionAlgorithm::Aes128Cbc, 32, 48),
] {
let error = EncryptedDataBuilder::new(algorithm)
.provider(Arc::new(OverridingOutputProvider {
ciphertext: Some(vec![0_u8; actual]),
wrapped_key: None,
transported_key: None,
transport_calls: AtomicUsize::new(0),
}))
.direct_key(vec![0_u8; algorithm.key_len()])
.encrypt_binary(b"data")
.expect_err("overlong provider output must fail before XML serialization");
assert!(matches!(
error,
XmlEncError::Provider(crate::provider::ProviderError::InvalidOutputSize {
operation: crate::provider::ProviderOperation::Encrypt,
expected: observed_expected,
actual: observed_actual,
}) if observed_expected == expected && observed_actual == actual
));
}
}
#[test]
fn rejects_malformed_custom_provider_wrapped_keys_before_serialization() {
for wrapped_key in [vec![], vec![0_u8; 23], vec![0_u8; 25]] {
let result = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.provider(Arc::new(OverridingOutputProvider {
ciphertext: None,
wrapped_key: Some(wrapped_key),
transported_key: None,
transport_calls: AtomicUsize::new(0),
}))
.recipient_aes_kw([0_u8; 16], KeyWrapAlgorithm::AesKw128)
.encrypt_binary(b"data");
assert!(matches!(
result,
Err(XmlEncError::InvalidWrappedKeyLength { expected: 24, actual })
if actual != 24
));
}
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.provider(Arc::new(OverridingOutputProvider {
ciphertext: None,
wrapped_key: Some(vec![0_u8; 24]),
transported_key: None,
transport_calls: AtomicUsize::new(0),
}))
.recipient_aes_kw([0_u8; 16], KeyWrapAlgorithm::AesKw128)
.encrypt_binary(b"data")
.expect("exact RFC 3394 wrapped-key length must remain accepted");
}
#[test]
fn rejects_malformed_custom_provider_rsa_transport_before_serialization() {
let private_key = RsaPrivateKey::new(&mut UnwrapErr(SysRng), 2048)
.expect("RSA key generation should succeed");
let public_key = RsaPublicKey::from(&private_key);
for transported_key in [vec![], vec![0_u8; 255], vec![0_u8; 257]] {
let result = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.provider(Arc::new(OverridingOutputProvider {
ciphertext: None,
wrapped_key: None,
transported_key: Some(transported_key),
transport_calls: AtomicUsize::new(0),
}))
.recipient_rsa_oaep(public_key.clone())
.encrypt_binary(b"data");
assert!(matches!(
result,
Err(XmlEncError::InvalidWrappedKeyLength {
expected: 256,
actual,
}) if actual != 256
));
}
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.provider(Arc::new(OverridingOutputProvider {
ciphertext: None,
wrapped_key: None,
transported_key: Some(vec![0_u8; 256]),
transport_calls: AtomicUsize::new(0),
}))
.recipient_rsa_oaep(public_key)
.encrypt_binary(b"data")
.expect("modulus-sized RSA transport output must remain accepted");
}
#[test]
fn custom_provider_encrypts_with_an_opaque_transport_key() {
let provider = Arc::new(OverridingOutputProvider {
ciphertext: None,
wrapped_key: None,
transported_key: Some(vec![0x5a; 256]),
transport_calls: AtomicUsize::new(0),
});
let key = Arc::new(OpaqueTransportKey {
modulus: vec![0x80; 256],
exponent: vec![0x01, 0x00, 0x01],
});
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.provider(provider.clone())
.recipient_key_transport(key)
.encrypt_binary(b"opaque provider key")
.expect("custom provider must accept its opaque transport key");
assert_eq!(provider.transport_calls.load(Ordering::Relaxed), 1);
assert!(encrypted.encrypted_data_xml.contains("rsa-oaep"));
}
#[test]
fn opaque_transport_preflight_rejects_weak_rsa_metadata() {
let key = OpaqueTransportKey {
modulus: vec![0x80; 128],
exponent: vec![0x01, 0x00, 0x01],
};
assert!(matches!(
validate_key_transport_recipient(&key, &crate::policy::EncryptionPolicy::default()),
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::KeySize { .. }
))
));
}
#[test]
fn encryption_policy_rejects_weak_rsa_recipient_before_provider_dispatch() {
let private_key = RsaPrivateKey::new(&mut UnwrapErr(SysRng), 1024)
.expect("test RSA key generation should succeed");
let provider = Arc::new(OverridingOutputProvider {
ciphertext: None,
wrapped_key: None,
transported_key: Some(vec![0_u8; 128]),
transport_calls: AtomicUsize::new(0),
});
let result = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.provider(provider.clone())
.recipient_rsa_oaep(RsaPublicKey::from(&private_key))
.encrypt_binary(b"data");
assert!(matches!(
result,
Err(XmlEncError::Policy(
crate::policy::PolicyViolation::KeySize {
operation: "encryption",
minimum_bits: 2048,
maximum_bits: 8192,
actual_bits: 1024,
..
}
))
));
assert_eq!(provider.transport_calls.load(Ordering::Relaxed), 0);
}
#[test]
fn debug_output_redacts_symmetric_key_material() {
let direct_key = b"direct-key-secret".to_vec();
let kek = b"key-wrap-secret!".to_vec();
let builder = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key(direct_key.clone());
let recipient = EncryptionRecipient::aes_key_wrap(kek.clone(), KeyWrapAlgorithm::AesKw128);
let builder_debug = format!("{builder:?}");
let recipient_debug = format!("{recipient:?}");
assert!(builder_debug.contains("[REDACTED]"));
assert!(recipient_debug.contains("[REDACTED]"));
assert!(!builder_debug.contains(&format!("{direct_key:?}")));
assert!(!recipient_debug.contains(&format!("{kek:?}")));
}
}