Skip to main content

xml_sec/xmlenc/
encrypt.rs

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