Skip to main content

xml_sec/xmlenc/
encrypt.rs

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