Skip to main content

xml_sec/xmlenc/
encrypt.rs

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