Skip to main content

xml_sec/xmlenc/
encrypt.rs

1//! XMLEnc content encryption, key wrapping, and XML generation.
2
3use crate::xml_input as xml_sec_xml_input;
4use std::{fmt, sync::Arc};
5
6use crate::xml::dom::{Document, Node};
7use base64::{Engine as _, engine::general_purpose::STANDARD};
8use rsa::RsaPublicKey;
9use xml_sec_xml_input::lexical::Writer;
10
11use crate::document::{
12    DocumentParseSettings, XmlDocument, XmlDocumentError, XmlParseWorkBudget,
13    parse_borrowed_with_settings_and_budget,
14};
15use crate::operation::{
16    OperationExecutionContext, OperationNodeId, OperationNodeKind, OperationPlanError,
17    OperationResourceIdentity, OperationStage,
18};
19use crate::xml::{is_xml_1_0_character, is_xml_ncname};
20
21use super::types::{XMLDSIG_NS, XMLENC_NS, XMLENC11_NS};
22use super::{
23    DataEncryptionAlgorithm, DocumentEncryptionOptions, EncryptedDataType, EncryptionRecipient,
24    EncryptionResult, KeyWrapAlgorithm, ReplacementMode, RsaOaepParameters, XmlEncError,
25    has_single_element_with_boundary_trivia, map_document_error,
26};
27
28const XML_WHITESPACE: &[char] = &[' ', '\t', '\n', '\r'];
29
30/// Validate an RSA recipient key against the compiled encryption policy.
31///
32/// Key registries can use this preflight before selecting a candidate, ensuring
33/// ordered searches skip keys that the encryption operation would reject.
34pub fn validate_rsa_recipient_key(
35    key: &RsaPublicKey,
36    policy: &crate::policy::EncryptionPolicy,
37) -> Result<(), XmlEncError> {
38    validate_key_transport_recipient(key, policy)
39}
40
41/// Validate an opaque RSA transport key against the compiled encryption policy.
42///
43/// The handle's public metadata must identify the exact key used by the
44/// selected provider. This preflight lets provider-owned key registries apply
45/// the same policy as [`EncryptedDataBuilder`] before selecting a candidate.
46pub fn validate_key_transport_recipient(
47    key: &dyn crate::provider::KeyTransportKey,
48    policy: &crate::policy::EncryptionPolicy,
49) -> Result<(), XmlEncError> {
50    let modulus = key.rsa_modulus();
51    let exponent = key.rsa_exponent();
52    policy
53        .rsa_keys
54        .validate_components("encryption", &modulus, &exponent)?;
55    Ok(())
56}
57
58/// Builder for complete `EncryptedData` fragments and document replacement.
59#[derive(Clone)]
60pub struct EncryptedDataBuilder {
61    algorithm: DataEncryptionAlgorithm,
62    encrypted_type: EncryptedDataType,
63    id: Option<String>,
64    direct_key: Option<Vec<u8>>,
65    direct_key_name: Option<String>,
66    recipients: Vec<EncryptionRecipient>,
67    policy: crate::policy::EncryptionPolicy,
68    provider: Arc<dyn crate::provider::CryptoProvider>,
69    xml_backend: crate::XmlBackend,
70}
71
72struct GeneratedEncryption {
73    result: EncryptionResult,
74    xml_nodes: usize,
75    mutation: Option<OperationNodeId>,
76}
77
78struct EncryptionPlanNodes {
79    document: OperationNodeId,
80    keys: Vec<OperationNodeId>,
81    crypto: OperationNodeId,
82    evidence: OperationNodeId,
83    mutation: Option<OperationNodeId>,
84}
85
86struct EncryptionOperationBudgets {
87    xml_parse: XmlParseWorkBudget,
88}
89
90impl EncryptionOperationBudgets {
91    fn from_policy(policy: &crate::policy::EncryptionPolicy) -> Self {
92        Self {
93            xml_parse: XmlParseWorkBudget::from_resources(&policy.resources),
94        }
95    }
96}
97
98fn compile_encryption_plan(
99    operation: &mut OperationExecutionContext<
100        crate::policy::EncryptionPolicy,
101        EncryptionOperationBudgets,
102    >,
103    key_count: usize,
104    mutation: bool,
105    input_resource: OperationResourceIdentity,
106) -> Result<EncryptionPlanNodes, XmlEncError> {
107    let document = operation.add_node(
108        OperationNodeKind::Document,
109        OperationStage::Parse,
110        Some(input_resource),
111    );
112    let mut keys = Vec::with_capacity(key_count.max(1));
113    for index in 0..key_count.max(1) {
114        let key = operation.add_node(
115            OperationNodeKind::Key { index },
116            OperationStage::Resolve,
117            None,
118        );
119        operation
120            .add_dependency(key, document)
121            .map_err(map_encryption_plan_error)?;
122        keys.push(key);
123    }
124    let crypto = operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
125    for key in &keys {
126        operation
127            .add_dependency(crypto, *key)
128            .map_err(map_encryption_plan_error)?;
129    }
130    let evidence = operation.add_node(OperationNodeKind::Evidence, OperationStage::Evidence, None);
131    operation
132        .add_dependency(evidence, crypto)
133        .map_err(map_encryption_plan_error)?;
134    let mutation = mutation.then(|| {
135        let node = operation.add_node(OperationNodeKind::Mutation, OperationStage::Mutation, None);
136        operation
137            .add_dependency(node, evidence)
138            .expect("evidence-to-mutation stage order is fixed");
139        node
140    });
141    operation.compile().map_err(map_encryption_plan_error)?;
142    Ok(EncryptionPlanNodes {
143        document,
144        keys,
145        crypto,
146        evidence,
147        mutation,
148    })
149}
150
151fn map_encryption_plan_error(error: OperationPlanError) -> XmlEncError {
152    XmlEncError::from(error)
153}
154
155fn require_encryption_mutation(
156    mutation: Option<OperationNodeId>,
157) -> Result<OperationNodeId, XmlEncError> {
158    mutation
159        .ok_or_else(|| XmlEncError::OperationPlan("encryption mutation node is unavailable".into()))
160}
161
162impl fmt::Debug for EncryptedDataBuilder {
163    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164        formatter
165            .debug_struct("EncryptedDataBuilder")
166            .field("algorithm", &self.algorithm)
167            .field("encrypted_type", &self.encrypted_type)
168            .field("id", &self.id)
169            .field(
170                "direct_key",
171                &self.direct_key.as_ref().map(|_| "[REDACTED]"),
172            )
173            .field("direct_key_name", &self.direct_key_name)
174            .field("recipients", &self.recipients)
175            .field("policy", &self.policy)
176            .field("provider", &self.provider.name())
177            .finish()
178    }
179}
180
181impl EncryptedDataBuilder {
182    /// Create a builder for a content-encryption algorithm.
183    pub fn new(algorithm: DataEncryptionAlgorithm) -> Self {
184        Self {
185            algorithm,
186            encrypted_type: EncryptedDataType::Element,
187            id: None,
188            direct_key: None,
189            direct_key_name: None,
190            recipients: Vec::new(),
191            policy: crate::policy::EncryptionPolicy::default(),
192            provider: Arc::new(crate::provider::RustCryptoProvider),
193            xml_backend: crate::XmlBackend::default(),
194        }
195    }
196
197    /// Replace the complete immutable encryption policy snapshot.
198    pub fn policy(mut self, policy: crate::policy::EncryptionPolicy) -> Self {
199        self.policy = policy;
200        self
201    }
202
203    /// Select the cryptographic provider for this operation context.
204    pub fn provider(mut self, provider: Arc<dyn crate::provider::CryptoProvider>) -> Self {
205        self.provider = provider;
206        self
207    }
208
209    /// Select the compiled XML parser backend for encryption document work.
210    pub fn xml_backend(mut self, backend: crate::XmlBackend) -> Self {
211        self.xml_backend = backend;
212        self
213    }
214
215    fn document_parse_settings(&self) -> DocumentParseSettings {
216        DocumentParseSettings::from_policy(&self.policy.xml, &self.policy.resources)
217            .with_backend(self.xml_backend)
218    }
219
220    /// Set whether XML encryption covers one element or its child content.
221    pub fn encryption_type(mut self, encrypted_type: EncryptedDataType) -> Self {
222        self.encrypted_type = encrypted_type;
223        self
224    }
225
226    /// Set the generated `EncryptedData` identifier.
227    pub fn id(mut self, id: impl Into<String>) -> Self {
228        self.id = Some(id.into());
229        self
230    }
231
232    /// Use a caller-managed content key instead of generating and wrapping one.
233    pub fn direct_key(mut self, key: impl Into<Vec<u8>>) -> Self {
234        self.direct_key = Some(key.into());
235        self
236    }
237
238    /// Emit a direct `KeyName` hint for a caller-managed content key.
239    pub fn direct_key_name(mut self, key_name: impl Into<String>) -> Self {
240        self.direct_key_name = Some(key_name.into());
241        self
242    }
243
244    /// Add one independently wrapped recipient of the generated content key.
245    pub fn add_recipient(mut self, recipient: EncryptionRecipient) -> Self {
246        self.recipients.push(recipient);
247        self
248    }
249
250    /// Add an RSA-OAEP recipient using secure XMLEnc 1.1 defaults.
251    pub fn recipient_rsa_oaep(self, public_key: RsaPublicKey) -> Self {
252        self.add_recipient(EncryptionRecipient::rsa_oaep(public_key))
253    }
254
255    /// Add an RSA-OAEP recipient backed by an opaque provider key handle.
256    pub fn recipient_key_transport(
257        self,
258        public_key: Arc<dyn crate::provider::KeyTransportKey>,
259    ) -> Self {
260        self.add_recipient(EncryptionRecipient::provider_key_transport(public_key))
261    }
262
263    /// Add an AES Key Wrap recipient.
264    pub fn recipient_aes_kw(self, kek: impl Into<Vec<u8>>, algorithm: KeyWrapAlgorithm) -> Self {
265        self.add_recipient(EncryptionRecipient::aes_key_wrap(kek, algorithm))
266    }
267
268    /// Encrypt one complete XML element or an XML content fragment.
269    pub fn encrypt_xml(&self, xml: &str) -> Result<EncryptionResult, XmlEncError> {
270        self.policy.validate()?;
271        self.validate_plaintext_len(xml.len())?;
272        let mut operation = OperationExecutionContext::new(
273            self.policy.clone(),
274            EncryptionOperationBudgets::from_policy(&self.policy),
275            None,
276        );
277        validate_xml_plaintext(
278            xml,
279            &self.encrypted_type,
280            operation.policy(),
281            &operation.budgets().xml_parse,
282            self.xml_backend,
283        )?;
284        let generated = self.encrypt_payload_with_operation(
285            xml.as_bytes(),
286            Some(self.encrypted_type.clone()),
287            &mut operation,
288            false,
289        )?;
290        validate_standalone_encrypted_data_nodes(
291            generated.xml_nodes,
292            operation.policy().resources.effective_xml_nodes() as usize,
293        )?;
294        Ok(generated.result)
295    }
296
297    /// Encrypt opaque bytes, preserving a configured non-XML `Type` hint.
298    ///
299    /// Element and Content are XML replacement semantics and are omitted from
300    /// binary output. Any other URI remains application metadata.
301    pub fn encrypt_binary(&self, data: &[u8]) -> Result<EncryptionResult, XmlEncError> {
302        self.policy.validate()?;
303        let mut operation = OperationExecutionContext::new(
304            self.policy.clone(),
305            EncryptionOperationBudgets::from_policy(&self.policy),
306            None,
307        );
308        let encrypted_type = match &self.encrypted_type {
309            EncryptedDataType::Other(uri) => Some(EncryptedDataType::Other(uri.clone())),
310            EncryptedDataType::Element | EncryptedDataType::Content => None,
311        };
312        let generated =
313            self.encrypt_payload_with_operation(data, encrypted_type, &mut operation, false)?;
314        validate_standalone_encrypted_data_nodes(
315            generated.xml_nodes,
316            operation.policy().resources.effective_xml_nodes() as usize,
317        )?;
318        Ok(generated.result)
319    }
320
321    /// Encrypt and replace the document root or one element selected by XML ID.
322    pub fn encrypt_document(
323        &self,
324        xml: &str,
325        options: DocumentEncryptionOptions<'_>,
326    ) -> Result<String, XmlEncError> {
327        self.policy.validate()?;
328        self.validate_document_len(xml.len())?;
329        let budgets = EncryptionOperationBudgets::from_policy(&self.policy);
330        let settings = self.document_parse_settings();
331        let mut document = XmlDocument::parse_with_settings_and_budget(
332            xml.to_owned(),
333            settings,
334            &budgets.xml_parse,
335        )
336        .map_err(|error| map_document_error(error, settings))?;
337        self.encrypt_owned_document_with_budgets(&mut document, options, budgets)?;
338        Ok(document.into_xml())
339    }
340
341    /// Encrypt and replace a node in a reusable owned XML document.
342    ///
343    /// Successful mutation advances the document generation and invalidates
344    /// identities captured before this call.
345    pub fn encrypt_owned_document(
346        &self,
347        document: &mut XmlDocument,
348        options: DocumentEncryptionOptions<'_>,
349    ) -> Result<(), XmlEncError> {
350        self.encrypt_owned_document_with_budgets(
351            document,
352            options,
353            EncryptionOperationBudgets::from_policy(&self.policy),
354        )
355    }
356
357    fn encrypt_owned_document_with_budgets(
358        &self,
359        document: &mut XmlDocument,
360        options: DocumentEncryptionOptions<'_>,
361        budgets: EncryptionOperationBudgets,
362    ) -> Result<(), XmlEncError> {
363        self.policy.validate()?;
364        document.validate_operation_policy(&self.policy.xml, &self.policy.resources)?;
365        let document_nodes = document.with_view(|view| view.node_count());
366        let (target, source, content_boundaries, selected_nodes) = document.with_view(|view| {
367            let selected = select_encryption_target(view.document(), options.element_id)?;
368            let source = &view.xml()[selected.range()];
369            let content_boundaries = match self.encrypted_type {
370                EncryptedDataType::Element => {
371                    self.validate_plaintext_len(source.len())?;
372                    None
373                }
374                EncryptedDataType::Content => {
375                    let boundaries = element_content_boundaries(source)?;
376                    self.validate_plaintext_len(boundaries.content.len())?;
377                    Some(boundaries)
378                }
379                EncryptedDataType::Other(_) => None,
380            };
381            Ok::<_, XmlEncError>((
382                view.node_identity(selected),
383                source.to_owned(),
384                content_boundaries,
385                selected.descendants().count(),
386            ))
387        })?;
388        let mut operation = OperationExecutionContext::new(
389            self.policy.clone(),
390            budgets,
391            Some((document.identity(), document.generation())),
392        );
393        document
394            .with_view(|view| operation.validate_document_view(view))
395            .map_err(map_encryption_plan_error)?;
396
397        match self.encrypted_type {
398            EncryptedDataType::Element => {
399                let generated = self.encrypt_payload_with_operation(
400                    source.as_bytes(),
401                    Some(EncryptedDataType::Element),
402                    &mut operation,
403                    true,
404                )?;
405                let result = generated.result;
406                validate_replacement_document_len(
407                    document.as_xml().len(),
408                    source.len(),
409                    result.encrypted_data_xml.len(),
410                    operation.policy().resources.max_xml_document_bytes,
411                )?;
412                validate_replacement_node_counts(
413                    document_nodes,
414                    selected_nodes,
415                    generated.xml_nodes,
416                    ReplacementMode::ReplaceElement,
417                    operation.policy().resources.effective_xml_nodes() as usize,
418                )?;
419                let settings = self.document_parse_settings();
420                let mutation = require_encryption_mutation(generated.mutation)?;
421                operation.run_document_transition(mutation, document, |document, budgets| {
422                    document
423                        .replace_element_with_budget(
424                            target,
425                            &result.encrypted_data_xml,
426                            settings,
427                            &budgets.xml_parse,
428                        )
429                        .map_err(|error| map_document_error(error, settings))
430                })?;
431                Ok(())
432            }
433            EncryptedDataType::Content => {
434                let boundaries = content_boundaries.ok_or_else(|| {
435                    XmlEncError::InvalidStructure(
436                        "content encryption target boundaries are unavailable".into(),
437                    )
438                })?;
439                let plaintext = &source[boundaries.content.clone()];
440                let generated = self.encrypt_payload_with_operation(
441                    plaintext.as_bytes(),
442                    Some(EncryptedDataType::Content),
443                    &mut operation,
444                    true,
445                )?;
446                let result = generated.result;
447                let (removed, inserted) = if boundaries.self_closing {
448                    let slash = source[..boundaries.start_tag_end]
449                        .rfind('/')
450                        .ok_or_else(|| {
451                            XmlEncError::InvalidStructure("self-closing tag has no slash".into())
452                        })?;
453                    (
454                        source.len(),
455                        slash
456                            .saturating_add(result.encrypted_data_xml.len())
457                            .saturating_add(boundaries.qualified_name.len())
458                            .saturating_add(4),
459                    )
460                } else {
461                    (boundaries.content.len(), result.encrypted_data_xml.len())
462                };
463                validate_replacement_document_len(
464                    document.as_xml().len(),
465                    removed,
466                    inserted,
467                    operation.policy().resources.max_xml_document_bytes,
468                )?;
469                validate_replacement_node_counts(
470                    document_nodes,
471                    selected_nodes,
472                    generated.xml_nodes,
473                    ReplacementMode::ReplaceContent,
474                    operation.policy().resources.effective_xml_nodes() as usize,
475                )?;
476                let settings = self.document_parse_settings();
477                let mutation = require_encryption_mutation(generated.mutation)?;
478                operation.run_document_transition(mutation, document, |document, budgets| {
479                    document
480                        .replace_content_with_budget(
481                            target,
482                            &result.encrypted_data_xml,
483                            settings,
484                            &budgets.xml_parse,
485                        )
486                        .map_err(|error| map_document_error(error, settings))
487                })?;
488                Ok(())
489            }
490            EncryptedDataType::Other(_) => Err(XmlEncError::InvalidEncryptionConfig(
491                "document encryption requires Element or Content Type".into(),
492            )),
493        }
494    }
495
496    fn encrypt_payload_with_operation(
497        &self,
498        plaintext: &[u8],
499        encrypted_type: Option<EncryptedDataType>,
500        operation: &mut OperationExecutionContext<
501            crate::policy::EncryptionPolicy,
502            EncryptionOperationBudgets,
503        >,
504        mutates_document: bool,
505    ) -> Result<GeneratedEncryption, XmlEncError> {
506        // Bound caller-controlled inputs before hashing or allocating one plan
507        // node per recipient. The compiled graph then gates all accepted work.
508        validate_plaintext_len(
509            plaintext.len(),
510            operation.policy().resources.max_encryption_plaintext_bytes,
511        )?;
512        self.validate_configuration_with_policy(operation.policy())?;
513        let input_resource = OperationResourceIdentity::external("encryption-input", plaintext);
514        let plan = compile_encryption_plan(
515            operation,
516            self.recipients.len() + usize::from(self.direct_key.is_some()),
517            mutates_document,
518            input_resource.clone(),
519        )?;
520        // `plaintext` is immutably borrowed for this call, so the fingerprint
521        // computed after preflight remains the exact observed input identity.
522        operation.run_with_resource(plan.document, &input_resource, || Ok::<_, XmlEncError>(()))?;
523
524        let (content_key, encrypted_keys) = operation.run_batch(&plan.keys, || {
525            let content_key = if let Some(key) = &self.direct_key {
526                validate_content_key(self.algorithm, key)?;
527                key.clone()
528            } else {
529                let key = random_bytes(self.provider.as_ref(), self.algorithm.key_len())?;
530                #[cfg(feature = "legacy-algorithms")]
531                let mut key = key;
532                #[cfg(feature = "legacy-algorithms")]
533                if self.algorithm == DataEncryptionAlgorithm::TripleDesCbc {
534                    // RFC 3217 §3.1: generated DES CEKs use odd octet parity.
535                    // Normalize here, not in wrapping, which also accepts AES keys.
536                    // https://www.rfc-editor.org/rfc/rfc3217#section-3.1
537                    for octet in &mut key {
538                        *octet = (*octet & 0xfe) | (((*octet & 0xfe).count_ones() as u8 & 1) ^ 1);
539                    }
540                }
541                key
542            };
543            let encrypted_keys = self
544                .recipients
545                .iter()
546                .map(|recipient| wrap_content_key(self.provider.as_ref(), recipient, &content_key))
547                .collect::<Result<Vec<_>, _>>()?;
548            Ok::<_, XmlEncError>((content_key, encrypted_keys))
549        })?;
550        let ciphertext = operation.run(plan.crypto, || {
551            encrypt_content(
552                self.provider.as_ref(),
553                self.algorithm,
554                &content_key,
555                plaintext,
556            )
557        })?;
558        let (encrypted_data_xml, xml_nodes) = operation.run(plan.evidence, || {
559            let encrypted_data_xml = render_encrypted_data(
560                self.algorithm,
561                encrypted_type.as_ref(),
562                self.id.as_deref(),
563                self.direct_key_name.as_deref(),
564                &encrypted_keys,
565                &ciphertext,
566            )?;
567            validate_document_len(
568                encrypted_data_xml.len(),
569                operation.policy().resources.max_xml_document_bytes,
570            )?;
571            let xml_nodes = count_generated_encrypted_data_nodes(
572                &encrypted_data_xml,
573                operation.policy(),
574                &operation.budgets().xml_parse,
575                self.xml_backend,
576            )?;
577            Ok::<_, XmlEncError>((encrypted_data_xml, xml_nodes))
578        })?;
579        let replacement = match encrypted_type {
580            Some(EncryptedDataType::Content) => ReplacementMode::ReplaceContent,
581            Some(EncryptedDataType::Element | EncryptedDataType::Other(_)) | None => {
582                ReplacementMode::ReplaceElement
583            }
584        };
585        Ok(GeneratedEncryption {
586            result: EncryptionResult {
587                encrypted_data_xml,
588                replacement,
589            },
590            xml_nodes,
591            mutation: plan.mutation,
592        })
593    }
594
595    fn validate_configuration_with_policy(
596        &self,
597        policy: &crate::policy::EncryptionPolicy,
598    ) -> Result<(), XmlEncError> {
599        policy.validate()?;
600        let metadata_limit = policy.resources.max_encryption_metadata_bytes;
601        if let EncryptedDataType::Other(uri) = &self.encrypted_type {
602            validate_metadata("EncryptedData Type", Some(uri), metadata_limit)?;
603        }
604        crate::policy::check_content_algorithm(
605            policy.data_algorithms.as_ref(),
606            self.algorithm,
607            "encryption",
608        )?;
609        if self.recipients.len() > policy.resources.max_encryption_recipients {
610            return Err(crate::policy::PolicyViolation::ResourceLimit {
611                resource: crate::policy::resource_name::ENCRYPTION_RECIPIENTS,
612                maximum: policy.resources.max_encryption_recipients,
613                actual: self.recipients.len(),
614            }
615            .into());
616        }
617        let key_candidates = self.recipients.len() + usize::from(self.direct_key.is_some());
618        policy.resources.validate_key_candidates(key_candidates)?;
619        validate_metadata("EncryptedData Id", self.id.as_deref(), metadata_limit)?;
620        if self.id.as_deref().is_some_and(|id| !is_xml_ncname(id)) {
621            return Err(XmlEncError::InvalidEncryptionConfig(
622                "EncryptedData Id must be an XML NCName".into(),
623            ));
624        }
625        validate_key_name(
626            "direct KeyName",
627            self.direct_key_name.as_deref(),
628            metadata_limit,
629        )?;
630        for recipient in &self.recipients {
631            match recipient {
632                #[cfg(feature = "legacy-algorithms")]
633                EncryptionRecipient::RsaPkcs1v15 {
634                    public_key,
635                    recipient,
636                    key_name,
637                } => {
638                    let algorithm = super::KeyTransportAlgorithm::RsaPkcs1v15;
639                    if !policy
640                        .key_transport_algorithms
641                        .as_ref()
642                        .is_some_and(|allowed| allowed.contains(&algorithm))
643                    {
644                        return Err(crate::policy::PolicyViolation::Algorithm {
645                            operation: "encryption",
646                            algorithm: algorithm.uri().into(),
647                        }
648                        .into());
649                    }
650                    validate_key_transport_recipient(public_key.as_ref(), policy)?;
651                    validate_metadata(
652                        "EncryptedKey Recipient",
653                        recipient.as_deref(),
654                        metadata_limit,
655                    )?;
656                    validate_key_name("EncryptedKey KeyName", key_name.as_deref(), metadata_limit)?;
657                }
658                EncryptionRecipient::RsaOaep {
659                    public_key,
660                    parameters,
661                    recipient,
662                    key_name,
663                } => {
664                    // XMLEnc 1.1 §5.5.1 has no OAEP parameters. Its algorithm
665                    // must use the separate typed recipient, never this OAEP path.
666                    // https://www.w3.org/TR/xmlenc-core1/#sec-RSA-v1.5
667                    if parameters.algorithm.requires_explicit_permission() {
668                        return Err(XmlEncError::InvalidEncryptionConfig(
669                            "RSA-OAEP recipient requires an OAEP algorithm".into(),
670                        ));
671                    }
672                    validate_key_transport_recipient(public_key.as_ref(), policy)?;
673                    if policy
674                        .key_transport_algorithms
675                        .as_ref()
676                        .is_some_and(|allowed| !allowed.contains(&parameters.algorithm))
677                    {
678                        return Err(crate::policy::PolicyViolation::Algorithm {
679                            operation: "encryption",
680                            algorithm: parameters.algorithm.uri().to_string(),
681                        }
682                        .into());
683                    }
684                    if let Some(allowed) = &policy.oaep_digests {
685                        let rejected_uri = if !allowed.contains(&parameters.digest) {
686                            Some(parameters.digest.uri())
687                        } else if !allowed.contains(&parameters.mgf_digest) {
688                            Some(parameters.mgf_digest.mgf_uri())
689                        } else {
690                            None
691                        };
692                        if let Some(algorithm) = rejected_uri {
693                            return Err(crate::policy::PolicyViolation::Algorithm {
694                                operation: "encryption",
695                                algorithm: algorithm.to_owned(),
696                            }
697                            .into());
698                        }
699                    }
700                    validate_metadata(
701                        "EncryptedKey Recipient",
702                        recipient.as_deref(),
703                        metadata_limit,
704                    )?;
705                    validate_key_name("EncryptedKey KeyName", key_name.as_deref(), metadata_limit)?;
706                    validate_metadata_len(parameters.label.len(), metadata_limit)?;
707                }
708                EncryptionRecipient::AesKeyWrap {
709                    kek,
710                    algorithm,
711                    recipient,
712                    key_name,
713                } => {
714                    if policy
715                        .key_wrap_algorithms
716                        .as_ref()
717                        .map_or(algorithm.requires_explicit_permission(), |allowed| {
718                            !allowed.contains(algorithm)
719                        })
720                    {
721                        return Err(crate::policy::PolicyViolation::Algorithm {
722                            operation: "encryption",
723                            algorithm: algorithm.uri().to_string(),
724                        }
725                        .into());
726                    }
727                    if kek.len() != algorithm.key_len() {
728                        return Err(XmlEncError::InvalidEncryptionConfig(format!(
729                            "{} requires a {}-byte key-encryption key, got {} bytes",
730                            algorithm.uri(),
731                            algorithm.key_len(),
732                            kek.len()
733                        )));
734                    }
735                    validate_metadata(
736                        "EncryptedKey Recipient",
737                        recipient.as_deref(),
738                        metadata_limit,
739                    )?;
740                    validate_key_name("EncryptedKey KeyName", key_name.as_deref(), metadata_limit)?;
741                }
742            }
743        }
744        match (self.direct_key.is_some(), self.recipients.is_empty()) {
745            (false, true) => Err(XmlEncError::InvalidEncryptionConfig(
746                "configure a direct content key or at least one wrapped recipient".into(),
747            )),
748            (true, false) => Err(XmlEncError::InvalidEncryptionConfig(
749                "a direct content key cannot be combined with wrapped recipients".into(),
750            )),
751            _ if self.direct_key_name.is_some() && self.direct_key.is_none() => {
752                Err(XmlEncError::InvalidEncryptionConfig(
753                    "direct KeyName requires a direct content key".into(),
754                ))
755            }
756            _ => Ok(()),
757        }
758    }
759
760    fn validate_plaintext_len(&self, actual: usize) -> Result<(), XmlEncError> {
761        validate_plaintext_len(actual, self.policy.resources.max_encryption_plaintext_bytes)
762    }
763
764    fn validate_document_len(&self, actual: usize) -> Result<(), XmlEncError> {
765        validate_document_len(actual, self.policy.resources.max_xml_document_bytes)
766    }
767}
768
769fn validate_metadata(
770    field: &'static str,
771    value: Option<&str>,
772    maximum: usize,
773) -> Result<(), XmlEncError> {
774    if value.is_some_and(|value| !value.chars().all(is_xml_1_0_character)) {
775        return Err(XmlEncError::InvalidEncryptionConfig(format!(
776            "{field} contains a character forbidden by XML 1.0"
777        )));
778    }
779    validate_metadata_len(value.map_or(0, str::len), maximum)
780}
781
782fn validate_key_name(
783    field: &'static str,
784    value: Option<&str>,
785    maximum: usize,
786) -> Result<(), XmlEncError> {
787    if value.is_some_and(str::is_empty) {
788        return Err(XmlEncError::InvalidEncryptionConfig(format!(
789            "{field} must not be empty"
790        )));
791    }
792    validate_metadata(field, value, maximum)
793}
794
795fn validate_metadata_len(actual: usize, maximum: usize) -> Result<(), XmlEncError> {
796    if actual <= maximum {
797        Ok(())
798    } else {
799        Err(crate::policy::PolicyViolation::ResourceLimit {
800            resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
801            maximum,
802            actual,
803        }
804        .into())
805    }
806}
807
808#[derive(Debug)]
809struct WrappedKey {
810    algorithm_uri: &'static str,
811    oaep: Option<RsaOaepParameters>,
812    recipient: Option<String>,
813    key_name: Option<String>,
814    ciphertext: Vec<u8>,
815}
816
817#[derive(Debug)]
818struct ContentBoundaries {
819    content: std::ops::Range<usize>,
820    self_closing: bool,
821    qualified_name: String,
822    start_tag_end: usize,
823}
824
825fn validate_plaintext_len(actual: usize, maximum: usize) -> Result<(), XmlEncError> {
826    if actual <= maximum {
827        Ok(())
828    } else {
829        Err(crate::policy::PolicyViolation::ResourceLimit {
830            resource: crate::policy::resource_name::ENCRYPTION_PLAINTEXT_BYTES,
831            maximum,
832            actual,
833        }
834        .into())
835    }
836}
837
838fn validate_document_len(actual: usize, maximum: usize) -> Result<(), XmlEncError> {
839    if actual > maximum {
840        return Err(crate::policy::PolicyViolation::ResourceLimit {
841            resource: crate::policy::resource_name::XML_DOCUMENT,
842            maximum,
843            actual,
844        }
845        .into());
846    }
847    Ok(())
848}
849
850fn validate_replacement_document_len(
851    document_len: usize,
852    removed_len: usize,
853    inserted_len: usize,
854    maximum: usize,
855) -> Result<(), XmlEncError> {
856    let actual = document_len
857        .saturating_sub(removed_len)
858        .saturating_add(inserted_len);
859    validate_document_len(actual, maximum)
860}
861
862fn validate_replacement_node_counts(
863    document_nodes: usize,
864    selected_nodes: usize,
865    inserted_nodes: usize,
866    replacement: ReplacementMode,
867    maximum: usize,
868) -> Result<(), XmlEncError> {
869    let removed_nodes = match replacement {
870        ReplacementMode::ReplaceElement => selected_nodes,
871        ReplacementMode::ReplaceContent => selected_nodes.saturating_sub(1),
872    };
873    let actual = document_nodes
874        .saturating_sub(removed_nodes)
875        .saturating_add(inserted_nodes);
876    if actual > maximum {
877        return Err(crate::policy::PolicyViolation::ResourceLimit {
878            resource: crate::policy::resource_name::XML_NODES,
879            maximum,
880            actual,
881        }
882        .into());
883    }
884    Ok(())
885}
886
887fn count_generated_encrypted_data_nodes(
888    encrypted_data_xml: &str,
889    policy: &crate::policy::EncryptionPolicy,
890    parse_budget: &XmlParseWorkBudget,
891    backend: crate::XmlBackend,
892) -> Result<usize, XmlEncError> {
893    let maximum_nodes = policy.resources.effective_xml_nodes();
894    // The standalone parser adds one document node that is not inserted into
895    // the caller's tree. Bound the generated subtree by the active policy while
896    // admitting that transient root so exact-fit content replacement remains valid.
897    let settings = DocumentParseSettings {
898        nodes_limit: maximum_nodes.saturating_add(1),
899        ..DocumentParseSettings::from_policy(&policy.xml, &policy.resources).with_backend(backend)
900    };
901    let generated =
902        parse_borrowed_with_settings_and_budget(encrypted_data_xml, settings, Some(parse_budget))
903            .map_err(|error| match error {
904            XmlDocumentError::Parse(crate::xml::dom::ParseError::NodesLimitReached) => {
905                XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
906                    resource: crate::policy::resource_name::XML_NODES,
907                    maximum: maximum_nodes as usize,
908                    actual: maximum_nodes as usize + 1,
909                })
910            }
911            error => map_document_error(error, settings),
912        })?;
913    Ok(generated.root_element().descendants().count())
914}
915
916fn validate_standalone_encrypted_data_nodes(
917    encrypted_data_subtree_nodes: usize,
918    maximum: usize,
919) -> Result<(), XmlEncError> {
920    let actual = encrypted_data_subtree_nodes.saturating_add(1);
921    if actual > maximum {
922        return Err(crate::policy::PolicyViolation::ResourceLimit {
923            resource: crate::policy::resource_name::XML_NODES,
924            maximum,
925            actual,
926        }
927        .into());
928    }
929    Ok(())
930}
931
932fn validate_content_key(algorithm: DataEncryptionAlgorithm, key: &[u8]) -> Result<(), XmlEncError> {
933    if key.len() == algorithm.key_len() {
934        Ok(())
935    } else {
936        Err(XmlEncError::InvalidKeySize {
937            algorithm,
938            expected: algorithm.key_len(),
939            actual: key.len(),
940        })
941    }
942}
943
944fn random_bytes(
945    provider: &dyn crate::provider::CryptoProvider,
946    len: usize,
947) -> Result<Vec<u8>, XmlEncError> {
948    provider.require_capability(crate::provider::ProviderCapability::Random)?;
949    let mut bytes = vec![0_u8; len];
950    provider.fill_random(&mut bytes)?;
951    Ok(bytes)
952}
953
954fn encrypt_content(
955    provider: &dyn crate::provider::CryptoProvider,
956    algorithm: DataEncryptionAlgorithm,
957    key: &[u8],
958    plaintext: &[u8],
959) -> Result<Vec<u8>, XmlEncError> {
960    provider.require_capability(crate::provider::ProviderCapability::Encrypt(algorithm))?;
961    let ciphertext = provider.encrypt_data(algorithm, key, plaintext)?;
962    super::types::validate_ciphertext_framing(algorithm, ciphertext.len())?;
963    let expected = algorithm
964        .ciphertext_len_for_plaintext(plaintext.len())
965        .ok_or(crate::policy::PolicyViolation::ResourceLimit {
966            resource: crate::policy::resource_name::ENCRYPTION_PLAINTEXT_BYTES,
967            maximum: crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING,
968            actual: plaintext.len(),
969        })?;
970    if ciphertext.len() != expected {
971        return Err(crate::provider::ProviderError::InvalidOutputSize {
972            operation: crate::provider::ProviderOperation::Encrypt,
973            expected,
974            actual: ciphertext.len(),
975        }
976        .into());
977    }
978    Ok(ciphertext)
979}
980
981fn wrap_content_key(
982    provider: &dyn crate::provider::CryptoProvider,
983    recipient: &EncryptionRecipient,
984    content_key: &[u8],
985) -> Result<WrappedKey, XmlEncError> {
986    match recipient {
987        #[cfg(feature = "legacy-algorithms")]
988        EncryptionRecipient::RsaPkcs1v15 {
989            public_key,
990            recipient,
991            key_name,
992        } => {
993            provider.require_capability(crate::provider::ProviderCapability::Pkcs1v15Transport)?;
994            let ciphertext = provider.transport_pkcs1v15(public_key.as_ref(), content_key)?;
995            let expected = public_key.rsa_modulus().len();
996            if ciphertext.len() != expected {
997                return Err(XmlEncError::InvalidWrappedKeyLength {
998                    expected,
999                    actual: ciphertext.len(),
1000                });
1001            }
1002            Ok(WrappedKey {
1003                algorithm_uri: super::KeyTransportAlgorithm::RsaPkcs1v15.uri(),
1004                oaep: None,
1005                recipient: recipient.clone(),
1006                key_name: key_name.clone(),
1007                ciphertext,
1008            })
1009        }
1010        EncryptionRecipient::RsaOaep {
1011            public_key,
1012            parameters,
1013            recipient,
1014            key_name,
1015        } => Ok(WrappedKey {
1016            algorithm_uri: parameters.algorithm.uri(),
1017            oaep: Some(parameters.clone()),
1018            recipient: recipient.clone(),
1019            key_name: key_name.clone(),
1020            ciphertext: wrap_rsa_oaep(provider, public_key.as_ref(), parameters, content_key)?,
1021        }),
1022        EncryptionRecipient::AesKeyWrap {
1023            kek,
1024            algorithm,
1025            recipient,
1026            key_name,
1027        } => {
1028            provider
1029                .require_capability(crate::provider::ProviderCapability::KeyWrap(*algorithm))?;
1030            let wrapped = provider.wrap_key(*algorithm, kek, content_key)?;
1031            let expected = content_key.len() + algorithm.overhead();
1032            if wrapped.len() != expected {
1033                return Err(XmlEncError::InvalidWrappedKeyLength {
1034                    expected,
1035                    actual: wrapped.len(),
1036                });
1037            }
1038            Ok(WrappedKey {
1039                algorithm_uri: algorithm.uri(),
1040                oaep: None,
1041                recipient: recipient.clone(),
1042                key_name: key_name.clone(),
1043                ciphertext: wrapped,
1044            })
1045        }
1046    }
1047}
1048
1049fn wrap_rsa_oaep(
1050    provider: &dyn crate::provider::CryptoProvider,
1051    public_key: &dyn crate::provider::KeyTransportKey,
1052    parameters: &RsaOaepParameters,
1053    content_key: &[u8],
1054) -> Result<Vec<u8>, XmlEncError> {
1055    provider.require_capability(crate::provider::ProviderCapability::KeyTransport(
1056        parameters,
1057    ))?;
1058    let ciphertext = provider
1059        .transport_key(public_key, parameters, content_key)
1060        .map_err(|error| match error {
1061            crate::provider::ProviderError::Random(message) => XmlEncError::Rng(message),
1062            crate::provider::ProviderError::InvalidInput(reason) => {
1063                XmlEncError::InvalidEncryptionConfig(reason.to_string())
1064            }
1065            error => XmlEncError::RsaEncrypt(error.to_string()),
1066        })?;
1067    let expected = public_key.rsa_modulus().len();
1068    if ciphertext.len() != expected {
1069        return Err(XmlEncError::InvalidWrappedKeyLength {
1070            expected,
1071            actual: ciphertext.len(),
1072        });
1073    }
1074    Ok(ciphertext)
1075}
1076
1077fn render_encrypted_data(
1078    algorithm: DataEncryptionAlgorithm,
1079    encrypted_type: Option<&EncryptedDataType>,
1080    id: Option<&str>,
1081    direct_key_name: Option<&str>,
1082    encrypted_keys: &[WrappedKey],
1083    ciphertext: &[u8],
1084) -> Result<String, XmlEncError> {
1085    let mut writer = Writer::new(Vec::new());
1086    let mut root_attributes = vec![
1087        ("xmlns:xenc", XMLENC_NS),
1088        ("xmlns:xenc11", XMLENC11_NS),
1089        ("xmlns:ds", XMLDSIG_NS),
1090    ];
1091    if let Some(id) = id {
1092        root_attributes.push(("Id", id));
1093    }
1094    let encrypted_type_uri;
1095    if let Some(encrypted_type) = encrypted_type {
1096        encrypted_type_uri = match encrypted_type {
1097            EncryptedDataType::Element => format!("{XMLENC_NS}Element"),
1098            EncryptedDataType::Content => format!("{XMLENC_NS}Content"),
1099            EncryptedDataType::Other(uri) => uri.clone(),
1100        };
1101        root_attributes.push(("Type", encrypted_type_uri.as_str()));
1102    }
1103    write_start(&mut writer, "xenc:EncryptedData", root_attributes)?;
1104    write_empty_with_algorithm(&mut writer, "xenc:EncryptionMethod", algorithm.uri())?;
1105
1106    if direct_key_name.is_some() || !encrypted_keys.is_empty() {
1107        write_start(&mut writer, "ds:KeyInfo", [])?;
1108        if let Some(key_name) = direct_key_name {
1109            write_text_element(&mut writer, "ds:KeyName", key_name)?;
1110        }
1111        for encrypted_key in encrypted_keys {
1112            write_encrypted_key(&mut writer, encrypted_key)?;
1113        }
1114        write_end(&mut writer, "ds:KeyInfo")?;
1115    }
1116
1117    write_cipher_data(&mut writer, ciphertext)?;
1118    write_end(&mut writer, "xenc:EncryptedData")?;
1119    String::from_utf8(writer.into_inner())
1120        .map_err(|error| XmlEncError::XmlSerialize(error.to_string()))
1121}
1122
1123fn write_encrypted_key(
1124    writer: &mut Writer<Vec<u8>>,
1125    encrypted_key: &WrappedKey,
1126) -> Result<(), XmlEncError> {
1127    write_start(
1128        writer,
1129        "xenc:EncryptedKey",
1130        encrypted_key
1131            .recipient
1132            .as_deref()
1133            .map(|recipient| vec![("Recipient", recipient)])
1134            .unwrap_or_default(),
1135    )?;
1136
1137    if let Some(parameters) = encrypted_key.oaep.as_ref() {
1138        write_start(
1139            writer,
1140            "xenc:EncryptionMethod",
1141            [("Algorithm", encrypted_key.algorithm_uri)],
1142        )?;
1143        if !parameters.label.is_empty() {
1144            write_text_element(
1145                writer,
1146                "xenc:OAEPparams",
1147                &STANDARD.encode(&parameters.label),
1148            )?;
1149        }
1150        write_empty_with_algorithm(writer, "ds:DigestMethod", parameters.digest.uri())?;
1151        if parameters.algorithm == super::KeyTransportAlgorithm::RsaOaep11
1152            || parameters.mgf_digest != super::OaepDigestAlgorithm::Sha1
1153        {
1154            write_empty_with_algorithm(writer, "xenc11:MGF", parameters.mgf_digest.mgf_uri())?;
1155        }
1156        write_end(writer, "xenc:EncryptionMethod")?;
1157    } else {
1158        write_empty_with_algorithm(writer, "xenc:EncryptionMethod", encrypted_key.algorithm_uri)?;
1159    }
1160
1161    if let Some(key_name) = encrypted_key.key_name.as_deref() {
1162        write_start(writer, "ds:KeyInfo", [])?;
1163        write_text_element(writer, "ds:KeyName", key_name)?;
1164        write_end(writer, "ds:KeyInfo")?;
1165    }
1166    write_cipher_data(writer, &encrypted_key.ciphertext)?;
1167    write_end(writer, "xenc:EncryptedKey")
1168}
1169
1170fn write_cipher_data(writer: &mut Writer<Vec<u8>>, value: &[u8]) -> Result<(), XmlEncError> {
1171    write_start(writer, "xenc:CipherData", [])?;
1172    write_text_element(writer, "xenc:CipherValue", &STANDARD.encode(value))?;
1173    write_end(writer, "xenc:CipherData")
1174}
1175
1176fn write_empty_with_algorithm(
1177    writer: &mut Writer<Vec<u8>>,
1178    name: &str,
1179    algorithm: &str,
1180) -> Result<(), XmlEncError> {
1181    writer
1182        .empty(name, [("Algorithm", algorithm)])
1183        .map_err(|error| XmlEncError::XmlSerialize(error.to_string()))
1184}
1185
1186fn write_text_element(
1187    writer: &mut Writer<Vec<u8>>,
1188    name: &str,
1189    text: &str,
1190) -> Result<(), XmlEncError> {
1191    write_start(writer, name, [])?;
1192    writer
1193        .text(text)
1194        .map_err(|error| XmlEncError::XmlSerialize(error.to_string()))?;
1195    write_end(writer, name)
1196}
1197
1198fn write_start<'a>(
1199    writer: &mut Writer<Vec<u8>>,
1200    name: &str,
1201    attributes: impl IntoIterator<Item = (&'a str, &'a str)>,
1202) -> Result<(), XmlEncError> {
1203    writer
1204        .start(name, attributes)
1205        .map_err(|error| XmlEncError::XmlSerialize(error.to_string()))
1206}
1207
1208fn write_end(writer: &mut Writer<Vec<u8>>, name: &str) -> Result<(), XmlEncError> {
1209    writer
1210        .end(name)
1211        .map_err(|error| XmlEncError::XmlSerialize(error.to_string()))
1212}
1213
1214fn validate_xml_plaintext(
1215    xml: &str,
1216    encrypted_type: &EncryptedDataType,
1217    policy: &crate::policy::EncryptionPolicy,
1218    parse_budget: &XmlParseWorkBudget,
1219    backend: crate::XmlBackend,
1220) -> Result<(), XmlEncError> {
1221    let settings =
1222        DocumentParseSettings::from_policy(&policy.xml, &policy.resources).with_backend(backend);
1223    match encrypted_type {
1224        EncryptedDataType::Element => {
1225            let document =
1226                parse_borrowed_with_settings_and_budget(xml, settings, Some(parse_budget))
1227                    .map_err(|error| map_document_error(error, settings))?;
1228            if !has_single_element_with_boundary_trivia(document.root()) {
1229                return Err(XmlEncError::InvalidStructure(
1230                    "Element plaintext must contain exactly one element".into(),
1231                ));
1232            }
1233            Ok(())
1234        }
1235        EncryptedDataType::Content => {
1236            const WRAPPER_START: &str = "<xmlsec-content>";
1237            const WRAPPER_END: &str = "</xmlsec-content>";
1238
1239            policy.resources.validate_xml_document_len(xml.len())?;
1240            let wrapped = format!("{WRAPPER_START}{xml}{WRAPPER_END}");
1241            let wrapper_bytes = WRAPPER_START.len() + WRAPPER_END.len();
1242            // The wrapper exists only to parse an XML fragment. Its node must
1243            // not consume the caller-owned byte, node, or depth allowance.
1244            let wrapped_settings = DocumentParseSettings {
1245                nodes_limit: settings.nodes_limit.saturating_add(1),
1246                depth_limit: settings.depth_limit.saturating_add(1),
1247                max_bytes: settings.max_bytes.saturating_add(wrapper_bytes),
1248                ..settings
1249            };
1250            parse_borrowed_with_settings_and_budget(&wrapped, wrapped_settings, Some(parse_budget))
1251                .map_err(|error| match error {
1252                    XmlDocumentError::DocumentTooLarge { actual, .. } => {
1253                        XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1254                            resource: crate::policy::resource_name::XML_DOCUMENT,
1255                            maximum: settings.max_bytes,
1256                            actual: actual.saturating_sub(wrapper_bytes),
1257                        })
1258                    }
1259                    XmlDocumentError::Parse(crate::xml::dom::ParseError::NodesLimitReached) => {
1260                        XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1261                            resource: crate::policy::resource_name::XML_NODES,
1262                            maximum: settings.nodes_limit as usize,
1263                            actual: settings.nodes_limit as usize + 1,
1264                        })
1265                    }
1266                    XmlDocumentError::DocumentTooDeep { actual, .. } => {
1267                        XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1268                            resource: crate::policy::resource_name::XML_DEPTH,
1269                            maximum: settings.depth_limit,
1270                            actual: actual.saturating_sub(1),
1271                        })
1272                    }
1273                    error => map_document_error(error, wrapped_settings),
1274                })?;
1275            Ok(())
1276        }
1277        EncryptedDataType::Other(_) => Err(XmlEncError::InvalidEncryptionConfig(
1278            "encrypt_xml requires Element or Content Type".into(),
1279        )),
1280    }
1281}
1282
1283fn select_encryption_target<'a, 'input>(
1284    document: &'a Document<'input>,
1285    id: Option<&str>,
1286) -> Result<Node<'a, 'input>, XmlEncError> {
1287    let Some(id) = id else {
1288        return Ok(document.root_element());
1289    };
1290    let mut matches = document.descendants().filter(|node| {
1291        node.is_element()
1292            && ["Id", "ID", "id"]
1293                .iter()
1294                .any(|name| node.attribute(*name) == Some(id))
1295    });
1296    let selected = matches
1297        .next()
1298        .ok_or(XmlEncError::EncryptionTargetNotFound)?;
1299    if matches.next().is_some() {
1300        return Err(XmlEncError::AmbiguousEncryptionTarget);
1301    }
1302    Ok(selected)
1303}
1304
1305fn element_content_boundaries(source: &str) -> Result<ContentBoundaries, XmlEncError> {
1306    let tag_end = find_start_tag_end(source)?;
1307    let before_end = source[..tag_end].trim_end_matches(XML_WHITESPACE);
1308    let self_closing = before_end.ends_with('/');
1309    let name_end = source[1..]
1310        .find(|character: char| character.is_ascii_whitespace() || matches!(character, '/' | '>'))
1311        .map(|index| index + 1)
1312        .ok_or_else(|| XmlEncError::InvalidStructure("source element has no name".into()))?;
1313    let qualified_name = source[1..name_end].to_owned();
1314    if self_closing {
1315        return Ok(ContentBoundaries {
1316            content: tag_end..tag_end,
1317            self_closing: true,
1318            qualified_name,
1319            start_tag_end: tag_end,
1320        });
1321    }
1322    // `Node::range()` ends at this element's closing tag, so its `</` marker is
1323    // necessarily the final one even when child text or CDATA contains `</`.
1324    let closing_start = source
1325        .rfind("</")
1326        .ok_or_else(|| XmlEncError::InvalidStructure("source element has no closing tag".into()))?;
1327    Ok(ContentBoundaries {
1328        content: tag_end + 1..closing_start,
1329        self_closing: false,
1330        qualified_name,
1331        start_tag_end: tag_end,
1332    })
1333}
1334
1335fn find_start_tag_end(source: &str) -> Result<usize, XmlEncError> {
1336    let mut quote = None;
1337    for (index, character) in source.char_indices() {
1338        match (quote, character) {
1339            (Some(expected), actual) if expected == actual => quote = None,
1340            (None, '\'' | '"') => quote = Some(character),
1341            (None, '>') => return Ok(index),
1342            _ => {}
1343        }
1344    }
1345    Err(XmlEncError::InvalidStructure(
1346        "source element start tag is unterminated".into(),
1347    ))
1348}
1349
1350#[cfg(test)]
1351mod tests {
1352    use std::sync::atomic::{AtomicUsize, Ordering};
1353
1354    use std::sync::Arc;
1355
1356    use getrandom::SysRng;
1357    use getrandom::rand_core::UnwrapErr;
1358    use rsa::pkcs8::DecodePublicKey as _;
1359    use rsa::{RsaPrivateKey, RsaPublicKey};
1360
1361    use super::*;
1362    use crate::hard_limits::{
1363        ENCRYPTION_METADATA_BYTE_CEILING as MAX_ENCRYPTION_METADATA_LEN,
1364        ENCRYPTION_PLAINTEXT_BYTE_CEILING as MAX_ENCRYPTION_PLAINTEXT_LEN,
1365        ENCRYPTION_RECIPIENT_CEILING as MAX_ENCRYPTION_RECIPIENTS,
1366        XML_DOCUMENT_BYTE_CEILING as MAX_ENCRYPTION_DOCUMENT_LEN,
1367    };
1368    use crate::xmlenc::{
1369        KekDecryptor, OaepDigestAlgorithm, PrivateKeyDecryptor, SymmetricKeyDecryptor, decrypt,
1370        decrypt_document, parse_encrypted_data,
1371    };
1372
1373    #[test]
1374    fn document_encryption_requires_a_compiled_mutation_node() {
1375        assert!(matches!(
1376            require_encryption_mutation(None),
1377            Err(XmlEncError::OperationPlan(message))
1378                if message == "encryption mutation node is unavailable"
1379        ));
1380    }
1381
1382    #[derive(Debug)]
1383    struct OverridingOutputProvider {
1384        ciphertext: Option<Vec<u8>>,
1385        wrapped_key: Option<Vec<u8>>,
1386        transported_key: Option<Vec<u8>>,
1387        transport_calls: AtomicUsize,
1388    }
1389
1390    struct OpaqueTransportKey {
1391        modulus: Vec<u8>,
1392        exponent: Vec<u8>,
1393    }
1394
1395    impl crate::provider::KeyTransportKey for OpaqueTransportKey {
1396        fn rsa_modulus(&self) -> std::borrow::Cow<'_, [u8]> {
1397            std::borrow::Cow::Borrowed(&self.modulus)
1398        }
1399
1400        fn rsa_exponent(&self) -> std::borrow::Cow<'_, [u8]> {
1401            std::borrow::Cow::Borrowed(&self.exponent)
1402        }
1403
1404        fn transport_with_provider(
1405            &self,
1406            _provider: &dyn crate::provider::CryptoProvider,
1407            _parameters: &RsaOaepParameters,
1408            _plaintext: &[u8],
1409        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
1410            panic!("custom provider must own transport for its opaque key")
1411        }
1412    }
1413
1414    impl crate::provider::CryptoProvider for OverridingOutputProvider {
1415        fn name(&self) -> &'static str {
1416            "overriding-output-test"
1417        }
1418
1419        fn supports(&self, capability: crate::provider::ProviderCapability<'_>) -> bool {
1420            crate::provider::CryptoProvider::supports(
1421                &crate::provider::RustCryptoProvider,
1422                capability,
1423            )
1424        }
1425
1426        fn fill_random(&self, output: &mut [u8]) -> Result<(), crate::provider::ProviderError> {
1427            crate::provider::CryptoProvider::fill_random(
1428                &crate::provider::RustCryptoProvider,
1429                output,
1430            )
1431        }
1432
1433        fn derive_key(
1434            &self,
1435            parameters: &crate::provider::KdfParameters<'_>,
1436            secret: &[u8],
1437        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
1438            crate::provider::RustCryptoProvider.derive_key(parameters, secret)
1439        }
1440
1441        #[cfg(feature = "xmldsig")]
1442        fn digest(
1443            &self,
1444            algorithm: crate::xmldsig::DigestAlgorithm,
1445            data: &[u8],
1446        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
1447            crate::provider::CryptoProvider::digest(
1448                &crate::provider::RustCryptoProvider,
1449                algorithm,
1450                data,
1451            )
1452        }
1453
1454        #[cfg(feature = "xmldsig")]
1455        fn sign(
1456            &self,
1457            key: &dyn crate::xmldsig::SigningKey,
1458            algorithm: crate::xmldsig::SignatureAlgorithm,
1459            data: &[u8],
1460        ) -> Result<Vec<u8>, crate::xmldsig::SigningKeyError> {
1461            crate::provider::CryptoProvider::sign(
1462                &crate::provider::RustCryptoProvider,
1463                key,
1464                algorithm,
1465                data,
1466            )
1467        }
1468
1469        #[cfg(feature = "xmldsig")]
1470        fn verify(
1471            &self,
1472            key: &dyn crate::xmldsig::VerifyingKey,
1473            algorithm: crate::xmldsig::SignatureAlgorithm,
1474            data: &[u8],
1475            signature: &[u8],
1476        ) -> Result<bool, crate::xmldsig::DsigError> {
1477            crate::provider::CryptoProvider::verify(
1478                &crate::provider::RustCryptoProvider,
1479                key,
1480                algorithm,
1481                data,
1482                signature,
1483            )
1484        }
1485
1486        fn encrypt_data(
1487            &self,
1488            algorithm: DataEncryptionAlgorithm,
1489            key: &[u8],
1490            plaintext: &[u8],
1491        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
1492            if let Some(ciphertext) = &self.ciphertext {
1493                return Ok(ciphertext.clone());
1494            }
1495            crate::provider::CryptoProvider::encrypt_data(
1496                &crate::provider::RustCryptoProvider,
1497                algorithm,
1498                key,
1499                plaintext,
1500            )
1501        }
1502
1503        fn decrypt_data(
1504            &self,
1505            algorithm: DataEncryptionAlgorithm,
1506            key: &[u8],
1507            ciphertext: &[u8],
1508        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
1509            crate::provider::CryptoProvider::decrypt_data(
1510                &crate::provider::RustCryptoProvider,
1511                algorithm,
1512                key,
1513                ciphertext,
1514            )
1515        }
1516
1517        fn wrap_key(
1518            &self,
1519            algorithm: KeyWrapAlgorithm,
1520            kek: &[u8],
1521            key: &[u8],
1522        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
1523            if let Some(wrapped_key) = &self.wrapped_key {
1524                return Ok(wrapped_key.clone());
1525            }
1526            crate::provider::CryptoProvider::wrap_key(
1527                &crate::provider::RustCryptoProvider,
1528                algorithm,
1529                kek,
1530                key,
1531            )
1532        }
1533
1534        fn unwrap_key(
1535            &self,
1536            algorithm: KeyWrapAlgorithm,
1537            kek: &[u8],
1538            wrapped: &[u8],
1539        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
1540            crate::provider::CryptoProvider::unwrap_key(
1541                &crate::provider::RustCryptoProvider,
1542                algorithm,
1543                kek,
1544                wrapped,
1545            )
1546        }
1547
1548        fn transport_key(
1549            &self,
1550            key: &dyn crate::provider::KeyTransportKey,
1551            parameters: &RsaOaepParameters,
1552            plaintext: &[u8],
1553        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
1554            self.transport_calls.fetch_add(1, Ordering::Relaxed);
1555            if let Some(transported_key) = &self.transported_key {
1556                return Ok(transported_key.clone());
1557            }
1558            crate::provider::CryptoProvider::transport_key(
1559                &crate::provider::RustCryptoProvider,
1560                key,
1561                parameters,
1562                plaintext,
1563            )
1564        }
1565
1566        fn recover_key(
1567            &self,
1568            key: &dyn crate::provider::KeyRecoveryKey,
1569            parameters: &RsaOaepParameters,
1570            ciphertext: &[u8],
1571        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
1572            crate::provider::CryptoProvider::recover_key(
1573                &crate::provider::RustCryptoProvider,
1574                key,
1575                parameters,
1576                ciphertext,
1577            )
1578        }
1579    }
1580
1581    #[test]
1582    fn direct_key_round_trips_every_content_algorithm() {
1583        // All emitted wire layouts must be accepted by the existing independent
1584        // decrypt path, including empty plaintext and full-block CBC padding.
1585        for algorithm in [
1586            DataEncryptionAlgorithm::Aes128Cbc,
1587            DataEncryptionAlgorithm::Aes256Cbc,
1588            DataEncryptionAlgorithm::Aes128Gcm,
1589            DataEncryptionAlgorithm::Aes256Gcm,
1590        ] {
1591            for plaintext in [b"".as_slice(), b"sixteen-byte-msg", b"not aligned"] {
1592                let key = vec![0x31; algorithm.key_len()];
1593                let encrypted = EncryptedDataBuilder::new(algorithm)
1594                    .direct_key(key.clone())
1595                    .direct_key_name("content-key")
1596                    .encrypt_binary(plaintext)
1597                    .expect("supported direct encryption must succeed");
1598                assert_eq!(
1599                    decrypt(
1600                        &encrypted.encrypted_data_xml,
1601                        &SymmetricKeyDecryptor::new(key)
1602                    )
1603                    .expect("generated ciphertext must decrypt"),
1604                    super::super::DecryptedContent::Bytes(plaintext.to_vec())
1605                );
1606            }
1607        }
1608    }
1609
1610    #[test]
1611    fn aes_key_wrap_round_trips_and_preserves_recipient_metadata() {
1612        let kek = [0x44; 32];
1613        let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1614            .add_recipient(
1615                EncryptionRecipient::aes_key_wrap(kek, KeyWrapAlgorithm::AesKw256)
1616                    .recipient("service-a")
1617                    .key_name("shared-kek"),
1618            )
1619            .encrypt_xml("<secret>value</secret>")
1620            .expect("AES-KW encryption must succeed");
1621        let parsed = parse_encrypted_data(&encrypted.encrypted_data_xml)
1622            .expect("generated EncryptedData must parse");
1623        assert_eq!(
1624            parsed.encrypted_keys[0].recipient.as_deref(),
1625            Some("service-a")
1626        );
1627        assert_eq!(
1628            parsed.encrypted_keys[0].key_name.as_deref(),
1629            Some("shared-kek")
1630        );
1631        assert_eq!(
1632            decrypt(&encrypted.encrypted_data_xml, &KekDecryptor::new(kek))
1633                .expect("wrapped key must decrypt"),
1634            super::super::DecryptedContent::Xml("<secret>value</secret>".into())
1635        );
1636    }
1637
1638    #[test]
1639    fn aes_key_wrap_rejects_mismatched_kek_before_provider_dispatch() {
1640        // Algorithm URIs define the KEK size. Provider implementations are
1641        // capabilities, not authorities allowed to reinterpret wire semantics.
1642        let builder = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1643            .recipient_aes_kw([0x44; 32], KeyWrapAlgorithm::AesKw128);
1644
1645        assert!(matches!(
1646            builder.validate_configuration_with_policy(&builder.policy),
1647            Err(XmlEncError::InvalidEncryptionConfig(_))
1648        ));
1649    }
1650
1651    #[test]
1652    fn rsa_oaep_round_trips_configurable_parameters() {
1653        let private = RsaPrivateKey::new(&mut UnwrapErr(SysRng), 2048)
1654            .expect("test RSA key generation must succeed");
1655        let public = RsaPublicKey::from(&private);
1656        let parameters =
1657            RsaOaepParameters::xmlenc11(OaepDigestAlgorithm::Sha256, OaepDigestAlgorithm::Sha512)
1658                .label(b"recipient-label".to_vec());
1659        let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes256Gcm)
1660            .add_recipient(
1661                EncryptionRecipient::rsa_oaep(public)
1662                    .oaep_parameters(parameters)
1663                    .recipient("rsa-recipient"),
1664            )
1665            .encrypt_xml("<secret/>")
1666            .expect("RSA-OAEP encryption must succeed");
1667        assert_eq!(
1668            decrypt(
1669                &encrypted.encrypted_data_xml,
1670                &PrivateKeyDecryptor::new(private)
1671            )
1672            .expect("RSA recipient must recover content key"),
1673            super::super::DecryptedContent::Xml("<secret/>".into())
1674        );
1675    }
1676
1677    #[test]
1678    fn legacy_oaep_serializes_explicit_non_default_mgf() {
1679        // libxmlsec1 accepts the XMLEnc 1.1 MGF child under both OAEP URIs. The
1680        // legacy URI supplies SHA-1 only when the child is absent.
1681        let public = RsaPublicKey::from_public_key_pem(include_str!(
1682            "../../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"
1683        ))
1684        .expect("tracked RSA public key must parse");
1685        let parameters = RsaOaepParameters {
1686            algorithm: super::super::KeyTransportAlgorithm::RsaOaepMgf1p,
1687            digest: OaepDigestAlgorithm::Sha256,
1688            mgf_digest: OaepDigestAlgorithm::Sha256,
1689            label: Vec::new(),
1690        };
1691        let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1692            .add_recipient(EncryptionRecipient::rsa_oaep(public).oaep_parameters(parameters))
1693            .encrypt_binary(b"legacy OAEP with explicit MGF")
1694            .expect("libxmlsec1-compatible legacy OAEP parameters must encrypt");
1695        assert!(encrypted.encrypted_data_xml.contains("xenc11:MGF"));
1696    }
1697
1698    #[test]
1699    fn encryption_policy_reports_the_rejected_mgf_uri() {
1700        // The transport and message digest are allowed; only the independently
1701        // selected MGF must be identified as the rejected algorithm.
1702        let public = RsaPublicKey::from_public_key_pem(include_str!(
1703            "../../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"
1704        ))
1705        .expect("tracked RSA public key must parse");
1706        let parameters = RsaOaepParameters {
1707            algorithm: super::super::KeyTransportAlgorithm::RsaOaepMgf1p,
1708            digest: OaepDigestAlgorithm::Sha256,
1709            mgf_digest: OaepDigestAlgorithm::Sha384,
1710            label: Vec::new(),
1711        };
1712        let policy = crate::policy::EncryptionPolicy {
1713            key_transport_algorithms: Some(std::collections::HashSet::from([parameters.algorithm])),
1714            oaep_digests: Some(std::collections::HashSet::from([parameters.digest])),
1715            ..crate::policy::EncryptionPolicy::default()
1716        };
1717        let builder = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1718            .policy(policy)
1719            .add_recipient(EncryptionRecipient::rsa_oaep(public).oaep_parameters(parameters));
1720
1721        assert!(matches!(
1722            builder.validate_configuration_with_policy(&builder.policy),
1723            Err(XmlEncError::Policy(
1724                crate::policy::PolicyViolation::Algorithm { algorithm, .. }
1725            )) if algorithm == OaepDigestAlgorithm::Sha384.mgf_uri()
1726        ));
1727    }
1728
1729    #[test]
1730    fn encrypt_document_replaces_element_and_self_closing_content() {
1731        let key = [0x55; 16];
1732        let document =
1733            "<root><target ID=\"element\"><child/></target><empty ID=\"content\"/></root>";
1734        let encrypted_element = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1735            .direct_key(key)
1736            .encrypt_document(
1737                document,
1738                DocumentEncryptionOptions {
1739                    element_id: Some("element"),
1740                },
1741            )
1742            .expect("element replacement must succeed");
1743        let decrypted_element =
1744            decrypt_document(&encrypted_element, None, &SymmetricKeyDecryptor::new(key))
1745                .expect("element replacement must round-trip");
1746        assert_eq!(decrypted_element, document);
1747
1748        let encrypted_content = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1749            .encryption_type(EncryptedDataType::Content)
1750            .direct_key(key)
1751            .encrypt_document(
1752                document,
1753                DocumentEncryptionOptions {
1754                    element_id: Some("content"),
1755                },
1756            )
1757            .expect("self-closing content replacement must expand the element");
1758        assert!(encrypted_content.contains("<empty ID=\"content\"><xenc:EncryptedData"));
1759        let decrypted_content =
1760            decrypt_document(&encrypted_content, None, &SymmetricKeyDecryptor::new(key))
1761                .expect("empty content must decrypt");
1762        assert_eq!(
1763            decrypted_content,
1764            "<root><target ID=\"element\"><child/></target><empty ID=\"content\"></empty></root>"
1765        );
1766    }
1767
1768    #[test]
1769    fn invalid_configuration_and_bounds_fail_before_encryption() {
1770        let no_key = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1771            .encrypt_binary(b"data")
1772            .expect_err("missing key source must fail");
1773        assert!(matches!(no_key, XmlEncError::InvalidEncryptionConfig(_)));
1774
1775        assert!(
1776            validate_plaintext_len(MAX_ENCRYPTION_PLAINTEXT_LEN, MAX_ENCRYPTION_PLAINTEXT_LEN,)
1777                .is_ok()
1778        );
1779        assert!(matches!(
1780            validate_plaintext_len(
1781                MAX_ENCRYPTION_PLAINTEXT_LEN + 1,
1782                MAX_ENCRYPTION_PLAINTEXT_LEN,
1783            ),
1784            Err(XmlEncError::Policy(
1785                crate::policy::PolicyViolation::ResourceLimit {
1786                    resource: crate::policy::resource_name::ENCRYPTION_PLAINTEXT_BYTES,
1787                    ..
1788                }
1789            ))
1790        ));
1791
1792        assert!(matches!(
1793            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1794                .direct_key([0_u8; 15])
1795                .encrypt_binary(b"data"),
1796            Err(XmlEncError::InvalidKeySize { .. })
1797        ));
1798
1799        let too_many_recipients = (0..=MAX_ENCRYPTION_RECIPIENTS).fold(
1800            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm),
1801            |builder, _| builder.recipient_aes_kw([0_u8; 16], KeyWrapAlgorithm::AesKw128),
1802        );
1803        assert!(matches!(
1804            too_many_recipients.encrypt_binary(b"data"),
1805            Err(XmlEncError::Policy(
1806                crate::policy::PolicyViolation::ResourceLimit {
1807                    resource: crate::policy::resource_name::ENCRYPTION_RECIPIENTS,
1808                    ..
1809                }
1810            ))
1811        ));
1812
1813        let oversized_metadata = "x".repeat(MAX_ENCRYPTION_METADATA_LEN + 1);
1814        assert!(matches!(
1815            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1816                .direct_key([0_u8; 16])
1817                .id(oversized_metadata)
1818                .encrypt_binary(b"data"),
1819            Err(XmlEncError::Policy(
1820                crate::policy::PolicyViolation::ResourceLimit {
1821                    resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
1822                    ..
1823                }
1824            ))
1825        ));
1826    }
1827
1828    #[test]
1829    fn oversized_xml_is_rejected_before_parsing() {
1830        // The input bound protects the parser and the Content wrapper
1831        // allocation, so size must take precedence over malformed XML.
1832        let oversized_malformed = format!(
1833            "<child>{}</unclosed>",
1834            "x".repeat(MAX_ENCRYPTION_PLAINTEXT_LEN)
1835        );
1836
1837        assert!(matches!(
1838            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1839                .encryption_type(EncryptedDataType::Content)
1840                .direct_key([0_u8; 16])
1841                .encrypt_xml(&oversized_malformed),
1842            Err(XmlEncError::Policy(
1843                crate::policy::PolicyViolation::ResourceLimit {
1844                    resource: crate::policy::resource_name::ENCRYPTION_PLAINTEXT_BYTES,
1845                    ..
1846                }
1847            ))
1848        ));
1849    }
1850
1851    #[test]
1852    fn oversized_document_is_rejected_before_parsing() {
1853        // The document API has a separate parser-input bound because the
1854        // selected plaintext may be much smaller than its enclosing document.
1855        let oversized_malformed = format!(
1856            "<root>{}</unclosed>",
1857            "x".repeat(MAX_ENCRYPTION_DOCUMENT_LEN)
1858        );
1859
1860        assert!(matches!(
1861            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1862                .direct_key([0_u8; 16])
1863                .encrypt_document(&oversized_malformed, DocumentEncryptionOptions::default()),
1864            Err(XmlEncError::Policy(
1865                crate::policy::PolicyViolation::ResourceLimit {
1866                    resource: crate::policy::resource_name::XML_DOCUMENT,
1867                    ..
1868                }
1869            ))
1870        ));
1871    }
1872
1873    #[test]
1874    fn encrypted_replacement_must_fit_document_policy() {
1875        // Cipher framing, base64, and EncryptedData markup expand the selected
1876        // range; the returned document must remain valid input to decryption.
1877        for encrypted_type in [EncryptedDataType::Element, EncryptedDataType::Content] {
1878            let document = "<root><target ID=\"selected\">x</target></root>";
1879            let policy = crate::policy::EncryptionPolicy {
1880                resources: crate::policy::ResourcePolicy {
1881                    max_xml_document_bytes: document.len(),
1882                    ..crate::policy::ResourcePolicy::default()
1883                },
1884                ..crate::policy::EncryptionPolicy::default()
1885            };
1886
1887            assert!(matches!(
1888                EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1889                    .encryption_type(encrypted_type)
1890                    .direct_key([0_u8; 16])
1891                    .policy(policy)
1892                    .encrypt_document(
1893                        document,
1894                        DocumentEncryptionOptions {
1895                            element_id: Some("selected"),
1896                        },
1897                    ),
1898                Err(XmlEncError::Policy(
1899                    crate::policy::PolicyViolation::ResourceLimit {
1900                        resource: crate::policy::resource_name::XML_DOCUMENT,
1901                        ..
1902                    }
1903                ))
1904            ));
1905        }
1906    }
1907
1908    #[test]
1909    fn binary_encryption_preserves_an_opaque_type_hint() {
1910        // A non-XML Type URI describes opaque application bytes. It must survive
1911        // binary encryption so decryption can continue returning byte content.
1912        let result = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1913            .encryption_type(EncryptedDataType::Other("urn:example:binary".into()))
1914            .direct_key([0x42_u8; 16])
1915            .encrypt_binary(b"opaque payload")
1916            .expect("opaque binary Type must be accepted");
1917
1918        assert!(
1919            result
1920                .encrypted_data_xml
1921                .contains("Type=\"urn:example:binary\"")
1922        );
1923    }
1924
1925    #[test]
1926    fn binary_encryption_bounds_an_opaque_type_hint() {
1927        // Generated metadata must obey the same policy as reciprocal parsing so
1928        // the builder cannot emit an EncryptedData document it would reject.
1929        let maximum = 64;
1930        let policy = crate::policy::EncryptionPolicy {
1931            resources: crate::policy::ResourcePolicy {
1932                max_encryption_metadata_bytes: maximum,
1933                ..crate::policy::ResourcePolicy::default()
1934            },
1935            ..crate::policy::EncryptionPolicy::default()
1936        };
1937        let encrypt = |uri: String| {
1938            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1939                .encryption_type(EncryptedDataType::Other(uri))
1940                .direct_key([0x42_u8; 16])
1941                .policy(policy.clone())
1942                .encrypt_binary(b"opaque payload")
1943        };
1944
1945        encrypt(format!("urn:{}", "x".repeat(maximum - 4)))
1946            .expect("metadata at the configured boundary must remain accepted");
1947        assert!(matches!(
1948            encrypt(format!("urn:{}", "x".repeat(maximum - 3))),
1949            Err(XmlEncError::Policy(
1950                crate::policy::PolicyViolation::ResourceLimit {
1951                    resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
1952                    maximum: 64,
1953                    actual: 65,
1954                }
1955            ))
1956        ));
1957    }
1958
1959    #[test]
1960    fn encryption_policy_bounds_xml_nodes_at_both_parse_entry_points() {
1961        // XML plaintext and whole-document encryption are separate parser paths;
1962        // both must consume the same immutable operation-policy node ceiling.
1963        let policy = crate::policy::EncryptionPolicy {
1964            resources: crate::policy::ResourcePolicy {
1965                max_xml_nodes: 4,
1966                ..crate::policy::ResourcePolicy::default()
1967            },
1968            ..crate::policy::EncryptionPolicy::default()
1969        };
1970        let builder = || {
1971            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
1972                .direct_key([0_u8; 16])
1973                .policy(policy.clone())
1974        };
1975        let xml = "<root><a/><b/><c/><d/></root>";
1976
1977        assert!(matches!(
1978            builder().encrypt_xml(xml),
1979            Err(XmlEncError::Policy(
1980                crate::policy::PolicyViolation::ResourceLimit {
1981                    resource: crate::policy::resource_name::XML_NODES,
1982                    maximum: 4,
1983                    actual: 5,
1984                }
1985            ))
1986        ));
1987        assert!(matches!(
1988            builder().encrypt_document(xml, DocumentEncryptionOptions::default()),
1989            Err(XmlEncError::Policy(
1990                crate::policy::PolicyViolation::ResourceLimit {
1991                    resource: crate::policy::resource_name::XML_NODES,
1992                    maximum: 4,
1993                    actual: 5,
1994                }
1995            ))
1996        ));
1997    }
1998
1999    #[test]
2000    fn encryption_entry_points_enforce_policy_depth() {
2001        // Whole-document encryption must apply the same depth policy to string
2002        // and retained inputs before target selection or encryption work.
2003        let xml = "<root><child><leaf/></child></root>";
2004        let policy = crate::policy::EncryptionPolicy {
2005            resources: crate::policy::ResourcePolicy {
2006                max_xml_depth: 2,
2007                ..crate::policy::ResourcePolicy::default()
2008            },
2009            ..crate::policy::EncryptionPolicy::default()
2010        };
2011        let builder = || {
2012            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2013                .direct_key([0_u8; 16])
2014                .policy(policy.clone())
2015        };
2016        let mut document = XmlDocument::parse(xml).expect("wide retained fixture must parse");
2017
2018        assert!(matches!(
2019            builder().encrypt_document(xml, DocumentEncryptionOptions::default()),
2020            Err(XmlEncError::Policy(
2021                crate::policy::PolicyViolation::ResourceLimit {
2022                    resource: crate::policy::resource_name::XML_DEPTH,
2023                    maximum: 2,
2024                    actual: 3,
2025                }
2026            ))
2027        ));
2028        assert!(matches!(
2029            builder().encrypt_owned_document(&mut document, DocumentEncryptionOptions::default()),
2030            Err(XmlEncError::Policy(
2031                crate::policy::PolicyViolation::ResourceLimit {
2032                    resource: crate::policy::resource_name::XML_DEPTH,
2033                    maximum: 2,
2034                    actual: 3,
2035                }
2036            ))
2037        ));
2038    }
2039
2040    #[test]
2041    fn encryption_plaintext_and_owned_mutations_use_the_active_depth() {
2042        // A document parsed under broad defaults must not retain those defaults
2043        // when a stricter encryption operation parses or commits replacement XML.
2044        let policy = crate::policy::EncryptionPolicy {
2045            resources: crate::policy::ResourcePolicy {
2046                max_xml_depth: 1,
2047                ..crate::policy::ResourcePolicy::default()
2048            },
2049            ..crate::policy::EncryptionPolicy::default()
2050        };
2051        let builder = || {
2052            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2053                .direct_key([0_u8; 16])
2054                .policy(policy.clone())
2055        };
2056
2057        assert!(matches!(
2058            builder().encrypt_xml("<root><child/></root>"),
2059            Err(XmlEncError::Policy(
2060                crate::policy::PolicyViolation::ResourceLimit {
2061                    resource: crate::policy::resource_name::XML_DEPTH,
2062                    maximum: 1,
2063                    actual: 2,
2064                }
2065            ))
2066        ));
2067
2068        let generated = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2069            .direct_key([0_u8; 16])
2070            .encryption_type(EncryptedDataType::Content)
2071            .encrypt_xml("")
2072            .expect("generated content replacement must parse");
2073        let generated_document = Document::parse(&generated.encrypted_data_xml)
2074            .expect("generated replacement must be XML");
2075        let generated_depth = generated_document
2076            .descendants()
2077            .filter(|node| node.is_element())
2078            .map(|node| {
2079                node.ancestors()
2080                    .filter(|ancestor| ancestor.is_element())
2081                    .count()
2082            })
2083            .max()
2084            .expect("generated replacement has elements");
2085        let mutation_policy = crate::policy::EncryptionPolicy {
2086            resources: crate::policy::ResourcePolicy {
2087                max_xml_depth: generated_depth,
2088                ..crate::policy::ResourcePolicy::default()
2089            },
2090            ..crate::policy::EncryptionPolicy::default()
2091        };
2092        let mut document = XmlDocument::parse("<root/>").expect("shallow fixture must parse");
2093        let original = document.as_xml().to_owned();
2094        let generation = document.generation();
2095        assert!(matches!(
2096            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2097                .direct_key([0_u8; 16])
2098                .encryption_type(EncryptedDataType::Content)
2099                .policy(mutation_policy)
2100                .encrypt_owned_document(&mut document, DocumentEncryptionOptions::default()),
2101            Err(XmlEncError::Policy(
2102                crate::policy::PolicyViolation::ResourceLimit {
2103                    resource: crate::policy::resource_name::XML_DEPTH,
2104                    maximum,
2105                    actual,
2106                }
2107            )) if maximum == generated_depth && actual == generated_depth + 1
2108        ));
2109        assert_eq!(document.as_xml(), original);
2110        assert_eq!(document.generation(), generation);
2111    }
2112
2113    #[test]
2114    fn owned_encryption_checks_active_node_limit_before_target_selection() {
2115        // A missing selector must not bypass the active policy or make target
2116        // discovery traverse a retained document parsed under a wider ceiling.
2117        let policy = crate::policy::EncryptionPolicy {
2118            resources: crate::policy::ResourcePolicy {
2119                max_xml_nodes: 1,
2120                ..crate::policy::ResourcePolicy::default()
2121            },
2122            ..crate::policy::EncryptionPolicy::default()
2123        };
2124        let mut document = XmlDocument::parse("<root><first/><second/></root>")
2125            .expect("retained fixture must parse");
2126        let before = document.as_xml().to_owned();
2127
2128        let error = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2129            .direct_key([0_u8; 16])
2130            .policy(policy)
2131            .encrypt_owned_document(
2132                &mut document,
2133                DocumentEncryptionOptions {
2134                    element_id: Some("missing"),
2135                },
2136            )
2137            .expect_err("active node ceiling must precede target selection");
2138
2139        assert!(matches!(
2140            error,
2141            XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2142                resource: crate::policy::resource_name::XML_NODES,
2143                maximum: 1,
2144                actual,
2145            }) if actual > 1
2146        ));
2147        assert_eq!(document.as_xml(), before);
2148        assert_eq!(document.generation(), 0);
2149    }
2150
2151    #[test]
2152    fn document_encryption_initial_parse_uses_the_policy_work_budget() {
2153        // Parsing the caller's XML and parsing the encrypted replacement must
2154        // consume one operation-wide allowance rather than independent caps.
2155        let xml = "<root/>";
2156        let policy = crate::policy::EncryptionPolicy {
2157            resources: crate::policy::ResourcePolicy {
2158                max_xml_parse_work_bytes: 0,
2159                ..crate::policy::ResourcePolicy::default()
2160            },
2161            ..crate::policy::EncryptionPolicy::default()
2162        };
2163
2164        let error = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2165            .direct_key([0_u8; 16])
2166            .policy(policy)
2167            .encrypt_document(xml, DocumentEncryptionOptions::default())
2168            .expect_err("a zero parse-work budget must reject the input parse");
2169
2170        assert!(matches!(
2171            error,
2172            XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2173                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
2174                maximum: 0,
2175                actual,
2176            }) if actual == xml.len()
2177        ));
2178    }
2179
2180    #[test]
2181    fn content_plaintext_node_limit_excludes_the_internal_wrapper() {
2182        let policy = |max_xml_nodes| crate::policy::EncryptionPolicy {
2183            resources: crate::policy::ResourcePolicy {
2184                max_xml_nodes,
2185                ..crate::policy::ResourcePolicy::default()
2186            },
2187            ..crate::policy::EncryptionPolicy::default()
2188        };
2189
2190        let policy_three = policy(3);
2191        let budget_three = XmlParseWorkBudget::from_resources(&policy_three.resources);
2192        validate_xml_plaintext(
2193            "<first/><second/>",
2194            &EncryptedDataType::Content,
2195            &policy_three,
2196            &budget_three,
2197            crate::XmlBackend::default(),
2198        )
2199        .expect("the caller root and two elements must fit a three-node policy");
2200        let policy_two = policy(2);
2201        let budget_two = XmlParseWorkBudget::from_resources(&policy_two.resources);
2202        assert!(matches!(
2203            validate_xml_plaintext(
2204                "<first/><second/>",
2205                &EncryptedDataType::Content,
2206                &policy_two,
2207                &budget_two,
2208                crate::XmlBackend::default(),
2209            ),
2210            Err(XmlEncError::Policy(
2211                crate::policy::PolicyViolation::ResourceLimit {
2212                    resource: crate::policy::resource_name::XML_NODES,
2213                    maximum: 2,
2214                    actual: 3,
2215                }
2216            ))
2217        ));
2218    }
2219
2220    #[test]
2221    fn content_plaintext_byte_limit_excludes_only_the_internal_wrapper() {
2222        // The parser-only wrapper must not replace the caller's byte ceiling.
2223        // Encryption plaintext may be larger than the XML input policy, but an
2224        // XML Content fragment still has to satisfy both limits before parsing.
2225        let xml = "<first/><second/>";
2226        let maximum = xml.len() - 1;
2227        let policy = crate::policy::EncryptionPolicy {
2228            resources: crate::policy::ResourcePolicy {
2229                max_xml_document_bytes: maximum,
2230                max_encryption_plaintext_bytes: xml.len(),
2231                ..crate::policy::ResourcePolicy::default()
2232            },
2233            ..crate::policy::EncryptionPolicy::default()
2234        };
2235        let budget = XmlParseWorkBudget::from_resources(&policy.resources);
2236
2237        assert!(matches!(
2238            validate_xml_plaintext(
2239                xml,
2240                &EncryptedDataType::Content,
2241                &policy,
2242                &budget,
2243                crate::XmlBackend::default(),
2244            ),
2245            Err(XmlEncError::Policy(
2246                crate::policy::PolicyViolation::ResourceLimit {
2247                    resource: crate::policy::resource_name::XML_DOCUMENT,
2248                    maximum: observed_maximum,
2249                    actual,
2250                }
2251            )) if observed_maximum == maximum && actual == xml.len()
2252        ));
2253        assert_eq!(
2254            budget.consumed(),
2255            0,
2256            "oversized XML must fail before parsing"
2257        );
2258    }
2259
2260    #[test]
2261    fn generated_encrypted_data_parse_uses_the_active_node_limit() {
2262        // Generated XML is still operation work: reject it in the parser under
2263        // the active policy rather than allocating up to the absolute ceiling.
2264        let xml = "<EncryptedData><CipherData><CipherValue>AA==</CipherValue></CipherData></EncryptedData>";
2265        let policy = crate::policy::EncryptionPolicy {
2266            resources: crate::policy::ResourcePolicy {
2267                max_xml_nodes: 1,
2268                ..crate::policy::ResourcePolicy::default()
2269            },
2270            ..crate::policy::EncryptionPolicy::default()
2271        };
2272        let budget = XmlParseWorkBudget::from_resources(&policy.resources);
2273
2274        assert!(matches!(
2275            count_generated_encrypted_data_nodes(
2276                xml,
2277                &policy,
2278                &budget,
2279                crate::XmlBackend::default(),
2280            ),
2281            Err(XmlEncError::Policy(
2282                crate::policy::PolicyViolation::ResourceLimit {
2283                    resource: crate::policy::resource_name::XML_NODES,
2284                    maximum: 1,
2285                    actual: 2,
2286                }
2287            ))
2288        ));
2289    }
2290
2291    #[test]
2292    fn document_encryption_bounds_projected_replacement_nodes() {
2293        fn policy(max_xml_nodes: usize) -> crate::policy::EncryptionPolicy {
2294            crate::policy::EncryptionPolicy {
2295                resources: crate::policy::ResourcePolicy {
2296                    max_xml_nodes,
2297                    ..crate::policy::ResourcePolicy::default()
2298                },
2299                ..crate::policy::EncryptionPolicy::default()
2300            }
2301        }
2302
2303        let generated_nodes = {
2304            let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2305                .direct_key([0_u8; 16])
2306                .encrypt_binary(b"payload")
2307                .expect("default policy must permit generated EncryptedData");
2308            Document::parse(&encrypted.encrypted_data_xml)
2309                .expect("generated EncryptedData must parse")
2310                .root_element()
2311                .descendants()
2312                .count()
2313        };
2314
2315        // The source document fits the low limit, but the generated
2316        // EncryptedData replacement does not. The ceiling admits the standalone
2317        // fragment so this specifically exercises whole-document projection.
2318        let element_actual = match EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2319            .direct_key([0_u8; 16])
2320            .policy(policy(generated_nodes))
2321            .encrypt_document("<root/>", DocumentEncryptionOptions::default())
2322        {
2323            Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2324                resource: "XML nodes",
2325                maximum,
2326                actual,
2327            })) if maximum == generated_nodes && actual > maximum => actual,
2328            result => panic!("expected projected element node bound, got {result:?}"),
2329        };
2330        EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2331            .direct_key([0_u8; 16])
2332            .policy(policy(element_actual))
2333            .encrypt_document("<root/>", DocumentEncryptionOptions::default())
2334            .expect("the exact projected element node limit must be accepted");
2335
2336        // Content replacement retains the selected element. In particular, a
2337        // self-closing element expands around EncryptedData without adding an
2338        // extra source node to the projection.
2339        let content_actual = match EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2340            .encryption_type(EncryptedDataType::Content)
2341            .direct_key([0_u8; 16])
2342            .policy(policy(generated_nodes))
2343            .encrypt_document("<root/>", DocumentEncryptionOptions::default())
2344        {
2345            Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2346                resource: "XML nodes",
2347                maximum,
2348                actual,
2349            })) if maximum == generated_nodes && actual > maximum => actual,
2350            result => panic!("expected projected content node bound, got {result:?}"),
2351        };
2352        EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2353            .encryption_type(EncryptedDataType::Content)
2354            .direct_key([0_u8; 16])
2355            .policy(policy(content_actual))
2356            .encrypt_document("<root/>", DocumentEncryptionOptions::default())
2357            .expect("the exact projected content node limit must be accepted");
2358    }
2359
2360    #[test]
2361    fn standalone_encryption_bounds_generated_xml_nodes() {
2362        fn policy(max_xml_nodes: usize) -> crate::policy::EncryptionPolicy {
2363            crate::policy::EncryptionPolicy {
2364                resources: crate::policy::ResourcePolicy {
2365                    max_xml_nodes,
2366                    ..crate::policy::ResourcePolicy::default()
2367                },
2368                ..crate::policy::EncryptionPolicy::default()
2369            }
2370        }
2371
2372        // Binary encryption has no input XML tree, but its generated EncryptedData
2373        // must still be consumable under the same operation-policy node ceiling.
2374        assert!(matches!(
2375            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2376                .direct_key([0_u8; 16])
2377                .policy(policy(1))
2378                .encrypt_binary(b"payload"),
2379            Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2380                resource: "XML nodes",
2381                maximum: 1,
2382                actual,
2383            })) if actual > 1
2384        ));
2385
2386        let generated = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2387            .direct_key([0_u8; 16])
2388            .encrypt_binary(b"payload")
2389            .expect("default policy must permit generated EncryptedData");
2390        let document = Document::parse(&generated.encrypted_data_xml)
2391            .expect("generated EncryptedData must parse");
2392        let actual = document.root().descendants().count();
2393        assert!(matches!(
2394            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2395                .direct_key([0_u8; 16])
2396                .policy(policy(actual - 1))
2397                .encrypt_binary(b"payload"),
2398            Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2399                resource: "XML nodes",
2400                maximum,
2401                actual: reported,
2402            })) if maximum == actual - 1 && reported == actual
2403        ));
2404        let exact = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2405            .direct_key([0_u8; 16])
2406            .policy(policy(actual))
2407            .encrypt_binary(b"payload")
2408            .expect("the exact generated node limit must be accepted");
2409        let exact_document =
2410            Document::parse(&exact.encrypted_data_xml).expect("exact-boundary output must parse");
2411        let decryption_policy = crate::policy::DecryptionPolicy {
2412            resources: crate::policy::ResourcePolicy {
2413                max_xml_nodes: actual,
2414                ..crate::policy::ResourcePolicy::default()
2415            },
2416            ..crate::policy::DecryptionPolicy::default()
2417        };
2418        super::super::parse::parse_encrypted_data_node_with_policy(
2419            exact_document.root_element(),
2420            &decryption_policy,
2421        )
2422        .expect("output accepted at the exact limit must be consumable at the same limit");
2423    }
2424
2425    #[test]
2426    fn document_dtd_is_controlled_only_by_operation_policy() {
2427        // Parser behavior comes from the immutable operation snapshot; target
2428        // selection options cannot independently weaken or tighten it.
2429        let document = "<!DOCTYPE root [<!ELEMENT root ANY>]><root/>";
2430        assert!(matches!(
2431            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2432                .direct_key([0_u8; 16])
2433                .encrypt_document(document, DocumentEncryptionOptions { element_id: None },),
2434            Err(XmlEncError::XmlParse(_))
2435        ));
2436        let mut policy = crate::policy::EncryptionPolicy::default();
2437        policy.xml.allow_internal_dtd = true;
2438        EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2439            .direct_key([0_u8; 16])
2440            .policy(policy.clone())
2441            .encrypt_document(document, DocumentEncryptionOptions { element_id: None })
2442            .expect("the explicit operation policy should permit parsing");
2443    }
2444
2445    #[test]
2446    fn invalid_resource_policy_is_rejected_at_every_entry_point() {
2447        // Entry points must reject an invalid snapshot before parsing or using
2448        // any caller-selected limit derived from it.
2449        let mut policy = crate::policy::EncryptionPolicy::default();
2450        policy.resources.max_encryption_plaintext_bytes =
2451            crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING + 1;
2452        let builder = || {
2453            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2454                .direct_key([0_u8; 16])
2455                .policy(policy.clone())
2456        };
2457
2458        assert!(matches!(
2459            builder().encrypt_xml("<broken>"),
2460            Err(XmlEncError::Policy(_))
2461        ));
2462        assert!(matches!(
2463            builder().encrypt_binary(b"x"),
2464            Err(XmlEncError::Policy(_))
2465        ));
2466        assert!(matches!(
2467            builder().encrypt_document("<broken>", DocumentEncryptionOptions::default()),
2468            Err(XmlEncError::Policy(_))
2469        ));
2470    }
2471
2472    #[test]
2473    fn standalone_encrypted_output_obeys_document_byte_ceiling() {
2474        // The returned fragment must remain admissible to the reciprocal parser;
2475        // plaintext bounds alone do not account for framing, base64, or markup.
2476        let policy = |maximum| crate::policy::EncryptionPolicy {
2477            resources: crate::policy::ResourcePolicy {
2478                max_xml_document_bytes: maximum,
2479                ..crate::policy::ResourcePolicy::default()
2480            },
2481            ..crate::policy::EncryptionPolicy::default()
2482        };
2483
2484        let binary_len = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2485            .direct_key([0_u8; 16])
2486            .encrypt_binary(b"bounded binary")
2487            .expect("baseline binary encryption must succeed")
2488            .encrypted_data_xml
2489            .len();
2490        assert!(matches!(
2491            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2492                .direct_key([0_u8; 16])
2493                .policy(policy(binary_len - 1))
2494                .encrypt_binary(b"bounded binary"),
2495            Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2496                resource: crate::policy::resource_name::XML_DOCUMENT,
2497                maximum,
2498                actual,
2499            }))
2500                if maximum == binary_len - 1 && actual == binary_len
2501        ));
2502        EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2503            .direct_key([0_u8; 16])
2504            .policy(policy(binary_len))
2505            .encrypt_binary(b"bounded binary")
2506            .expect("the exact standalone binary output bound must be accepted");
2507
2508        let xml_len = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2509            .direct_key([0_u8; 16])
2510            .encrypt_xml("<secret>bounded XML</secret>")
2511            .expect("baseline XML encryption must succeed")
2512            .encrypted_data_xml
2513            .len();
2514        assert!(matches!(
2515            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2516                .direct_key([0_u8; 16])
2517                .policy(policy(xml_len - 1))
2518                .encrypt_xml("<secret>bounded XML</secret>"),
2519            Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2520                resource: crate::policy::resource_name::XML_DOCUMENT,
2521                maximum,
2522                actual,
2523            }))
2524                if maximum == xml_len - 1 && actual == xml_len
2525        ));
2526        EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2527            .direct_key([0_u8; 16])
2528            .policy(policy(xml_len))
2529            .encrypt_xml("<secret>bounded XML</secret>")
2530            .expect("the exact standalone XML output bound must be accepted");
2531    }
2532
2533    #[test]
2534    fn zero_resource_ceilings_allow_operations_that_consume_none() {
2535        // Zero is deny-all, not an invalid policy. Direct-key encryption has no
2536        // recipients, and an empty binary payload consumes no plaintext bytes.
2537        let policy = crate::policy::EncryptionPolicy {
2538            resources: crate::policy::ResourcePolicy {
2539                max_encryption_plaintext_bytes: 0,
2540                max_encryption_recipients: 0,
2541                ..crate::policy::ResourcePolicy::default()
2542            },
2543            ..crate::policy::EncryptionPolicy::default()
2544        };
2545
2546        EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2547            .direct_key([0_u8; 16])
2548            .policy(policy.clone())
2549            .encrypt_binary(&[])
2550            .expect("zero ceilings must allow resources the operation does not consume");
2551
2552        assert!(matches!(
2553            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2554                .direct_key([0_u8; 16])
2555                .policy(policy.clone())
2556                .encrypt_binary(b"x"),
2557            Err(XmlEncError::Policy(
2558                crate::policy::PolicyViolation::ResourceLimit {
2559                    resource: crate::policy::resource_name::ENCRYPTION_PLAINTEXT_BYTES,
2560                    maximum: 0,
2561                    actual: 1
2562                }
2563            ))
2564        ));
2565        assert!(matches!(
2566            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2567                .recipient_aes_kw([0_u8; 16], KeyWrapAlgorithm::AesKw128)
2568                .policy(policy)
2569                .encrypt_binary(&[]),
2570            Err(XmlEncError::Policy(
2571                crate::policy::PolicyViolation::ResourceLimit {
2572                    resource: crate::policy::resource_name::ENCRYPTION_RECIPIENTS,
2573                    maximum: 0,
2574                    actual: 1,
2575                }
2576            ))
2577        ));
2578    }
2579
2580    #[test]
2581    fn encryption_enforces_key_candidate_budget_before_inspection() {
2582        // Candidate accounting must reject configured keys before validating or
2583        // dispatching them, while accepting the exact operation-wide boundary.
2584        let deny_keys = crate::policy::EncryptionPolicy {
2585            resources: crate::policy::ResourcePolicy {
2586                max_key_candidates: 0,
2587                ..crate::policy::ResourcePolicy::default()
2588            },
2589            ..crate::policy::EncryptionPolicy::default()
2590        };
2591
2592        for error in [
2593            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2594                .direct_key([0_u8; 15])
2595                .policy(deny_keys.clone())
2596                .encrypt_binary(b"data")
2597                .expect_err("a direct key must consume one candidate"),
2598            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2599                .recipient_aes_kw([0_u8; 15], KeyWrapAlgorithm::AesKw128)
2600                .policy(deny_keys)
2601                .encrypt_binary(b"data")
2602                .expect_err("a recipient key must consume one candidate"),
2603        ] {
2604            assert!(matches!(
2605                error,
2606                XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2607                    resource: crate::policy::resource_name::KEY_CANDIDATES,
2608                    maximum: 0,
2609                    actual: 1,
2610                })
2611            ));
2612        }
2613
2614        let recipient =
2615            || EncryptionRecipient::aes_key_wrap([0_u8; 16], KeyWrapAlgorithm::AesKw128);
2616        let policy_with_candidate_limit = |maximum| crate::policy::EncryptionPolicy {
2617            resources: crate::policy::ResourcePolicy {
2618                max_key_candidates: maximum,
2619                ..crate::policy::ResourcePolicy::default()
2620            },
2621            ..crate::policy::EncryptionPolicy::default()
2622        };
2623        let builder = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2624            .add_recipient(recipient())
2625            .add_recipient(recipient());
2626
2627        assert!(matches!(
2628            builder
2629                .clone()
2630                .policy(policy_with_candidate_limit(1))
2631                .encrypt_binary(b"data"),
2632            Err(XmlEncError::Policy(
2633                crate::policy::PolicyViolation::ResourceLimit {
2634                    resource: crate::policy::resource_name::KEY_CANDIDATES,
2635                    maximum: 1,
2636                    actual: 2,
2637                }
2638            ))
2639        ));
2640        builder
2641            .policy(policy_with_candidate_limit(2))
2642            .encrypt_binary(b"data")
2643            .expect("the exact key-candidate boundary must be accepted");
2644    }
2645
2646    #[test]
2647    fn element_plaintext_enforces_replacement_node_contract() {
2648        // Element ciphertext must be safe for the reciprocal document replacement:
2649        // boundary whitespace/comments are harmless, but processing instructions are not.
2650        let builder =
2651            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm).direct_key([0_u8; 16]);
2652        builder
2653            .encrypt_xml("\n<!--before--><secret/><!--after-->\n")
2654            .expect("one element with boundary trivia must be accepted");
2655
2656        assert!(matches!(
2657            builder.encrypt_xml("<?target value?><secret/>"),
2658            Err(XmlEncError::InvalidStructure(_))
2659        ));
2660    }
2661
2662    #[test]
2663    fn empty_key_names_are_rejected_before_serialization() {
2664        // The reciprocal parser rejects empty KeyName elements, so encryption
2665        // must not emit output that its own decrypt pipeline cannot consume.
2666        assert!(matches!(
2667            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2668                .direct_key([0_u8; 16])
2669                .direct_key_name("")
2670                .encrypt_binary(b"data"),
2671            Err(XmlEncError::InvalidEncryptionConfig(_))
2672        ));
2673        assert!(matches!(
2674            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2675                .add_recipient(
2676                    EncryptionRecipient::aes_key_wrap([0_u8; 16], KeyWrapAlgorithm::AesKw128)
2677                        .key_name("")
2678                )
2679                .encrypt_binary(b"data"),
2680            Err(XmlEncError::InvalidEncryptionConfig(_))
2681        ));
2682    }
2683
2684    #[test]
2685    fn xml_forbidden_metadata_characters_are_rejected() {
2686        // XML escaping cannot legalize forbidden XML 1.0 code points, so every
2687        // caller-controlled attribute/text path must fail before serialization.
2688        assert!(matches!(
2689            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2690                .direct_key([0_u8; 16])
2691                .id("invalid\0id")
2692                .encrypt_binary(b"data"),
2693            Err(XmlEncError::InvalidEncryptionConfig(_))
2694        ));
2695        assert!(matches!(
2696            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2697                .direct_key([0_u8; 16])
2698                .direct_key_name("invalid\0name")
2699                .encrypt_binary(b"data"),
2700            Err(XmlEncError::InvalidEncryptionConfig(_))
2701        ));
2702        assert!(matches!(
2703            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2704                .add_recipient(
2705                    EncryptionRecipient::aes_key_wrap([0_u8; 16], KeyWrapAlgorithm::AesKw128)
2706                        .recipient("invalid\0recipient")
2707                )
2708                .encrypt_binary(b"data"),
2709            Err(XmlEncError::InvalidEncryptionConfig(_))
2710        ));
2711        assert!(matches!(
2712            EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2713                .add_recipient(
2714                    EncryptionRecipient::aes_key_wrap([0_u8; 16], KeyWrapAlgorithm::AesKw128)
2715                        .key_name("invalid\0name")
2716                )
2717                .encrypt_binary(b"data"),
2718            Err(XmlEncError::InvalidEncryptionConfig(_))
2719        ));
2720    }
2721
2722    #[test]
2723    fn encrypted_data_id_must_be_an_xml_ncname() {
2724        // xsd:ID derives from NCName; escaping arbitrary attribute text cannot
2725        // make whitespace, a leading digit, or a colon schema-valid.
2726        for invalid in ["bad id", "1leading", "qualified:name", ""] {
2727            assert!(matches!(
2728                EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2729                    .direct_key([0_u8; 16])
2730                    .id(invalid)
2731                    .encrypt_binary(b"data"),
2732                Err(XmlEncError::InvalidEncryptionConfig(_))
2733            ));
2734        }
2735
2736        EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2737            .direct_key([0_u8; 16])
2738            .id("Δοκιμή")
2739            .encrypt_binary(b"data")
2740            .expect("Unicode XML NCNames must remain valid identifiers");
2741    }
2742
2743    #[test]
2744    fn rejects_malformed_custom_provider_ciphertext_before_serialization() {
2745        // Providers supply primitives, but the facade owns the standard wire
2746        // contract and must not serialize output its own decryptor rejects.
2747        for (algorithm, ciphertext) in [
2748            (DataEncryptionAlgorithm::Aes128Gcm, vec![0_u8; 27]),
2749            (DataEncryptionAlgorithm::Aes128Cbc, vec![0_u8; 31]),
2750            (DataEncryptionAlgorithm::Aes256Cbc, vec![0_u8; 33]),
2751        ] {
2752            let error = EncryptedDataBuilder::new(algorithm)
2753                .provider(Arc::new(OverridingOutputProvider {
2754                    ciphertext: Some(ciphertext),
2755                    wrapped_key: None,
2756                    transported_key: None,
2757                    transport_calls: AtomicUsize::new(0),
2758                }))
2759                .direct_key(vec![0_u8; algorithm.key_len()])
2760                .encrypt_binary(b"data")
2761                .expect_err("malformed provider output must fail before XML serialization");
2762            assert!(matches!(
2763                error,
2764                XmlEncError::DataTooShort { .. } | XmlEncError::InvalidCbcCiphertextLength { .. }
2765            ));
2766        }
2767    }
2768
2769    #[test]
2770    fn rejects_overlong_custom_provider_ciphertext_before_serialization() {
2771        // Provider success cannot change the algorithm-defined relationship
2772        // between plaintext and ciphertext length.
2773        for (algorithm, expected, actual) in [
2774            (DataEncryptionAlgorithm::Aes128Gcm, 32, 33),
2775            (DataEncryptionAlgorithm::Aes128Cbc, 32, 48),
2776        ] {
2777            let error = EncryptedDataBuilder::new(algorithm)
2778                .provider(Arc::new(OverridingOutputProvider {
2779                    ciphertext: Some(vec![0_u8; actual]),
2780                    wrapped_key: None,
2781                    transported_key: None,
2782                    transport_calls: AtomicUsize::new(0),
2783                }))
2784                .direct_key(vec![0_u8; algorithm.key_len()])
2785                .encrypt_binary(b"data")
2786                .expect_err("overlong provider output must fail before XML serialization");
2787            assert!(matches!(
2788                error,
2789                XmlEncError::Provider(crate::provider::ProviderError::InvalidOutputSize {
2790                    operation: crate::provider::ProviderOperation::Encrypt,
2791                    expected: observed_expected,
2792                    actual: observed_actual,
2793                }) if observed_expected == expected && observed_actual == actual
2794            ));
2795        }
2796    }
2797
2798    #[test]
2799    fn rejects_malformed_custom_provider_wrapped_keys_before_serialization() {
2800        // RFC 3394 adds exactly one 64-bit integrity block. Accepting any other
2801        // provider output would emit EncryptedKey data no recipient can unwrap.
2802        for wrapped_key in [vec![], vec![0_u8; 23], vec![0_u8; 25]] {
2803            let result = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2804                .provider(Arc::new(OverridingOutputProvider {
2805                    ciphertext: None,
2806                    wrapped_key: Some(wrapped_key),
2807                    transported_key: None,
2808                    transport_calls: AtomicUsize::new(0),
2809                }))
2810                .recipient_aes_kw([0_u8; 16], KeyWrapAlgorithm::AesKw128)
2811                .encrypt_binary(b"data");
2812            assert!(matches!(
2813                result,
2814                Err(XmlEncError::InvalidWrappedKeyLength { expected: 24, actual })
2815                    if actual != 24
2816            ));
2817        }
2818
2819        EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2820            .provider(Arc::new(OverridingOutputProvider {
2821                ciphertext: None,
2822                wrapped_key: Some(vec![0_u8; 24]),
2823                transported_key: None,
2824                transport_calls: AtomicUsize::new(0),
2825            }))
2826            .recipient_aes_kw([0_u8; 16], KeyWrapAlgorithm::AesKw128)
2827            .encrypt_binary(b"data")
2828            .expect("exact RFC 3394 wrapped-key length must remain accepted");
2829    }
2830
2831    #[test]
2832    fn rejects_malformed_custom_provider_rsa_transport_before_serialization() {
2833        // RSA ciphertext is exactly one modulus wide. Enforcing that invariant
2834        // here prevents custom providers from emitting undecryptable XML.
2835        let private_key = RsaPrivateKey::new(&mut UnwrapErr(SysRng), 2048)
2836            .expect("RSA key generation should succeed");
2837        let public_key = RsaPublicKey::from(&private_key);
2838        for transported_key in [vec![], vec![0_u8; 255], vec![0_u8; 257]] {
2839            let result = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2840                .provider(Arc::new(OverridingOutputProvider {
2841                    ciphertext: None,
2842                    wrapped_key: None,
2843                    transported_key: Some(transported_key),
2844                    transport_calls: AtomicUsize::new(0),
2845                }))
2846                .recipient_rsa_oaep(public_key.clone())
2847                .encrypt_binary(b"data");
2848            assert!(matches!(
2849                result,
2850                Err(XmlEncError::InvalidWrappedKeyLength {
2851                    expected: 256,
2852                    actual,
2853                }) if actual != 256
2854            ));
2855        }
2856
2857        EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2858            .provider(Arc::new(OverridingOutputProvider {
2859                ciphertext: None,
2860                wrapped_key: None,
2861                transported_key: Some(vec![0_u8; 256]),
2862                transport_calls: AtomicUsize::new(0),
2863            }))
2864            .recipient_rsa_oaep(public_key)
2865            .encrypt_binary(b"data")
2866            .expect("modulus-sized RSA transport output must remain accepted");
2867    }
2868
2869    #[test]
2870    fn custom_provider_encrypts_with_an_opaque_transport_key() {
2871        // The key exposes only public policy metadata. Successful encryption
2872        // proves orchestration never needs a concrete RustCrypto RSA object.
2873        let provider = Arc::new(OverridingOutputProvider {
2874            ciphertext: None,
2875            wrapped_key: None,
2876            transported_key: Some(vec![0x5a; 256]),
2877            transport_calls: AtomicUsize::new(0),
2878        });
2879        let key = Arc::new(OpaqueTransportKey {
2880            modulus: vec![0x80; 256],
2881            exponent: vec![0x01, 0x00, 0x01],
2882        });
2883
2884        let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2885            .provider(provider.clone())
2886            .recipient_key_transport(key)
2887            .encrypt_binary(b"opaque provider key")
2888            .expect("custom provider must accept its opaque transport key");
2889
2890        assert_eq!(provider.transport_calls.load(Ordering::Relaxed), 1);
2891        assert!(encrypted.encrypted_data_xml.contains("rsa-oaep"));
2892    }
2893
2894    #[test]
2895    fn opaque_transport_preflight_rejects_weak_rsa_metadata() {
2896        // Provider-owned keys cannot bypass the same outbound RSA policy used
2897        // by the RustCrypto convenience constructor.
2898        let key = OpaqueTransportKey {
2899            modulus: vec![0x80; 128],
2900            exponent: vec![0x01, 0x00, 0x01],
2901        };
2902        assert!(matches!(
2903            validate_key_transport_recipient(&key, &crate::policy::EncryptionPolicy::default()),
2904            Err(XmlEncError::Policy(
2905                crate::policy::PolicyViolation::KeySize { .. }
2906            ))
2907        ));
2908    }
2909
2910    #[test]
2911    fn encryption_policy_rejects_weak_rsa_recipient_before_provider_dispatch() {
2912        // Provider capability cannot weaken the outbound recipient-key policy.
2913        let private_key = RsaPrivateKey::new(&mut UnwrapErr(SysRng), 1024)
2914            .expect("test RSA key generation should succeed");
2915        let provider = Arc::new(OverridingOutputProvider {
2916            ciphertext: None,
2917            wrapped_key: None,
2918            transported_key: Some(vec![0_u8; 128]),
2919            transport_calls: AtomicUsize::new(0),
2920        });
2921
2922        let result = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2923            .provider(provider.clone())
2924            .recipient_rsa_oaep(RsaPublicKey::from(&private_key))
2925            .encrypt_binary(b"data");
2926
2927        assert!(matches!(
2928            result,
2929            Err(XmlEncError::Policy(
2930                crate::policy::PolicyViolation::KeySize {
2931                    operation: "encryption",
2932                    minimum_bits: 2048,
2933                    maximum_bits: 8192,
2934                    actual_bits: 1024,
2935                    ..
2936                }
2937            ))
2938        ));
2939        assert_eq!(provider.transport_calls.load(Ordering::Relaxed), 0);
2940    }
2941
2942    #[test]
2943    fn debug_output_redacts_symmetric_key_material() {
2944        let direct_key = b"direct-key-secret".to_vec();
2945        let kek = b"key-wrap-secret!".to_vec();
2946        let builder = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
2947            .direct_key(direct_key.clone());
2948        let recipient = EncryptionRecipient::aes_key_wrap(kek.clone(), KeyWrapAlgorithm::AesKw128);
2949
2950        let builder_debug = format!("{builder:?}");
2951        let recipient_debug = format!("{recipient:?}");
2952        assert!(builder_debug.contains("[REDACTED]"));
2953        assert!(recipient_debug.contains("[REDACTED]"));
2954        assert!(!builder_debug.contains(&format!("{direct_key:?}")));
2955        assert!(!recipient_debug.contains(&format!("{kek:?}")));
2956    }
2957}