Skip to main content

xml_sec/xmlenc/
types.rs

1//! Public XMLEnc data structures and errors.
2
3use std::{fmt, sync::Arc};
4
5use rsa::RsaPublicKey;
6
7/// XML Encryption 1.0 namespace.
8pub const XMLENC_NS: &str = "http://www.w3.org/2001/04/xmlenc#";
9/// XML Encryption 1.1 namespace.
10pub const XMLENC11_NS: &str = "http://www.w3.org/2009/xmlenc11#";
11/// XML Signature namespace, used by OAEP parameter elements.
12pub const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
13
14/// Maximum normalized base64 text accepted from a `CipherValue`.
15pub const MAX_CIPHER_VALUE_BASE64_LEN: usize =
16    crate::hard_limits::ENCRYPTION_CIPHER_VALUE_BASE64_BYTE_CEILING;
17/// The `Type` attribute on an `EncryptedData` element.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum EncryptedDataType {
20    /// The plaintext contains one complete XML element.
21    Element,
22    /// The plaintext contains the encrypted element's child content.
23    Content,
24    /// An application-defined or empty type hint whose plaintext remains opaque.
25    Other(String),
26}
27
28/// Supported content-encryption algorithms.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum DataEncryptionAlgorithm {
31    /// Legacy three-key Triple DES CBC, available only with explicit policy permission.
32    #[cfg(feature = "legacy-algorithms")]
33    TripleDesCbc,
34    /// AES-192 CBC compatibility capability.
35    #[cfg(feature = "legacy-algorithms")]
36    Aes192Cbc,
37    /// AES-192 GCM compatibility capability.
38    #[cfg(feature = "legacy-algorithms")]
39    Aes192Gcm,
40    /// AES-128 in CBC mode with XMLEnc padding.
41    Aes128Cbc,
42    /// AES-256 in CBC mode with XMLEnc padding.
43    Aes256Cbc,
44    /// AES-128 in GCM mode.
45    Aes128Gcm,
46    /// AES-256 in GCM mode.
47    Aes256Gcm,
48    /// Camellia CBC with a 128-bit key (RFC 9231 §2.6.2).
49    Camellia128Cbc,
50    /// Camellia CBC with a 192-bit key.
51    Camellia192Cbc,
52    /// Camellia CBC with a 256-bit key.
53    Camellia256Cbc,
54    /// RFC 9231 ChaCha20 profile; unauthenticated and opt-in.
55    ChaCha20,
56    /// ChaCha20-Poly1305 with XML-carried nonce and optional AAD.
57    ChaCha20Poly1305,
58}
59
60impl DataEncryptionAlgorithm {
61    /// Whether successful decryption cryptographically authenticates the key.
62    pub const fn is_authenticated(self) -> bool {
63        // RFC 9231 §§2.6.7-2.6.8 distinguishes the stream cipher from AEAD.
64        // https://www.rfc-editor.org/rfc/rfc9231.html#section-2.6.8
65        match self {
66            Self::Aes128Gcm | Self::Aes256Gcm | Self::ChaCha20Poly1305 => true,
67            #[cfg(feature = "legacy-algorithms")]
68            Self::Aes192Gcm => true,
69            Self::Aes128Cbc
70            | Self::Aes256Cbc
71            | Self::Camellia128Cbc
72            | Self::Camellia192Cbc
73            | Self::Camellia256Cbc
74            | Self::ChaCha20 => false,
75            #[cfg(feature = "legacy-algorithms")]
76            Self::TripleDesCbc | Self::Aes192Cbc => false,
77        }
78    }
79
80    /// Key family required independently of the byte length (AES-192 and
81    /// three-key Triple DES both use 24 bytes).
82    pub const fn key_kind(self) -> crate::key_manager::SymmetricKeyKind {
83        if matches!(self, Self::ChaCha20 | Self::ChaCha20Poly1305) {
84            return crate::key_manager::SymmetricKeyKind::ChaCha20;
85        }
86        if matches!(
87            self,
88            Self::Camellia128Cbc | Self::Camellia192Cbc | Self::Camellia256Cbc
89        ) {
90            return crate::key_manager::SymmetricKeyKind::Camellia;
91        }
92        #[cfg(feature = "legacy-algorithms")]
93        if matches!(self, Self::TripleDesCbc) {
94            return crate::key_manager::SymmetricKeyKind::Des;
95        }
96        crate::key_manager::SymmetricKeyKind::Aes
97    }
98    /// Parse a supported XMLEnc content-encryption URI.
99    pub fn from_uri(uri: &str) -> Result<Self, XmlEncError> {
100        match uri {
101            #[cfg(feature = "legacy-algorithms")]
102            "http://www.w3.org/2001/04/xmlenc#tripledes-cbc" => Ok(Self::TripleDesCbc),
103            #[cfg(feature = "legacy-algorithms")]
104            "http://www.w3.org/2001/04/xmlenc#aes192-cbc" => Ok(Self::Aes192Cbc),
105            #[cfg(feature = "legacy-algorithms")]
106            "http://www.w3.org/2009/xmlenc11#aes192-gcm" => Ok(Self::Aes192Gcm),
107            "http://www.w3.org/2001/04/xmlenc#aes128-cbc" => Ok(Self::Aes128Cbc),
108            "http://www.w3.org/2001/04/xmlenc#aes256-cbc" => Ok(Self::Aes256Cbc),
109            "http://www.w3.org/2009/xmlenc11#aes128-gcm" => Ok(Self::Aes128Gcm),
110            "http://www.w3.org/2009/xmlenc11#aes256-gcm" => Ok(Self::Aes256Gcm),
111            "http://www.w3.org/2001/04/xmldsig-more#camellia128-cbc" => Ok(Self::Camellia128Cbc),
112            "http://www.w3.org/2001/04/xmldsig-more#camellia192-cbc" => Ok(Self::Camellia192Cbc),
113            "http://www.w3.org/2001/04/xmldsig-more#camellia256-cbc" => Ok(Self::Camellia256Cbc),
114            "http://www.w3.org/2021/04/xmldsig-more#chacha20" => Ok(Self::ChaCha20),
115            "http://www.w3.org/2021/04/xmldsig-more#chacha20poly1305" => Ok(Self::ChaCha20Poly1305),
116            _ => Err(XmlEncError::UnsupportedAlgorithm(uri.to_owned())),
117        }
118    }
119
120    /// Required symmetric key length in bytes.
121    pub const fn key_len(self) -> usize {
122        match self {
123            #[cfg(feature = "legacy-algorithms")]
124            Self::TripleDesCbc | Self::Aes192Cbc | Self::Aes192Gcm => 24,
125            Self::Aes128Cbc | Self::Aes128Gcm => 16,
126            Self::Aes256Cbc | Self::Aes256Gcm => 32,
127            Self::Camellia128Cbc => 16,
128            Self::Camellia192Cbc => 24,
129            Self::Camellia256Cbc => 32,
130            Self::ChaCha20 | Self::ChaCha20Poly1305 => 32,
131        }
132    }
133
134    /// Return the standard XMLEnc algorithm URI.
135    pub const fn uri(self) -> &'static str {
136        match self {
137            #[cfg(feature = "legacy-algorithms")]
138            Self::TripleDesCbc => "http://www.w3.org/2001/04/xmlenc#tripledes-cbc",
139            #[cfg(feature = "legacy-algorithms")]
140            Self::Aes192Cbc => "http://www.w3.org/2001/04/xmlenc#aes192-cbc",
141            #[cfg(feature = "legacy-algorithms")]
142            Self::Aes192Gcm => "http://www.w3.org/2009/xmlenc11#aes192-gcm",
143            Self::Aes128Cbc => "http://www.w3.org/2001/04/xmlenc#aes128-cbc",
144            Self::Aes256Cbc => "http://www.w3.org/2001/04/xmlenc#aes256-cbc",
145            Self::Aes128Gcm => "http://www.w3.org/2009/xmlenc11#aes128-gcm",
146            Self::Aes256Gcm => "http://www.w3.org/2009/xmlenc11#aes256-gcm",
147            Self::Camellia128Cbc => "http://www.w3.org/2001/04/xmldsig-more#camellia128-cbc",
148            Self::Camellia192Cbc => "http://www.w3.org/2001/04/xmldsig-more#camellia192-cbc",
149            Self::Camellia256Cbc => "http://www.w3.org/2001/04/xmldsig-more#camellia256-cbc",
150            Self::ChaCha20 => "http://www.w3.org/2021/04/xmldsig-more#chacha20",
151            Self::ChaCha20Poly1305 => "http://www.w3.org/2021/04/xmldsig-more#chacha20poly1305",
152        }
153    }
154
155    /// Minimum standard wire length for ciphertext produced by this algorithm.
156    pub(crate) const fn minimum_ciphertext_len(self) -> usize {
157        if matches!(self, Self::ChaCha20) {
158            return 0;
159        }
160        if matches!(self, Self::ChaCha20Poly1305) {
161            return 16;
162        }
163        match self.cbc_block_len() {
164            Some(block) => block * 2,
165            None => 28,
166        }
167    }
168
169    /// Compile-time capability is not the operation's permission.
170    /// Whether this capability must be explicitly selected in the operation allowlist.
171    pub const fn requires_explicit_permission(self) -> bool {
172        match self {
173            Self::Camellia128Cbc | Self::Camellia192Cbc | Self::Camellia256Cbc => true,
174            Self::ChaCha20 | Self::ChaCha20Poly1305 => true,
175            #[cfg(feature = "legacy-algorithms")]
176            Self::TripleDesCbc | Self::Aes192Cbc | Self::Aes192Gcm => true,
177            _ => false,
178        }
179    }
180
181    pub(crate) const fn cbc_block_len(self) -> Option<usize> {
182        // XMLEnc 1.1 §§5.2.2-5.2.4: CBC frames prefix one cipher block;
183        // GCM instead prefixes a 96-bit nonce and appends a 128-bit tag.
184        // https://www.w3.org/TR/xmlenc-core1/#sec-Block-Encryption
185        match self {
186            Self::Aes128Cbc | Self::Aes256Cbc => Some(16),
187            Self::Camellia128Cbc | Self::Camellia192Cbc | Self::Camellia256Cbc => Some(16),
188            #[cfg(feature = "legacy-algorithms")]
189            Self::Aes192Cbc => Some(16),
190            #[cfg(feature = "legacy-algorithms")]
191            Self::TripleDesCbc => Some(8),
192            _ => None,
193        }
194    }
195
196    /// Exact wire length produced when encrypting the given plaintext length.
197    pub(crate) fn ciphertext_len_for_plaintext(self, plaintext_len: usize) -> Option<usize> {
198        if matches!(self, Self::ChaCha20) {
199            return Some(plaintext_len);
200        }
201        if matches!(self, Self::ChaCha20Poly1305) {
202            return plaintext_len.checked_add(16);
203        }
204        match self.cbc_block_len() {
205            Some(block) => (plaintext_len / block)
206                .checked_add(1)?
207                .checked_mul(block)?
208                .checked_add(block),
209            None => plaintext_len.checked_add(28),
210        }
211    }
212}
213
214pub(crate) fn validate_ciphertext_framing(
215    algorithm: DataEncryptionAlgorithm,
216    ciphertext_len: usize,
217) -> Result<(), XmlEncError> {
218    let minimum = algorithm.minimum_ciphertext_len();
219    if ciphertext_len < minimum {
220        let algorithm_name = match algorithm {
221            DataEncryptionAlgorithm::ChaCha20 => "ChaCha20",
222            DataEncryptionAlgorithm::ChaCha20Poly1305 => "ChaCha20-Poly1305",
223            DataEncryptionAlgorithm::Camellia128Cbc
224            | DataEncryptionAlgorithm::Camellia192Cbc
225            | DataEncryptionAlgorithm::Camellia256Cbc => "Camellia-CBC",
226            _ => match algorithm.cbc_block_len() {
227                Some(8) => "Triple DES CBC",
228                Some(_) => "AES-CBC",
229                None => "AES-GCM",
230            },
231        };
232        return Err(XmlEncError::DataTooShort {
233            algorithm: algorithm_name,
234            minimum,
235            actual: ciphertext_len,
236        });
237    }
238    if let Some(block) = algorithm.cbc_block_len()
239        && !(ciphertext_len - block).is_multiple_of(block)
240    {
241        return Err(XmlEncError::InvalidCbcCiphertextLength {
242            algorithm,
243            block,
244            actual: ciphertext_len - block,
245        });
246    }
247    Ok(())
248}
249
250impl KeyTransportAlgorithm {
251    /// Historical transport is never enabled merely by compiling its primitive.
252    pub const fn requires_explicit_permission(self) -> bool {
253        match self {
254            #[cfg(feature = "legacy-algorithms")]
255            Self::RsaPkcs1v15 => true,
256            _ => false,
257        }
258    }
259    /// Parse a supported XMLEnc key-transport URI.
260    pub fn from_uri(uri: &str) -> Result<Self, XmlEncError> {
261        match uri {
262            "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" => Ok(Self::RsaOaepMgf1p),
263            "http://www.w3.org/2009/xmlenc11#rsa-oaep" => Ok(Self::RsaOaep11),
264            #[cfg(feature = "legacy-algorithms")]
265            "http://www.w3.org/2001/04/xmlenc#rsa-1_5" => Ok(Self::RsaPkcs1v15),
266            _ => Err(XmlEncError::UnsupportedAlgorithm(uri.to_owned())),
267        }
268    }
269
270    /// Return the standard XMLEnc key-transport URI.
271    pub const fn uri(self) -> &'static str {
272        match self {
273            Self::RsaOaepMgf1p => "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p",
274            Self::RsaOaep11 => "http://www.w3.org/2009/xmlenc11#rsa-oaep",
275            #[cfg(feature = "legacy-algorithms")]
276            Self::RsaPkcs1v15 => "http://www.w3.org/2001/04/xmlenc#rsa-1_5",
277        }
278    }
279}
280
281impl KeyWrapAlgorithm {
282    /// Family of the wrapping key, not of the wrapped content key.
283    pub const fn key_kind(self) -> crate::key_manager::SymmetricKeyKind {
284        if let Self::Cbc(algorithm) = self {
285            return algorithm.key_kind();
286        }
287        if matches!(
288            self,
289            Self::CamelliaKw128 | Self::CamelliaKw192 | Self::CamelliaKw256
290        ) {
291            return crate::key_manager::SymmetricKeyKind::Camellia;
292        }
293        #[cfg(feature = "legacy-algorithms")]
294        if matches!(self, Self::TripleDes) {
295            return crate::key_manager::SymmetricKeyKind::Des;
296        }
297        crate::key_manager::SymmetricKeyKind::Aes
298    }
299    /// Parse a supported XMLEnc symmetric key-wrap URI.
300    pub fn from_uri(uri: &str) -> Result<Self, XmlEncError> {
301        match uri {
302            #[cfg(feature = "legacy-algorithms")]
303            "http://www.w3.org/2001/04/xmlenc#kw-aes192" => Ok(Self::AesKw192),
304            #[cfg(feature = "legacy-algorithms")]
305            "http://www.w3.org/2001/04/xmlenc#kw-tripledes" => Ok(Self::TripleDes),
306            "http://www.w3.org/2001/04/xmlenc#kw-aes128" => Ok(Self::AesKw128),
307            "http://www.w3.org/2001/04/xmlenc#kw-aes256" => Ok(Self::AesKw256),
308            "http://www.w3.org/2001/04/xmldsig-more#kw-camellia128" => Ok(Self::CamelliaKw128),
309            "http://www.w3.org/2001/04/xmldsig-more#kw-camellia192" => Ok(Self::CamelliaKw192),
310            "http://www.w3.org/2001/04/xmldsig-more#kw-camellia256" => Ok(Self::CamelliaKw256),
311            _ => DataEncryptionAlgorithm::from_uri(uri)
312                .ok()
313                .filter(|algorithm| algorithm.cbc_block_len().is_some())
314                .map(Self::Cbc)
315                .ok_or_else(|| XmlEncError::UnsupportedAlgorithm(uri.to_owned())),
316        }
317    }
318
319    /// Required key-encryption-key length in bytes.
320    pub const fn key_len(self) -> usize {
321        match self {
322            #[cfg(feature = "legacy-algorithms")]
323            Self::AesKw192 | Self::TripleDes => 24,
324            Self::AesKw128 => 16,
325            Self::AesKw256 => 32,
326            Self::CamelliaKw128 => 16,
327            Self::CamelliaKw192 => 24,
328            Self::CamelliaKw256 => 32,
329            Self::Cbc(algorithm) => algorithm.key_len(),
330        }
331    }
332
333    /// Return the standard XMLEnc key-wrap URI.
334    pub const fn uri(self) -> &'static str {
335        match self {
336            #[cfg(feature = "legacy-algorithms")]
337            Self::AesKw192 => "http://www.w3.org/2001/04/xmlenc#kw-aes192",
338            #[cfg(feature = "legacy-algorithms")]
339            Self::TripleDes => "http://www.w3.org/2001/04/xmlenc#kw-tripledes",
340            Self::AesKw128 => "http://www.w3.org/2001/04/xmlenc#kw-aes128",
341            Self::AesKw256 => "http://www.w3.org/2001/04/xmlenc#kw-aes256",
342            Self::CamelliaKw128 => "http://www.w3.org/2001/04/xmldsig-more#kw-camellia128",
343            Self::CamelliaKw192 => "http://www.w3.org/2001/04/xmldsig-more#kw-camellia192",
344            Self::CamelliaKw256 => "http://www.w3.org/2001/04/xmldsig-more#kw-camellia256",
345            Self::Cbc(algorithm) => algorithm.uri(),
346        }
347    }
348
349    /// Whether use requires an explicit compiled-policy allowlist entry.
350    pub const fn requires_explicit_permission(self) -> bool {
351        match self {
352            Self::CamelliaKw128 | Self::CamelliaKw192 | Self::CamelliaKw256 => true,
353            Self::Cbc(_) => true,
354            #[cfg(feature = "legacy-algorithms")]
355            Self::AesKw192 | Self::TripleDes => true,
356            _ => false,
357        }
358    }
359
360    pub(crate) fn wrapped_len(self, plaintext_len: usize) -> Option<usize> {
361        // XMLEnc 1.1 §5.7.1 / RFC 3217 §§2-3: CMS wrapping includes
362        // an 8-byte checksum and an 8-byte IV, unlike RFC 3394's A register.
363        // https://www.w3.org/TR/xmlenc-core1/#sec-CMS-3DES
364        let overhead = match self {
365            Self::Cbc(algorithm) => {
366                return if algorithm.cbc_block_len().is_some() {
367                    algorithm.ciphertext_len_for_plaintext(plaintext_len)
368                } else {
369                    None
370                };
371            }
372            #[cfg(feature = "legacy-algorithms")]
373            Self::TripleDes => 16,
374            _ => 8,
375        };
376        plaintext_len.checked_add(overhead)
377    }
378}
379
380/// Supported asymmetric session-key transport algorithms.
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
382pub enum KeyTransportAlgorithm {
383    /// Historical RSAES-PKCS1-v1_5 transport; explicit policy permission is required.
384    #[cfg(feature = "legacy-algorithms")]
385    RsaPkcs1v15,
386    /// XML Encryption 1.0 OAEP URI with SHA-1/MGF1-SHA1 absent-field defaults.
387    ///
388    /// An explicit DigestMethod or XMLEnc 1.1 MGF child overrides the corresponding default,
389    /// matching libxmlsec1 1.3.13 behavior.
390    RsaOaepMgf1p,
391    /// XML Encryption 1.1 OAEP with explicitly parsed digest and MGF settings.
392    RsaOaep11,
393}
394
395/// Supported symmetric key-wrap algorithms.
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
397pub enum KeyWrapAlgorithm {
398    /// CBC encryption of key bytes. Unlike RFC 3394 this is not authenticated;
399    /// permission must name this exact algorithm. Only CBC ciphers are valid.
400    /// XMLEnc 1.1 §3.4 permits EncryptedKey's inherited EncryptionMethod.
401    Cbc(DataEncryptionAlgorithm),
402    /// AES-192 RFC 3394 key wrap, explicitly permitted by compatibility policy.
403    #[cfg(feature = "legacy-algorithms")]
404    AesKw192,
405    /// CMS Triple DES key wrap, including XMLEnc's optional non-DES key inputs.
406    #[cfg(feature = "legacy-algorithms")]
407    TripleDes,
408    /// RFC 3394 AES key wrap with a 128-bit KEK.
409    AesKw128,
410    /// RFC 3394 AES key wrap with a 256-bit KEK.
411    AesKw256,
412    /// RFC 9231 §2.6.3 Camellia wrapping with a 128-bit KEK.
413    CamelliaKw128,
414    /// Camellia wrapping with a 192-bit KEK.
415    CamelliaKw192,
416    /// Camellia wrapping with a 256-bit KEK.
417    CamelliaKw256,
418}
419
420/// Digest algorithms accepted by RSA-OAEP encryption.
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
422pub enum OaepDigestAlgorithm {
423    /// MD5 compatibility digest; requires explicit legacy permission.
424    #[cfg(feature = "legacy-algorithms")]
425    Md5,
426    /// RIPEMD-160 compatibility digest; requires explicit legacy permission.
427    #[cfg(feature = "legacy-algorithms")]
428    Ripemd160,
429    /// SHA-1, retained for legacy XMLEnc OAEP interoperability.
430    Sha1,
431    /// SHA-224, including the XML Encryption 1.1 MGF1-SHA224 identifier.
432    Sha224,
433    /// SHA-256.
434    Sha256,
435    /// SHA-384.
436    Sha384,
437    /// SHA-512.
438    Sha512,
439    /// SHA3-224.
440    Sha3_224,
441    /// SHA3-256.
442    Sha3_256,
443    /// SHA3-384.
444    Sha3_384,
445    /// SHA3-512.
446    Sha3_512,
447}
448
449impl OaepDigestAlgorithm {
450    /// Parse a digest URI accepted by XML Encryption and libxmlsec1.
451    pub fn from_uri(uri: &str) -> Option<Self> {
452        match uri {
453            #[cfg(feature = "legacy-algorithms")]
454            "http://www.w3.org/2001/04/xmldsig-more#md5" => Some(Self::Md5),
455            #[cfg(feature = "legacy-algorithms")]
456            "http://www.w3.org/2001/04/xmlenc#ripemd160" => Some(Self::Ripemd160),
457            "http://www.w3.org/2000/09/xmldsig#sha1" => Some(Self::Sha1),
458            "http://www.w3.org/2001/04/xmldsig-more#sha224" => Some(Self::Sha224),
459            "http://www.w3.org/2001/04/xmlenc#sha256" => Some(Self::Sha256),
460            "http://www.w3.org/2001/04/xmlenc#sha384"
461            | "http://www.w3.org/2001/04/xmldsig-more#sha384" => Some(Self::Sha384),
462            "http://www.w3.org/2001/04/xmlenc#sha512" => Some(Self::Sha512),
463            "http://www.w3.org/2007/05/xmldsig-more#sha3-224" => Some(Self::Sha3_224),
464            "http://www.w3.org/2007/05/xmldsig-more#sha3-256" => Some(Self::Sha3_256),
465            "http://www.w3.org/2007/05/xmldsig-more#sha3-384" => Some(Self::Sha3_384),
466            "http://www.w3.org/2007/05/xmldsig-more#sha3-512" => Some(Self::Sha3_512),
467            _ => None,
468        }
469    }
470
471    /// Parse an XML Encryption 1.1 MGF1 URI.
472    pub fn from_mgf_uri(uri: &str) -> Option<Self> {
473        match uri {
474            "http://www.w3.org/2009/xmlenc11#mgf1sha1" => Some(Self::Sha1),
475            "http://www.w3.org/2009/xmlenc11#mgf1sha224" => Some(Self::Sha224),
476            "http://www.w3.org/2009/xmlenc11#mgf1sha256" => Some(Self::Sha256),
477            "http://www.w3.org/2009/xmlenc11#mgf1sha384" => Some(Self::Sha384),
478            "http://www.w3.org/2009/xmlenc11#mgf1sha512" => Some(Self::Sha512),
479            _ => None,
480        }
481    }
482
483    /// Return the standard digest URI.
484    pub const fn uri(self) -> &'static str {
485        match self {
486            #[cfg(feature = "legacy-algorithms")]
487            Self::Md5 => "http://www.w3.org/2001/04/xmldsig-more#md5",
488            #[cfg(feature = "legacy-algorithms")]
489            Self::Ripemd160 => "http://www.w3.org/2001/04/xmlenc#ripemd160",
490            Self::Sha1 => "http://www.w3.org/2000/09/xmldsig#sha1",
491            Self::Sha224 => "http://www.w3.org/2001/04/xmldsig-more#sha224",
492            Self::Sha256 => "http://www.w3.org/2001/04/xmlenc#sha256",
493            Self::Sha384 => "http://www.w3.org/2001/04/xmlenc#sha384",
494            Self::Sha512 => "http://www.w3.org/2001/04/xmlenc#sha512",
495            Self::Sha3_224 => "http://www.w3.org/2007/05/xmldsig-more#sha3-224",
496            Self::Sha3_256 => "http://www.w3.org/2007/05/xmldsig-more#sha3-256",
497            Self::Sha3_384 => "http://www.w3.org/2007/05/xmldsig-more#sha3-384",
498            Self::Sha3_512 => "http://www.w3.org/2007/05/xmldsig-more#sha3-512",
499        }
500    }
501
502    /// Return an XML Encryption 1.1 MGF URI, if one exists for this digest.
503    /// DigestMethod support does not invent additional wire MGF identifiers.
504    pub const fn mgf_uri(self) -> Option<&'static str> {
505        // XML Encryption 1.1 §5.5.2 defines five MGF1 URIs. RFC 8017's
506        // arbitrary Hash parameter does not register further XML identifiers.
507        // https://www.w3.org/TR/xmlenc-core1/#sec-RSA-OAEP
508        match self {
509            Self::Sha1 => Some("http://www.w3.org/2009/xmlenc11#mgf1sha1"),
510            Self::Sha224 => Some("http://www.w3.org/2009/xmlenc11#mgf1sha224"),
511            Self::Sha256 => Some("http://www.w3.org/2009/xmlenc11#mgf1sha256"),
512            Self::Sha384 => Some("http://www.w3.org/2009/xmlenc11#mgf1sha384"),
513            Self::Sha512 => Some("http://www.w3.org/2009/xmlenc11#mgf1sha512"),
514            _ => None,
515        }
516    }
517
518    /// Compatibility digest capability never implicitly permits its use.
519    pub const fn requires_explicit_permission(self) -> bool {
520        match self {
521            #[cfg(feature = "legacy-algorithms")]
522            Self::Md5 | Self::Ripemd160 => true,
523            _ => false,
524        }
525    }
526}
527
528#[cfg(test)]
529mod tests {
530    use super::{OaepDigestAlgorithm, XmlEncError};
531
532    #[test]
533    fn operation_plan_failures_have_a_distinct_error_class() {
534        let error = XmlEncError::from(crate::operation::OperationPlanError::StaleResourceIdentity);
535        assert!(matches!(error, XmlEncError::OperationPlan(_)));
536    }
537
538    #[test]
539    fn oaep_sha384_accepts_both_interoperable_digest_uris() {
540        // The canonical XML Encryption spelling and libxmlsec1's XMLDSig-more
541        // spelling identify the same OAEP digest algorithm.
542        for uri in [
543            "http://www.w3.org/2001/04/xmlenc#sha384",
544            "http://www.w3.org/2001/04/xmldsig-more#sha384",
545        ] {
546            assert_eq!(
547                OaepDigestAlgorithm::from_uri(uri),
548                Some(OaepDigestAlgorithm::Sha384)
549            );
550        }
551    }
552
553    #[test]
554    fn oaep_mgf_uris_round_trip() {
555        for algorithm in [
556            OaepDigestAlgorithm::Sha1,
557            OaepDigestAlgorithm::Sha224,
558            OaepDigestAlgorithm::Sha256,
559            OaepDigestAlgorithm::Sha384,
560            OaepDigestAlgorithm::Sha512,
561        ] {
562            assert_eq!(
563                OaepDigestAlgorithm::from_mgf_uri(
564                    algorithm.mgf_uri().expect("standard MGF1 digest URI")
565                ),
566                Some(algorithm)
567            );
568        }
569        assert_eq!(
570            OaepDigestAlgorithm::from_mgf_uri("urn:unsupported-mgf"),
571            None
572        );
573    }
574}
575
576/// RSA-OAEP parameters emitted in an `EncryptedKey`.
577#[derive(Debug, Clone, PartialEq, Eq)]
578pub struct RsaOaepParameters {
579    /// XMLEnc 1.0 legacy OAEP or XMLEnc 1.1 configurable OAEP.
580    pub algorithm: KeyTransportAlgorithm,
581    /// Digest used by OAEP.
582    pub digest: OaepDigestAlgorithm,
583    /// Digest used by MGF1.
584    pub mgf_digest: OaepDigestAlgorithm,
585    /// Optional OAEP label bytes.
586    pub label: Vec<u8>,
587}
588
589impl RsaOaepParameters {
590    /// Create legacy OAEP parameters with SHA-1 and MGF1-SHA-1.
591    pub fn legacy() -> Self {
592        Self {
593            algorithm: KeyTransportAlgorithm::RsaOaepMgf1p,
594            digest: OaepDigestAlgorithm::Sha1,
595            mgf_digest: OaepDigestAlgorithm::Sha1,
596            label: Vec::new(),
597        }
598    }
599
600    /// Create XMLEnc 1.1 OAEP parameters.
601    pub fn xmlenc11(digest: OaepDigestAlgorithm, mgf_digest: OaepDigestAlgorithm) -> Self {
602        Self {
603            algorithm: KeyTransportAlgorithm::RsaOaep11,
604            digest,
605            mgf_digest,
606            label: Vec::new(),
607        }
608    }
609
610    /// Set the OAEP label bytes.
611    pub fn label(mut self, label: impl Into<Vec<u8>>) -> Self {
612        self.label = label.into();
613        self
614    }
615}
616
617impl Default for RsaOaepParameters {
618    fn default() -> Self {
619        Self::xmlenc11(OaepDigestAlgorithm::Sha256, OaepDigestAlgorithm::Sha256)
620    }
621}
622
623/// One recipient of a generated content-encryption key.
624#[derive(Clone)]
625pub enum EncryptionRecipient {
626    /// Historical RSA transport, admitted only by explicit compiled policy.
627    #[cfg(feature = "legacy-algorithms")]
628    RsaPkcs1v15 {
629        /// Opaque RSA public key used by the selected provider.
630        public_key: Arc<dyn crate::provider::KeyTransportKey>,
631        /// Optional recipient identifier.
632        recipient: Option<String>,
633        /// Optional key hint.
634        key_name: Option<String>,
635    },
636    /// Wrap the content key with an RSA public key and OAEP.
637    RsaOaep {
638        /// Opaque recipient public-key handle.
639        public_key: Arc<dyn crate::provider::KeyTransportKey>,
640        /// OAEP algorithm parameters.
641        parameters: RsaOaepParameters,
642        /// Optional `Recipient` attribute.
643        recipient: Option<String>,
644        /// Optional key hint inside the encrypted key's `KeyInfo`.
645        key_name: Option<String>,
646    },
647    /// Wrap the content key with a pre-shared AES KEK.
648    AesKeyWrap {
649        /// AES key-encryption key, zeroized on drop (including cloned owners).
650        kek: Vec<u8>,
651        /// RFC 3394 key-wrap variant.
652        algorithm: KeyWrapAlgorithm,
653        /// Optional `Recipient` attribute.
654        recipient: Option<String>,
655        /// Optional key hint inside the encrypted key's `KeyInfo`.
656        key_name: Option<String>,
657    },
658}
659
660impl zeroize::Zeroize for EncryptionRecipient {
661    fn zeroize(&mut self) {
662        if let Self::AesKeyWrap { kek, .. } = self {
663            kek.zeroize();
664        }
665    }
666}
667
668impl zeroize::ZeroizeOnDrop for EncryptionRecipient {}
669
670impl Drop for EncryptionRecipient {
671    fn drop(&mut self) {
672        zeroize::Zeroize::zeroize(self);
673    }
674}
675
676impl fmt::Debug for EncryptionRecipient {
677    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
678        match self {
679            #[cfg(feature = "legacy-algorithms")]
680            Self::RsaPkcs1v15 {
681                recipient,
682                key_name,
683                ..
684            } => formatter
685                .debug_struct("EncryptionRecipient::RsaPkcs1v15")
686                .field("public_key", &"[PUBLIC KEY]")
687                .field("recipient", recipient)
688                .field("key_name", key_name)
689                .finish(),
690            Self::RsaOaep {
691                parameters,
692                recipient,
693                key_name,
694                ..
695            } => formatter
696                .debug_struct("EncryptionRecipient::RsaOaep")
697                .field("public_key", &"[PUBLIC KEY]")
698                .field("parameters", parameters)
699                .field("recipient", recipient)
700                .field("key_name", key_name)
701                .finish(),
702            Self::AesKeyWrap {
703                algorithm,
704                recipient,
705                key_name,
706                ..
707            } => formatter
708                .debug_struct("EncryptionRecipient::AesKeyWrap")
709                .field("kek", &"[REDACTED]")
710                .field("algorithm", algorithm)
711                .field("recipient", recipient)
712                .field("key_name", key_name)
713                .finish(),
714        }
715    }
716}
717
718impl EncryptionRecipient {
719    /// Create a historical RSAES-PKCS1-v1_5 recipient. Defaults still deny it.
720    #[cfg(feature = "legacy-algorithms")]
721    pub fn rsa_pkcs1v15(public_key: RsaPublicKey) -> Self {
722        Self::provider_pkcs1v15(Arc::new(crate::provider::RustCryptoRsaPublicKey::new(
723            public_key,
724        )))
725    }
726
727    /// Create a historical RSA recipient without extracting provider-owned keys.
728    #[cfg(feature = "legacy-algorithms")]
729    pub fn provider_pkcs1v15(public_key: Arc<dyn crate::provider::KeyTransportKey>) -> Self {
730        Self::RsaPkcs1v15 {
731            public_key,
732            recipient: None,
733            key_name: None,
734        }
735    }
736    /// Create an RSA-OAEP recipient using SHA-256 and MGF1-SHA-256.
737    ///
738    /// XMLEnc 1.1 assigns SHA-1 and MGF1-SHA-1 when these parameters are
739    /// omitted. Serialized keys therefore include both algorithm values
740    /// explicitly instead of relying on the specification's legacy defaults.
741    pub fn rsa_oaep(public_key: RsaPublicKey) -> Self {
742        Self::provider_key_transport(Arc::new(crate::provider::RustCryptoRsaPublicKey::new(
743            public_key,
744        )))
745    }
746
747    /// Create an RSA-OAEP recipient from an opaque provider key handle.
748    pub fn provider_key_transport(public_key: Arc<dyn crate::provider::KeyTransportKey>) -> Self {
749        Self::RsaOaep {
750            public_key,
751            parameters: RsaOaepParameters::default(),
752            recipient: None,
753            key_name: None,
754        }
755    }
756
757    /// Create an AES Key Wrap recipient.
758    pub fn aes_key_wrap(kek: impl Into<Vec<u8>>, algorithm: KeyWrapAlgorithm) -> Self {
759        Self::AesKeyWrap {
760            kek: kek.into(),
761            algorithm,
762            recipient: None,
763            key_name: None,
764        }
765    }
766
767    /// Override RSA-OAEP parameters.
768    pub fn oaep_parameters(mut self, parameters: RsaOaepParameters) -> Self {
769        if let Self::RsaOaep {
770            parameters: current,
771            ..
772        } = &mut self
773        {
774            *current = parameters;
775        }
776        self
777    }
778
779    /// Set the recipient identifier emitted on `EncryptedKey`.
780    pub fn recipient(mut self, value: impl Into<String>) -> Self {
781        match &mut self {
782            #[cfg(feature = "legacy-algorithms")]
783            Self::RsaPkcs1v15 { recipient, .. } => *recipient = Some(value.into()),
784            Self::RsaOaep { recipient, .. } | Self::AesKeyWrap { recipient, .. } => {
785                *recipient = Some(value.into());
786            }
787        }
788        self
789    }
790
791    /// Set the key name emitted inside the encrypted key's `KeyInfo`.
792    pub fn key_name(mut self, value: impl Into<String>) -> Self {
793        match &mut self {
794            #[cfg(feature = "legacy-algorithms")]
795            Self::RsaPkcs1v15 { key_name, .. } => *key_name = Some(value.into()),
796            Self::RsaOaep { key_name, .. } | Self::AesKeyWrap { key_name, .. } => {
797                *key_name = Some(value.into());
798            }
799        }
800        self
801    }
802}
803
804/// How generated `EncryptedData` replaces caller-owned XML.
805#[derive(Debug, Clone, Copy, PartialEq, Eq)]
806pub enum ReplacementMode {
807    /// Replace the selected element, including its start and end tags.
808    ReplaceElement,
809    /// Replace only the selected element's child content.
810    ReplaceContent,
811}
812
813/// Result returned after encrypting bytes or XML.
814#[derive(Debug, Clone, PartialEq, Eq)]
815pub struct EncryptionResult {
816    /// Complete `EncryptedData` XML fragment.
817    pub encrypted_data_xml: String,
818    /// Required caller-owned document replacement operation.
819    pub replacement: ReplacementMode,
820}
821
822/// Caller-owned target selection for document encryption.
823#[derive(Debug, Clone, Copy, Default)]
824pub struct DocumentEncryptionOptions<'a> {
825    /// Select an element by `Id`, `ID`, or `id`; `None` selects the document root.
826    pub element_id: Option<&'a str>,
827}
828
829/// Parsed `EncryptionMethod` data.
830#[derive(Debug, Clone, PartialEq, Eq)]
831pub struct EncryptionMethod {
832    /// Algorithm URI from the mandatory `Algorithm` attribute.
833    pub algorithm: String,
834    /// Optional explicit key size in bits.
835    pub key_size_bits: Option<usize>,
836    /// Digest URI used by XML Encryption 1.1 OAEP.
837    pub oaep_digest: Option<String>,
838    /// MGF URI used by XML Encryption 1.1 OAEP.
839    pub mgf_algorithm: Option<String>,
840    /// Decoded OAEP label bytes.
841    pub oaep_params: Option<Vec<u8>>,
842    /// ChaCha profile parameters, separate from RSA-OAEP labels.
843    pub chacha: Option<ChaChaParameters>,
844}
845
846/// Request/wire data for the RFC 9231 ChaCha profiles, not policy.
847/// A missing nonce is only allowed in an encryption template; decryption
848/// requires an explicit nonce, and encryption generates it through its provider.
849#[derive(Debug, Clone, Default, PartialEq, Eq)]
850pub struct ChaChaParameters {
851    /// 96-bit nonce encoded as hexBinary in EncryptionMethod.
852    pub nonce: Option<[u8; 12]>,
853    /// Raw ChaCha's four little-endian counter bytes; absent for Poly1305.
854    pub counter: Option<[u8; 4]>,
855    /// Poly1305 additional authenticated data: UTF-8 XML text, not base64.
856    pub aad: Option<String>,
857}
858
859impl ChaChaParameters {
860    /// Borrow metadata for cryptographic dispatch and serialization without
861    /// copying AAD when the operation supplies a generated nonce.
862    pub fn borrowed(&self) -> ChaChaParametersRef<'_> {
863        ChaChaParametersRef {
864            nonce: self.nonce.as_ref(),
865            counter: self.counter,
866            aad: self.aad.as_deref(),
867        }
868    }
869    /// Check the selected profile before any resolver or provider work.
870    pub fn validate(&self, algorithm: DataEncryptionAlgorithm) -> Result<(), XmlEncError> {
871        self.borrowed().validate(algorithm)
872    }
873}
874
875/// Borrowed request parameters; no metadata allocation at provider dispatch.
876#[derive(Debug, Clone, Copy, Default)]
877pub struct ChaChaParametersRef<'a> {
878    /// Explicit or operation-generated nonce.
879    pub nonce: Option<&'a [u8; 12]>,
880    /// Raw ChaCha's encoded initial block counter.
881    pub counter: Option<[u8; 4]>,
882    /// Borrowed UTF-8 additional authenticated data.
883    pub aad: Option<&'a str>,
884}
885
886impl ChaChaParametersRef<'_> {
887    /// Enforce the same profile invariants as owned parsed parameters.
888    pub fn validate(self, algorithm: DataEncryptionAlgorithm) -> Result<(), XmlEncError> {
889        // draft-eastlake-rfc9231bis-xmlsec-uris-06 §§3.6.7–3.6.8 separates
890        // Counter from AAD. These experimental identifiers are not RFC 9231.
891        // https://www.ietf.org/archive/id/draft-eastlake-rfc9231bis-xmlsec-uris-06.html#section-3.6.7
892        let valid = match algorithm {
893            DataEncryptionAlgorithm::ChaCha20 => self.counter.is_some() && self.aad.is_none(),
894            DataEncryptionAlgorithm::ChaCha20Poly1305 => self.counter.is_none(),
895            _ => false,
896        };
897        if !valid {
898            return Err(XmlEncError::InvalidStructure(
899                "invalid ChaCha EncryptionMethod parameters".into(),
900            ));
901        }
902        Ok(())
903    }
904}
905
906impl EncryptionMethod {
907    /// Validate invariants imposed by the selected algorithm URI.
908    ///
909    /// Parsed XML and caller-constructed typed values share this check so the
910    /// public typed API cannot express wire structures that XML parsing rejects.
911    pub(crate) fn validate_structure(&self) -> Result<(), XmlEncError> {
912        if let Some(parameters) = &self.chacha {
913            parameters.validate(DataEncryptionAlgorithm::from_uri(&self.algorithm)?)?;
914        } else if self.algorithm == DataEncryptionAlgorithm::ChaCha20.uri() {
915            return Err(XmlEncError::MissingRequired("ChaCha20 Counter"));
916        }
917        if self.key_size_bits == Some(0) {
918            return Err(XmlEncError::InvalidStructure(
919                "KeySize must be a positive integer".into(),
920            ));
921        }
922        let is_legacy_oaep = self.algorithm == KeyTransportAlgorithm::RsaOaepMgf1p.uri();
923        let is_oaep11 = self.algorithm == KeyTransportAlgorithm::RsaOaep11.uri();
924        if (self.oaep_params.is_some()
925            || self.oaep_digest.is_some()
926            || self.mgf_algorithm.is_some())
927            && !is_legacy_oaep
928            && !is_oaep11
929        {
930            return Err(XmlEncError::InvalidStructure(
931                "OAEP parameters are only valid for RSA-OAEP EncryptionMethod".into(),
932            ));
933        }
934        if let (Some(actual), Some(expected)) =
935            (self.key_size_bits, fixed_aes_key_size(&self.algorithm))
936            && actual != expected
937        {
938            return Err(XmlEncError::InvalidStructure(format!(
939                "EncryptionMethod {} requires KeySize {expected}, got {actual}",
940                self.algorithm
941            )));
942        }
943        Ok(())
944    }
945}
946
947fn fixed_aes_key_size(algorithm: &str) -> Option<usize> {
948    let key_len = DataEncryptionAlgorithm::from_uri(algorithm)
949        .map(DataEncryptionAlgorithm::key_len)
950        .or_else(|_| KeyWrapAlgorithm::from_uri(algorithm).map(KeyWrapAlgorithm::key_len))
951        .ok()?;
952    Some(key_len * 8)
953}
954
955/// Ciphertext storage specified by XMLEnc 1.1 section 3.3.
956#[derive(Debug, Clone, PartialEq, Eq)]
957pub enum CipherData {
958    /// Whitespace-normalized base64 text from `CipherValue`.
959    Value {
960        /// Encoded ciphertext, not plaintext or key material.
961        value: String,
962    },
963    /// A source-anchored reference, resolved before cryptographic dispatch.
964    Reference {
965        /// Required URI, including an explicitly empty URI.
966        uri: String,
967        /// Ordered transforms retained with their original XPath source identity.
968        transforms: Vec<crate::xmldsig::transforms::Transform>,
969    },
970    /// Ciphertext octets resolved by the operation or supplied by the caller.
971    Bytes(Vec<u8>),
972}
973
974impl CipherData {
975    pub(super) fn octets(&self) -> Result<std::borrow::Cow<'_, [u8]>, XmlEncError> {
976        use base64::Engine as _;
977        match self {
978            Self::Value { value } => base64::engine::general_purpose::STANDARD
979                .decode(value)
980                .map(std::borrow::Cow::Owned)
981                .map_err(|error| XmlEncError::Base64(error.to_string())),
982            Self::Bytes(bytes) => Ok(std::borrow::Cow::Borrowed(bytes)),
983            Self::Reference { .. } => Err(XmlEncError::InvalidStructure(
984                "CipherReference requires its original document context".into(),
985            )),
986        }
987    }
988
989    /// Return an inline base64 value, if this ciphertext is enveloped.
990    pub fn inline_value(&self) -> Option<&str> {
991        match self {
992            Self::Value { value } => Some(value),
993            Self::Reference { .. } | Self::Bytes(_) => None,
994        }
995    }
996
997    /// Mutably access an inline value without changing its storage variant.
998    pub fn inline_value_mut(&mut self) -> Option<&mut String> {
999        match self {
1000            Self::Value { value } => Some(value),
1001            Self::Reference { .. } | Self::Bytes(_) => None,
1002        }
1003    }
1004}
1005
1006/// Parsed embedded `EncryptedKey` used to recover a content-encryption key.
1007#[derive(Debug, Clone, PartialEq, Eq)]
1008pub struct EncryptedKey {
1009    /// Sources of the key which protects this transported key. These are not
1010    /// alternative content keys: their output feeds this key's EncryptionMethod.
1011    pub sources: EncryptionKeySources,
1012    /// Optional XML identifier.
1013    pub id: Option<String>,
1014    /// Optional recipient hint.
1015    pub recipient: Option<String>,
1016    /// Optional direct `ds:KeyName` hint from the key's `KeyInfo`.
1017    pub key_name: Option<String>,
1018    /// Method which wrapped the session key.
1019    pub encryption_method: EncryptionMethod,
1020    /// Wrapped session-key bytes in base64 form.
1021    pub cipher_data: CipherData,
1022    /// Optional references identifying data or keys associated with this key.
1023    pub reference_list: Option<ReferenceList>,
1024    /// Optional name associated with the transported plaintext key.
1025    pub carried_key_name: Option<String>,
1026}
1027
1028/// Typed establishment sources inside an encrypted key's `ds:KeyInfo`.
1029/// Recursion is checked against the operation's depth and candidate allowances.
1030#[derive(Debug, Clone, PartialEq, Eq, Default)]
1031pub struct EncryptionKeySources {
1032    /// Experimental KEM sources producing this wrapping key.
1033    pub encapsulation_methods: Vec<crate::key_establishment::EncapsulationMechanism>,
1034    /// Keys transporting the wrapping key, rather than the final content key.
1035    pub encrypted_keys: Vec<EncryptedKey>,
1036    /// Derivations producing the wrapping key.
1037    pub derived_keys: Vec<super::DerivedKey>,
1038    /// Agreements producing the wrapping key.
1039    pub agreement_methods: Vec<super::AgreementMethod>,
1040}
1041
1042impl EncryptionKeySources {
1043    pub(super) fn is_empty(&self) -> bool {
1044        self.encrypted_keys.is_empty()
1045            && self.encapsulation_methods.is_empty()
1046            && self.derived_keys.is_empty()
1047            && self.agreement_methods.is_empty()
1048    }
1049}
1050
1051/// References associated with an `EncryptedKey`.
1052#[derive(Debug, Clone, PartialEq, Eq)]
1053pub struct ReferenceList {
1054    /// URI references to `EncryptedData` elements encrypted with this key.
1055    pub data_references: Vec<String>,
1056    /// URI references to other `EncryptedKey` elements encrypted with this key.
1057    pub key_references: Vec<String>,
1058}
1059
1060/// Parsed `EncryptedData` document fragment.
1061#[derive(Debug, Clone, PartialEq, Eq)]
1062pub struct EncryptedData {
1063    /// Experimental KEM sources; never silently treated as raw content keys.
1064    pub encapsulation_methods: Vec<crate::key_establishment::EncapsulationMechanism>,
1065    /// Optional XML identifier.
1066    pub id: Option<String>,
1067    /// Optional plaintext representation hint.
1068    pub encrypted_type: Option<EncryptedDataType>,
1069    /// Optional direct `ds:KeyName` hint from `KeyInfo`.
1070    pub key_name: Option<String>,
1071    /// Content-encryption method.
1072    pub encryption_method: EncryptionMethod,
1073    /// Embedded recipient session keys in `KeyInfo` document order.
1074    pub encrypted_keys: Vec<EncryptedKey>,
1075    /// Ordered XML derivation candidates. Master material remains request-owned.
1076    pub derived_keys: Vec<super::DerivedKey>,
1077    /// Agreement candidates; trusted private keys remain in request context.
1078    pub agreement_methods: Vec<super::AgreementMethod>,
1079    /// Content ciphertext in base64 form.
1080    pub cipher_data: CipherData,
1081}
1082
1083/// Plaintext returned from XMLEnc decryption.
1084#[derive(Debug, Clone, PartialEq, Eq)]
1085pub enum DecryptedContent {
1086    /// XML plaintext for `Element` and `Content` encrypted data.
1087    Xml(String),
1088    /// Binary plaintext when the encrypted data has no standard XML type hint.
1089    Bytes(Vec<u8>),
1090}
1091
1092/// Errors raised while parsing, encrypting, resolving, or decrypting XMLEnc data.
1093#[derive(Debug, thiserror::Error)]
1094#[non_exhaustive]
1095pub enum XmlEncError {
1096    /// Shared key establishment failed before content encryption/decryption.
1097    #[error(transparent)]
1098    KeyEstablishment(#[from] crate::key_establishment::KeyEstablishmentError),
1099    /// URI resolution or ciphertext transforms failed.
1100    #[error("ciphertext reference error: {0}")]
1101    Transform(#[from] crate::xmldsig::TransformError),
1102    /// The compiled encryption or decryption policy rejected an operation input.
1103    #[error("XML Encryption policy violation: {0}")]
1104    Policy(#[from] crate::policy::PolicyViolation),
1105
1106    /// The selected cryptographic provider rejected or failed an operation.
1107    #[error("cryptographic provider error: {0}")]
1108    Provider(#[from] crate::provider::ProviderError),
1109
1110    /// XML document parsing failed.
1111    #[error("XML parsing error: {0}")]
1112    XmlParse(#[from] crate::xml::dom::ParseError),
1113    /// The owned XML document boundary rejected an identity or mutation.
1114    #[error("XML document error: {0}")]
1115    Document(#[from] crate::document::XmlDocumentError),
1116    /// Required child element or attribute was absent.
1117    #[error("missing required {0}")]
1118    MissingRequired(&'static str),
1119    /// The XML element order or namespace is invalid for the XMLEnc profile.
1120    #[error("invalid encrypted structure: {0}")]
1121    InvalidStructure(String),
1122    /// The compiled operation graph rejected an identity, dependency, or generation invariant.
1123    #[error("invalid XML Encryption operation plan: {0}")]
1124    OperationPlan(String),
1125    /// The selected operation-start node ID is absent or resolves ambiguously.
1126    #[error("selected node ID is missing or ambiguous: {id}")]
1127    SelectedNodeUnavailable {
1128        /// Caller-supplied node identifier.
1129        id: String,
1130    },
1131    /// An algorithm URI is not supported by this build.
1132    #[error("unsupported encryption algorithm: {0}")]
1133    UnsupportedAlgorithm(String),
1134    /// Base64 input is invalid or exceeds the configured input bound.
1135    #[error("invalid base64 data: {0}")]
1136    Base64(String),
1137    /// A decoded cipher value is too short for its algorithm's framing.
1138    #[error("{algorithm} ciphertext is too short: need at least {minimum} bytes, got {actual}")]
1139    DataTooShort {
1140        /// Algorithm name.
1141        algorithm: &'static str,
1142        /// Minimum valid byte length.
1143        minimum: usize,
1144        /// Actual byte length.
1145        actual: usize,
1146    },
1147    /// CBC ciphertext is not a non-empty multiple of the selected cipher's block size.
1148    #[error(
1149        "{algorithm} ciphertext length must be a non-zero multiple of {block} bytes, got {actual}"
1150    )]
1151    InvalidCbcCiphertextLength {
1152        /// Selected content cipher.
1153        algorithm: DataEncryptionAlgorithm,
1154        /// Cipher's block width in bytes.
1155        block: usize,
1156        /// Ciphertext body width, excluding the IV.
1157        actual: usize,
1158    },
1159    /// XMLEnc random padding is invalid.
1160    ///
1161    /// No decrypted padding details are exposed. This does not authenticate CBC
1162    /// ciphertexts or make success/failure safe to expose to an attacker.
1163    #[error("invalid XMLEnc padding")]
1164    InvalidPadding,
1165    /// Authenticated content decryption failed (GCM or ChaCha20-Poly1305).
1166    #[error("AEAD authentication failed")]
1167    AeadAuthenticationFailed,
1168    /// A supplied content key is not the expected size.
1169    #[error("{algorithm:?} requires a {expected}-byte key, got {actual}")]
1170    InvalidKeySize {
1171        /// Content algorithm requiring the key.
1172        algorithm: DataEncryptionAlgorithm,
1173        /// Expected key size.
1174        expected: usize,
1175        /// Actual key size.
1176        actual: usize,
1177    },
1178    /// An unauthenticated content algorithm cannot safely select among keys.
1179    #[error(
1180        "{algorithm:?} cannot safely select among {actual} unordered decryption key candidates"
1181    )]
1182    AmbiguousKeyCandidates {
1183        /// Unauthenticated algorithm for which key success is ambiguous.
1184        algorithm: DataEncryptionAlgorithm,
1185        /// Number of unresolved candidate keys.
1186        actual: usize,
1187    },
1188    /// A supplied AES key-encryption key is not the size declared by EncryptedKey.
1189    #[error("{algorithm:?} requires a {expected}-byte KEK, got {actual}")]
1190    InvalidKekSize {
1191        /// Key-wrap algorithm requiring the KEK.
1192        algorithm: KeyWrapAlgorithm,
1193        /// Expected KEK size.
1194        expected: usize,
1195        /// Actual KEK size.
1196        actual: usize,
1197    },
1198    /// A wrapped-key input or provider output has invalid algorithm framing.
1199    #[error("wrapped-key value must be {expected} bytes, got {actual}")]
1200    InvalidWrappedKeyLength {
1201        /// Exact wrapped length required by the algorithm and key context.
1202        expected: usize,
1203        /// Actual input or provider output length.
1204        actual: usize,
1205    },
1206    /// Encryption configuration is internally inconsistent.
1207    #[error("invalid encryption configuration: {0}")]
1208    InvalidEncryptionConfig(String),
1209    /// No caller-provided resolver could supply a usable key.
1210    #[error("no suitable decryption key was resolved")]
1211    KeyNotFound,
1212    /// No `EncryptedData` matched the requested document selection.
1213    #[error("no matching EncryptedData element was found")]
1214    EncryptedDataNotFound,
1215    /// More than one `EncryptedData` matched the requested document selection.
1216    #[error("more than one EncryptedData element matched; select one by Id")]
1217    AmbiguousEncryptedData,
1218    /// No source element matched the requested encryption target.
1219    #[error("no matching element was found for encryption")]
1220    EncryptionTargetNotFound,
1221    /// More than one source element matched the requested encryption target.
1222    #[error("more than one element matched the encryption target")]
1223    AmbiguousEncryptionTarget,
1224    /// Document replacement requires an XML `Type` declaration.
1225    #[error("EncryptedData must declare Element or Content Type for document replacement")]
1226    ReplacementRequiresXml,
1227    /// RSA-OAEP session-key recovery failed.
1228    #[error("RSA-OAEP key unwrap failed: {0}")]
1229    Rsa(String),
1230    /// RSA-OAEP session-key wrapping failed.
1231    #[error("RSA-OAEP key wrap failed: {0}")]
1232    RsaEncrypt(String),
1233    /// RFC 3394 integrity validation failed while unwrapping a key.
1234    #[error("AES key unwrap failed integrity validation")]
1235    KeyWrapIntegrity,
1236    /// Operating-system randomness was unavailable.
1237    #[error("operating-system random number generation failed: {0}")]
1238    Rng(String),
1239    /// Generated XML could not be serialized.
1240    #[error("XML encryption serialization failed: {0}")]
1241    XmlSerialize(String),
1242    /// XML-declared plaintext could not be decoded as UTF-8.
1243    #[error("decrypted XML is not valid UTF-8: {0}")]
1244    Utf8(#[from] std::string::FromUtf8Error),
1245}
1246
1247impl From<crate::operation::OperationPlanError> for XmlEncError {
1248    fn from(error: crate::operation::OperationPlanError) -> Self {
1249        Self::OperationPlan(error.to_string())
1250    }
1251}
1252
1253impl fmt::Display for DataEncryptionAlgorithm {
1254    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1255        formatter.write_str(match self {
1256            #[cfg(feature = "legacy-algorithms")]
1257            Self::TripleDesCbc => "Triple DES CBC",
1258            #[cfg(feature = "legacy-algorithms")]
1259            Self::Aes192Cbc => "AES-192-CBC",
1260            #[cfg(feature = "legacy-algorithms")]
1261            Self::Aes192Gcm => "AES-192-GCM",
1262            Self::Aes128Cbc => "AES-128-CBC",
1263            Self::Aes256Cbc => "AES-256-CBC",
1264            Self::Aes128Gcm => "AES-128-GCM",
1265            Self::Aes256Gcm => "AES-256-GCM",
1266            Self::Camellia128Cbc => "Camellia-128-CBC",
1267            Self::Camellia192Cbc => "Camellia-192-CBC",
1268            Self::Camellia256Cbc => "Camellia-256-CBC",
1269            Self::ChaCha20 => "ChaCha20",
1270            Self::ChaCha20Poly1305 => "ChaCha20-Poly1305",
1271        })
1272    }
1273}
1274
1275#[cfg(test)]
1276mod framing_tests {
1277    use super::*;
1278
1279    #[test]
1280    fn cbc_framing_error_names_the_selected_cipher() {
1281        // Diagnostics must identify the actual cipher and its block width,
1282        // rather than reporting AES framing for a DES input.
1283        let error = validate_ciphertext_framing(DataEncryptionAlgorithm::Aes128Cbc, 33)
1284            .expect_err("AES ciphertext body is not block-aligned");
1285        assert_eq!(
1286            error.to_string(),
1287            "AES-128-CBC ciphertext length must be a non-zero multiple of 16 bytes, got 17"
1288        );
1289        #[cfg(feature = "legacy-algorithms")]
1290        {
1291            let error = validate_ciphertext_framing(DataEncryptionAlgorithm::TripleDesCbc, 17)
1292                .expect_err("3DES ciphertext body is not block-aligned");
1293            assert_eq!(
1294                error.to_string(),
1295                "Triple DES CBC ciphertext length must be a non-zero multiple of 8 bytes, got 9"
1296            );
1297        }
1298    }
1299}