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}
49
50impl DataEncryptionAlgorithm {
51    /// Key family required independently of the byte length (AES-192 and
52    /// three-key Triple DES both use 24 bytes).
53    pub const fn key_kind(self) -> crate::key_manager::SymmetricKeyKind {
54        #[cfg(feature = "legacy-algorithms")]
55        if matches!(self, Self::TripleDesCbc) {
56            return crate::key_manager::SymmetricKeyKind::Des;
57        }
58        crate::key_manager::SymmetricKeyKind::Aes
59    }
60    /// Parse a supported XMLEnc content-encryption URI.
61    pub fn from_uri(uri: &str) -> Result<Self, XmlEncError> {
62        match uri {
63            #[cfg(feature = "legacy-algorithms")]
64            "http://www.w3.org/2001/04/xmlenc#tripledes-cbc" => Ok(Self::TripleDesCbc),
65            #[cfg(feature = "legacy-algorithms")]
66            "http://www.w3.org/2001/04/xmlenc#aes192-cbc" => Ok(Self::Aes192Cbc),
67            #[cfg(feature = "legacy-algorithms")]
68            "http://www.w3.org/2009/xmlenc11#aes192-gcm" => Ok(Self::Aes192Gcm),
69            "http://www.w3.org/2001/04/xmlenc#aes128-cbc" => Ok(Self::Aes128Cbc),
70            "http://www.w3.org/2001/04/xmlenc#aes256-cbc" => Ok(Self::Aes256Cbc),
71            "http://www.w3.org/2009/xmlenc11#aes128-gcm" => Ok(Self::Aes128Gcm),
72            "http://www.w3.org/2009/xmlenc11#aes256-gcm" => Ok(Self::Aes256Gcm),
73            _ => Err(XmlEncError::UnsupportedAlgorithm(uri.to_owned())),
74        }
75    }
76
77    /// Required symmetric key length in bytes.
78    pub const fn key_len(self) -> usize {
79        match self {
80            #[cfg(feature = "legacy-algorithms")]
81            Self::TripleDesCbc | Self::Aes192Cbc | Self::Aes192Gcm => 24,
82            Self::Aes128Cbc | Self::Aes128Gcm => 16,
83            Self::Aes256Cbc | Self::Aes256Gcm => 32,
84        }
85    }
86
87    /// Return the standard XMLEnc algorithm URI.
88    pub const fn uri(self) -> &'static str {
89        match self {
90            #[cfg(feature = "legacy-algorithms")]
91            Self::TripleDesCbc => "http://www.w3.org/2001/04/xmlenc#tripledes-cbc",
92            #[cfg(feature = "legacy-algorithms")]
93            Self::Aes192Cbc => "http://www.w3.org/2001/04/xmlenc#aes192-cbc",
94            #[cfg(feature = "legacy-algorithms")]
95            Self::Aes192Gcm => "http://www.w3.org/2009/xmlenc11#aes192-gcm",
96            Self::Aes128Cbc => "http://www.w3.org/2001/04/xmlenc#aes128-cbc",
97            Self::Aes256Cbc => "http://www.w3.org/2001/04/xmlenc#aes256-cbc",
98            Self::Aes128Gcm => "http://www.w3.org/2009/xmlenc11#aes128-gcm",
99            Self::Aes256Gcm => "http://www.w3.org/2009/xmlenc11#aes256-gcm",
100        }
101    }
102
103    /// Minimum standard wire length for ciphertext produced by this algorithm.
104    pub(crate) const fn minimum_ciphertext_len(self) -> usize {
105        match self.cbc_block_len() {
106            Some(block) => block * 2,
107            None => 28,
108        }
109    }
110
111    /// Compile-time capability is not the operation's permission.
112    /// Whether this capability must be explicitly selected in the operation allowlist.
113    pub const fn requires_explicit_permission(self) -> bool {
114        match self {
115            #[cfg(feature = "legacy-algorithms")]
116            Self::TripleDesCbc | Self::Aes192Cbc | Self::Aes192Gcm => true,
117            _ => false,
118        }
119    }
120
121    pub(crate) const fn cbc_block_len(self) -> Option<usize> {
122        // XMLEnc 1.1 ยงยง5.2.2-5.2.4: CBC frames prefix one cipher block;
123        // GCM instead prefixes a 96-bit nonce and appends a 128-bit tag.
124        // https://www.w3.org/TR/xmlenc-core1/#sec-Block-Encryption
125        match self {
126            Self::Aes128Cbc | Self::Aes256Cbc => Some(16),
127            #[cfg(feature = "legacy-algorithms")]
128            Self::Aes192Cbc => Some(16),
129            #[cfg(feature = "legacy-algorithms")]
130            Self::TripleDesCbc => Some(8),
131            _ => None,
132        }
133    }
134
135    /// Exact wire length produced when encrypting the given plaintext length.
136    pub(crate) fn ciphertext_len_for_plaintext(self, plaintext_len: usize) -> Option<usize> {
137        match self.cbc_block_len() {
138            Some(block) => (plaintext_len / block)
139                .checked_add(1)?
140                .checked_mul(block)?
141                .checked_add(block),
142            None => plaintext_len.checked_add(28),
143        }
144    }
145}
146
147pub(crate) fn validate_ciphertext_framing(
148    algorithm: DataEncryptionAlgorithm,
149    ciphertext_len: usize,
150) -> Result<(), XmlEncError> {
151    let minimum = algorithm.minimum_ciphertext_len();
152    if ciphertext_len < minimum {
153        let algorithm_name = match algorithm.cbc_block_len() {
154            Some(8) => "Triple DES CBC",
155            Some(_) => "AES-CBC",
156            None => "AES-GCM",
157        };
158        return Err(XmlEncError::DataTooShort {
159            algorithm: algorithm_name,
160            minimum,
161            actual: ciphertext_len,
162        });
163    }
164    if let Some(block) = algorithm.cbc_block_len()
165        && !(ciphertext_len - block).is_multiple_of(block)
166    {
167        return Err(XmlEncError::InvalidCbcCiphertextLength {
168            algorithm,
169            block,
170            actual: ciphertext_len - block,
171        });
172    }
173    Ok(())
174}
175
176impl KeyTransportAlgorithm {
177    /// Historical transport is never enabled merely by compiling its primitive.
178    pub const fn requires_explicit_permission(self) -> bool {
179        match self {
180            #[cfg(feature = "legacy-algorithms")]
181            Self::RsaPkcs1v15 => true,
182            _ => false,
183        }
184    }
185    /// Parse a supported XMLEnc key-transport URI.
186    pub fn from_uri(uri: &str) -> Result<Self, XmlEncError> {
187        match uri {
188            "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" => Ok(Self::RsaOaepMgf1p),
189            "http://www.w3.org/2009/xmlenc11#rsa-oaep" => Ok(Self::RsaOaep11),
190            #[cfg(feature = "legacy-algorithms")]
191            "http://www.w3.org/2001/04/xmlenc#rsa-1_5" => Ok(Self::RsaPkcs1v15),
192            _ => Err(XmlEncError::UnsupportedAlgorithm(uri.to_owned())),
193        }
194    }
195
196    /// Return the standard XMLEnc key-transport URI.
197    pub const fn uri(self) -> &'static str {
198        match self {
199            Self::RsaOaepMgf1p => "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p",
200            Self::RsaOaep11 => "http://www.w3.org/2009/xmlenc11#rsa-oaep",
201            #[cfg(feature = "legacy-algorithms")]
202            Self::RsaPkcs1v15 => "http://www.w3.org/2001/04/xmlenc#rsa-1_5",
203        }
204    }
205}
206
207impl KeyWrapAlgorithm {
208    /// Family of the wrapping key, not of the wrapped content key.
209    pub const fn key_kind(self) -> crate::key_manager::SymmetricKeyKind {
210        #[cfg(feature = "legacy-algorithms")]
211        if matches!(self, Self::TripleDes) {
212            return crate::key_manager::SymmetricKeyKind::Des;
213        }
214        crate::key_manager::SymmetricKeyKind::Aes
215    }
216    /// Parse a supported XMLEnc symmetric key-wrap URI.
217    pub fn from_uri(uri: &str) -> Result<Self, XmlEncError> {
218        match uri {
219            #[cfg(feature = "legacy-algorithms")]
220            "http://www.w3.org/2001/04/xmlenc#kw-aes192" => Ok(Self::AesKw192),
221            #[cfg(feature = "legacy-algorithms")]
222            "http://www.w3.org/2001/04/xmlenc#kw-tripledes" => Ok(Self::TripleDes),
223            "http://www.w3.org/2001/04/xmlenc#kw-aes128" => Ok(Self::AesKw128),
224            "http://www.w3.org/2001/04/xmlenc#kw-aes256" => Ok(Self::AesKw256),
225            _ => Err(XmlEncError::UnsupportedAlgorithm(uri.to_owned())),
226        }
227    }
228
229    /// Required key-encryption-key length in bytes.
230    pub const fn key_len(self) -> usize {
231        match self {
232            #[cfg(feature = "legacy-algorithms")]
233            Self::AesKw192 | Self::TripleDes => 24,
234            Self::AesKw128 => 16,
235            Self::AesKw256 => 32,
236        }
237    }
238
239    /// Return the standard XMLEnc key-wrap URI.
240    pub const fn uri(self) -> &'static str {
241        match self {
242            #[cfg(feature = "legacy-algorithms")]
243            Self::AesKw192 => "http://www.w3.org/2001/04/xmlenc#kw-aes192",
244            #[cfg(feature = "legacy-algorithms")]
245            Self::TripleDes => "http://www.w3.org/2001/04/xmlenc#kw-tripledes",
246            Self::AesKw128 => "http://www.w3.org/2001/04/xmlenc#kw-aes128",
247            Self::AesKw256 => "http://www.w3.org/2001/04/xmlenc#kw-aes256",
248        }
249    }
250
251    /// Whether use requires an explicit compiled-policy allowlist entry.
252    pub const fn requires_explicit_permission(self) -> bool {
253        match self {
254            #[cfg(feature = "legacy-algorithms")]
255            Self::AesKw192 | Self::TripleDes => true,
256            _ => false,
257        }
258    }
259
260    pub(crate) const fn overhead(self) -> usize {
261        // XMLEnc 1.1 ยง5.7.1 / RFC 3217 ยงยง2-3: CMS wrapping includes
262        // an 8-byte checksum and an 8-byte IV, unlike RFC 3394's A register.
263        // https://www.w3.org/TR/xmlenc-core1/#sec-CMS-3DES
264        match self {
265            #[cfg(feature = "legacy-algorithms")]
266            Self::TripleDes => 16,
267            _ => 8,
268        }
269    }
270}
271
272/// Supported asymmetric session-key transport algorithms.
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
274pub enum KeyTransportAlgorithm {
275    /// Historical RSAES-PKCS1-v1_5 transport; explicit policy permission is required.
276    #[cfg(feature = "legacy-algorithms")]
277    RsaPkcs1v15,
278    /// XML Encryption 1.0 OAEP URI with SHA-1/MGF1-SHA1 absent-field defaults.
279    ///
280    /// An explicit DigestMethod or XMLEnc 1.1 MGF child overrides the corresponding default,
281    /// matching libxmlsec1 1.3.13 behavior.
282    RsaOaepMgf1p,
283    /// XML Encryption 1.1 OAEP with explicitly parsed digest and MGF settings.
284    RsaOaep11,
285}
286
287/// Supported symmetric key-wrap algorithms.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
289pub enum KeyWrapAlgorithm {
290    /// AES-192 RFC 3394 key wrap, explicitly permitted by compatibility policy.
291    #[cfg(feature = "legacy-algorithms")]
292    AesKw192,
293    /// CMS Triple DES key wrap, including XMLEnc's optional non-DES key inputs.
294    #[cfg(feature = "legacy-algorithms")]
295    TripleDes,
296    /// RFC 3394 AES key wrap with a 128-bit KEK.
297    AesKw128,
298    /// RFC 3394 AES key wrap with a 256-bit KEK.
299    AesKw256,
300}
301
302/// Digest algorithms accepted by RSA-OAEP encryption.
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
304pub enum OaepDigestAlgorithm {
305    /// SHA-1, retained for legacy XMLEnc OAEP interoperability.
306    Sha1,
307    /// SHA-256.
308    Sha256,
309    /// SHA-384.
310    Sha384,
311    /// SHA-512.
312    Sha512,
313}
314
315impl OaepDigestAlgorithm {
316    /// Parse a digest URI accepted by XML Encryption and libxmlsec1.
317    pub fn from_uri(uri: &str) -> Option<Self> {
318        match uri {
319            "http://www.w3.org/2000/09/xmldsig#sha1" => Some(Self::Sha1),
320            "http://www.w3.org/2001/04/xmlenc#sha256" => Some(Self::Sha256),
321            "http://www.w3.org/2001/04/xmlenc#sha384"
322            | "http://www.w3.org/2001/04/xmldsig-more#sha384" => Some(Self::Sha384),
323            "http://www.w3.org/2001/04/xmlenc#sha512" => Some(Self::Sha512),
324            _ => None,
325        }
326    }
327
328    /// Parse an XML Encryption 1.1 MGF1 URI.
329    pub fn from_mgf_uri(uri: &str) -> Option<Self> {
330        match uri {
331            "http://www.w3.org/2009/xmlenc11#mgf1sha1" => Some(Self::Sha1),
332            "http://www.w3.org/2009/xmlenc11#mgf1sha256" => Some(Self::Sha256),
333            "http://www.w3.org/2009/xmlenc11#mgf1sha384" => Some(Self::Sha384),
334            "http://www.w3.org/2009/xmlenc11#mgf1sha512" => Some(Self::Sha512),
335            _ => None,
336        }
337    }
338
339    /// Return the standard digest URI.
340    pub const fn uri(self) -> &'static str {
341        match self {
342            Self::Sha1 => "http://www.w3.org/2000/09/xmldsig#sha1",
343            Self::Sha256 => "http://www.w3.org/2001/04/xmlenc#sha256",
344            Self::Sha384 => "http://www.w3.org/2001/04/xmlenc#sha384",
345            Self::Sha512 => "http://www.w3.org/2001/04/xmlenc#sha512",
346        }
347    }
348
349    /// Return the XML Encryption 1.1 MGF URI for this digest.
350    pub const fn mgf_uri(self) -> &'static str {
351        match self {
352            Self::Sha1 => "http://www.w3.org/2009/xmlenc11#mgf1sha1",
353            Self::Sha256 => "http://www.w3.org/2009/xmlenc11#mgf1sha256",
354            Self::Sha384 => "http://www.w3.org/2009/xmlenc11#mgf1sha384",
355            Self::Sha512 => "http://www.w3.org/2009/xmlenc11#mgf1sha512",
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::{OaepDigestAlgorithm, XmlEncError};
363
364    #[test]
365    fn operation_plan_failures_have_a_distinct_error_class() {
366        let error = XmlEncError::from(crate::operation::OperationPlanError::StaleResourceIdentity);
367        assert!(matches!(error, XmlEncError::OperationPlan(_)));
368    }
369
370    #[test]
371    fn oaep_sha384_accepts_both_interoperable_digest_uris() {
372        // The canonical XML Encryption spelling and libxmlsec1's XMLDSig-more
373        // spelling identify the same OAEP digest algorithm.
374        for uri in [
375            "http://www.w3.org/2001/04/xmlenc#sha384",
376            "http://www.w3.org/2001/04/xmldsig-more#sha384",
377        ] {
378            assert_eq!(
379                OaepDigestAlgorithm::from_uri(uri),
380                Some(OaepDigestAlgorithm::Sha384)
381            );
382        }
383    }
384
385    #[test]
386    fn oaep_mgf_uris_round_trip() {
387        for algorithm in [
388            OaepDigestAlgorithm::Sha1,
389            OaepDigestAlgorithm::Sha256,
390            OaepDigestAlgorithm::Sha384,
391            OaepDigestAlgorithm::Sha512,
392        ] {
393            assert_eq!(
394                OaepDigestAlgorithm::from_mgf_uri(algorithm.mgf_uri()),
395                Some(algorithm)
396            );
397        }
398        assert_eq!(
399            OaepDigestAlgorithm::from_mgf_uri("urn:unsupported-mgf"),
400            None
401        );
402    }
403}
404
405/// RSA-OAEP parameters emitted in an `EncryptedKey`.
406#[derive(Debug, Clone, PartialEq, Eq)]
407pub struct RsaOaepParameters {
408    /// XMLEnc 1.0 legacy OAEP or XMLEnc 1.1 configurable OAEP.
409    pub algorithm: KeyTransportAlgorithm,
410    /// Digest used by OAEP.
411    pub digest: OaepDigestAlgorithm,
412    /// Digest used by MGF1.
413    pub mgf_digest: OaepDigestAlgorithm,
414    /// Optional OAEP label bytes.
415    pub label: Vec<u8>,
416}
417
418impl RsaOaepParameters {
419    /// Create legacy OAEP parameters with SHA-1 and MGF1-SHA-1.
420    pub fn legacy() -> Self {
421        Self {
422            algorithm: KeyTransportAlgorithm::RsaOaepMgf1p,
423            digest: OaepDigestAlgorithm::Sha1,
424            mgf_digest: OaepDigestAlgorithm::Sha1,
425            label: Vec::new(),
426        }
427    }
428
429    /// Create XMLEnc 1.1 OAEP parameters.
430    pub fn xmlenc11(digest: OaepDigestAlgorithm, mgf_digest: OaepDigestAlgorithm) -> Self {
431        Self {
432            algorithm: KeyTransportAlgorithm::RsaOaep11,
433            digest,
434            mgf_digest,
435            label: Vec::new(),
436        }
437    }
438
439    /// Set the OAEP label bytes.
440    pub fn label(mut self, label: impl Into<Vec<u8>>) -> Self {
441        self.label = label.into();
442        self
443    }
444}
445
446impl Default for RsaOaepParameters {
447    fn default() -> Self {
448        Self::xmlenc11(OaepDigestAlgorithm::Sha256, OaepDigestAlgorithm::Sha256)
449    }
450}
451
452/// One recipient of a generated content-encryption key.
453#[derive(Clone)]
454pub enum EncryptionRecipient {
455    /// Historical RSA transport, admitted only by explicit compiled policy.
456    #[cfg(feature = "legacy-algorithms")]
457    RsaPkcs1v15 {
458        /// Opaque RSA public key used by the selected provider.
459        public_key: Arc<dyn crate::provider::KeyTransportKey>,
460        /// Optional recipient identifier.
461        recipient: Option<String>,
462        /// Optional key hint.
463        key_name: Option<String>,
464    },
465    /// Wrap the content key with an RSA public key and OAEP.
466    RsaOaep {
467        /// Opaque recipient public-key handle.
468        public_key: Arc<dyn crate::provider::KeyTransportKey>,
469        /// OAEP algorithm parameters.
470        parameters: RsaOaepParameters,
471        /// Optional `Recipient` attribute.
472        recipient: Option<String>,
473        /// Optional key hint inside the encrypted key's `KeyInfo`.
474        key_name: Option<String>,
475    },
476    /// Wrap the content key with a pre-shared AES KEK.
477    AesKeyWrap {
478        /// AES key-encryption key.
479        kek: Vec<u8>,
480        /// RFC 3394 key-wrap variant.
481        algorithm: KeyWrapAlgorithm,
482        /// Optional `Recipient` attribute.
483        recipient: Option<String>,
484        /// Optional key hint inside the encrypted key's `KeyInfo`.
485        key_name: Option<String>,
486    },
487}
488
489impl fmt::Debug for EncryptionRecipient {
490    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
491        match self {
492            #[cfg(feature = "legacy-algorithms")]
493            Self::RsaPkcs1v15 {
494                recipient,
495                key_name,
496                ..
497            } => formatter
498                .debug_struct("EncryptionRecipient::RsaPkcs1v15")
499                .field("public_key", &"[PUBLIC KEY]")
500                .field("recipient", recipient)
501                .field("key_name", key_name)
502                .finish(),
503            Self::RsaOaep {
504                parameters,
505                recipient,
506                key_name,
507                ..
508            } => formatter
509                .debug_struct("EncryptionRecipient::RsaOaep")
510                .field("public_key", &"[PUBLIC KEY]")
511                .field("parameters", parameters)
512                .field("recipient", recipient)
513                .field("key_name", key_name)
514                .finish(),
515            Self::AesKeyWrap {
516                algorithm,
517                recipient,
518                key_name,
519                ..
520            } => formatter
521                .debug_struct("EncryptionRecipient::AesKeyWrap")
522                .field("kek", &"[REDACTED]")
523                .field("algorithm", algorithm)
524                .field("recipient", recipient)
525                .field("key_name", key_name)
526                .finish(),
527        }
528    }
529}
530
531impl EncryptionRecipient {
532    /// Create a historical RSAES-PKCS1-v1_5 recipient. Defaults still deny it.
533    #[cfg(feature = "legacy-algorithms")]
534    pub fn rsa_pkcs1v15(public_key: RsaPublicKey) -> Self {
535        Self::provider_pkcs1v15(Arc::new(crate::provider::RustCryptoRsaPublicKey::new(
536            public_key,
537        )))
538    }
539
540    /// Create a historical RSA recipient without extracting provider-owned keys.
541    #[cfg(feature = "legacy-algorithms")]
542    pub fn provider_pkcs1v15(public_key: Arc<dyn crate::provider::KeyTransportKey>) -> Self {
543        Self::RsaPkcs1v15 {
544            public_key,
545            recipient: None,
546            key_name: None,
547        }
548    }
549    /// Create an RSA-OAEP recipient using SHA-256 and MGF1-SHA-256.
550    ///
551    /// XMLEnc 1.1 assigns SHA-1 and MGF1-SHA-1 when these parameters are
552    /// omitted. Serialized keys therefore include both algorithm values
553    /// explicitly instead of relying on the specification's legacy defaults.
554    pub fn rsa_oaep(public_key: RsaPublicKey) -> Self {
555        Self::provider_key_transport(Arc::new(crate::provider::RustCryptoRsaPublicKey::new(
556            public_key,
557        )))
558    }
559
560    /// Create an RSA-OAEP recipient from an opaque provider key handle.
561    pub fn provider_key_transport(public_key: Arc<dyn crate::provider::KeyTransportKey>) -> Self {
562        Self::RsaOaep {
563            public_key,
564            parameters: RsaOaepParameters::default(),
565            recipient: None,
566            key_name: None,
567        }
568    }
569
570    /// Create an AES Key Wrap recipient.
571    pub fn aes_key_wrap(kek: impl Into<Vec<u8>>, algorithm: KeyWrapAlgorithm) -> Self {
572        Self::AesKeyWrap {
573            kek: kek.into(),
574            algorithm,
575            recipient: None,
576            key_name: None,
577        }
578    }
579
580    /// Override RSA-OAEP parameters.
581    pub fn oaep_parameters(mut self, parameters: RsaOaepParameters) -> Self {
582        if let Self::RsaOaep {
583            parameters: current,
584            ..
585        } = &mut self
586        {
587            *current = parameters;
588        }
589        self
590    }
591
592    /// Set the recipient identifier emitted on `EncryptedKey`.
593    pub fn recipient(mut self, value: impl Into<String>) -> Self {
594        match &mut self {
595            #[cfg(feature = "legacy-algorithms")]
596            Self::RsaPkcs1v15 { recipient, .. } => *recipient = Some(value.into()),
597            Self::RsaOaep { recipient, .. } | Self::AesKeyWrap { recipient, .. } => {
598                *recipient = Some(value.into());
599            }
600        }
601        self
602    }
603
604    /// Set the key name emitted inside the encrypted key's `KeyInfo`.
605    pub fn key_name(mut self, value: impl Into<String>) -> Self {
606        match &mut self {
607            #[cfg(feature = "legacy-algorithms")]
608            Self::RsaPkcs1v15 { key_name, .. } => *key_name = Some(value.into()),
609            Self::RsaOaep { key_name, .. } | Self::AesKeyWrap { key_name, .. } => {
610                *key_name = Some(value.into());
611            }
612        }
613        self
614    }
615}
616
617/// How generated `EncryptedData` replaces caller-owned XML.
618#[derive(Debug, Clone, Copy, PartialEq, Eq)]
619pub enum ReplacementMode {
620    /// Replace the selected element, including its start and end tags.
621    ReplaceElement,
622    /// Replace only the selected element's child content.
623    ReplaceContent,
624}
625
626/// Result returned after encrypting bytes or XML.
627#[derive(Debug, Clone, PartialEq, Eq)]
628pub struct EncryptionResult {
629    /// Complete `EncryptedData` XML fragment.
630    pub encrypted_data_xml: String,
631    /// Required caller-owned document replacement operation.
632    pub replacement: ReplacementMode,
633}
634
635/// Caller-owned target selection for document encryption.
636#[derive(Debug, Clone, Copy, Default)]
637pub struct DocumentEncryptionOptions<'a> {
638    /// Select an element by `Id`, `ID`, or `id`; `None` selects the document root.
639    pub element_id: Option<&'a str>,
640}
641
642/// Parsed `EncryptionMethod` data.
643#[derive(Debug, Clone, PartialEq, Eq)]
644pub struct EncryptionMethod {
645    /// Algorithm URI from the mandatory `Algorithm` attribute.
646    pub algorithm: String,
647    /// Optional explicit key size in bits.
648    pub key_size_bits: Option<usize>,
649    /// Digest URI used by XML Encryption 1.1 OAEP.
650    pub oaep_digest: Option<String>,
651    /// MGF URI used by XML Encryption 1.1 OAEP.
652    pub mgf_algorithm: Option<String>,
653    /// Decoded OAEP label bytes.
654    pub oaep_params: Option<Vec<u8>>,
655}
656
657impl EncryptionMethod {
658    /// Validate invariants imposed by the selected algorithm URI.
659    ///
660    /// Parsed XML and caller-constructed typed values share this check so the
661    /// public typed API cannot express wire structures that XML parsing rejects.
662    pub(crate) fn validate_structure(&self) -> Result<(), XmlEncError> {
663        if self.key_size_bits == Some(0) {
664            return Err(XmlEncError::InvalidStructure(
665                "KeySize must be a positive integer".into(),
666            ));
667        }
668        let is_legacy_oaep = self.algorithm == KeyTransportAlgorithm::RsaOaepMgf1p.uri();
669        let is_oaep11 = self.algorithm == KeyTransportAlgorithm::RsaOaep11.uri();
670        if (self.oaep_params.is_some()
671            || self.oaep_digest.is_some()
672            || self.mgf_algorithm.is_some())
673            && !is_legacy_oaep
674            && !is_oaep11
675        {
676            return Err(XmlEncError::InvalidStructure(
677                "OAEP parameters are only valid for RSA-OAEP EncryptionMethod".into(),
678            ));
679        }
680        if let (Some(actual), Some(expected)) =
681            (self.key_size_bits, fixed_aes_key_size(&self.algorithm))
682            && actual != expected
683        {
684            return Err(XmlEncError::InvalidStructure(format!(
685                "EncryptionMethod {} requires KeySize {expected}, got {actual}",
686                self.algorithm
687            )));
688        }
689        Ok(())
690    }
691}
692
693fn fixed_aes_key_size(algorithm: &str) -> Option<usize> {
694    let key_len = DataEncryptionAlgorithm::from_uri(algorithm)
695        .map(DataEncryptionAlgorithm::key_len)
696        .or_else(|_| KeyWrapAlgorithm::from_uri(algorithm).map(KeyWrapAlgorithm::key_len))
697        .ok()?;
698    Some(key_len * 8)
699}
700
701/// Ciphertext storage specified by XMLEnc 1.1 section 3.3.
702#[derive(Debug, Clone, PartialEq, Eq)]
703pub enum CipherData {
704    /// Whitespace-normalized base64 text from `CipherValue`.
705    Value {
706        /// Encoded ciphertext, not plaintext or key material.
707        value: String,
708    },
709    /// A source-anchored reference, resolved before cryptographic dispatch.
710    Reference {
711        /// Required URI, including an explicitly empty URI.
712        uri: String,
713        /// Ordered transforms retained with their original XPath source identity.
714        transforms: Vec<crate::xmldsig::transforms::Transform>,
715    },
716    /// Ciphertext octets resolved by the operation or supplied by the caller.
717    Bytes(Vec<u8>),
718}
719
720impl CipherData {
721    pub(super) fn octets(&self) -> Result<std::borrow::Cow<'_, [u8]>, XmlEncError> {
722        use base64::Engine as _;
723        match self {
724            Self::Value { value } => base64::engine::general_purpose::STANDARD
725                .decode(value)
726                .map(std::borrow::Cow::Owned)
727                .map_err(|error| XmlEncError::Base64(error.to_string())),
728            Self::Bytes(bytes) => Ok(std::borrow::Cow::Borrowed(bytes)),
729            Self::Reference { .. } => Err(XmlEncError::InvalidStructure(
730                "CipherReference requires its original document context".into(),
731            )),
732        }
733    }
734
735    /// Return an inline base64 value, if this ciphertext is enveloped.
736    pub fn inline_value(&self) -> Option<&str> {
737        match self {
738            Self::Value { value } => Some(value),
739            Self::Reference { .. } | Self::Bytes(_) => None,
740        }
741    }
742
743    /// Mutably access an inline value without changing its storage variant.
744    pub fn inline_value_mut(&mut self) -> Option<&mut String> {
745        match self {
746            Self::Value { value } => Some(value),
747            Self::Reference { .. } | Self::Bytes(_) => None,
748        }
749    }
750}
751
752/// Parsed embedded `EncryptedKey` used to recover a content-encryption key.
753#[derive(Debug, Clone, PartialEq, Eq)]
754pub struct EncryptedKey {
755    /// Sources of the key which protects this transported key. These are not
756    /// alternative content keys: their output feeds this key's EncryptionMethod.
757    pub sources: EncryptionKeySources,
758    /// Optional XML identifier.
759    pub id: Option<String>,
760    /// Optional recipient hint.
761    pub recipient: Option<String>,
762    /// Optional direct `ds:KeyName` hint from the key's `KeyInfo`.
763    pub key_name: Option<String>,
764    /// Method which wrapped the session key.
765    pub encryption_method: EncryptionMethod,
766    /// Wrapped session-key bytes in base64 form.
767    pub cipher_data: CipherData,
768    /// Optional references identifying data or keys associated with this key.
769    pub reference_list: Option<ReferenceList>,
770    /// Optional name associated with the transported plaintext key.
771    pub carried_key_name: Option<String>,
772}
773
774/// Typed establishment sources inside an encrypted key's `ds:KeyInfo`.
775/// Recursion is checked against the operation's depth and candidate allowances.
776#[derive(Debug, Clone, PartialEq, Eq, Default)]
777pub struct EncryptionKeySources {
778    /// Keys transporting the wrapping key, rather than the final content key.
779    pub encrypted_keys: Vec<EncryptedKey>,
780    /// Derivations producing the wrapping key.
781    pub derived_keys: Vec<super::DerivedKey>,
782    /// Agreements producing the wrapping key.
783    pub agreement_methods: Vec<super::AgreementMethod>,
784}
785
786impl EncryptionKeySources {
787    pub(super) fn is_empty(&self) -> bool {
788        self.encrypted_keys.is_empty()
789            && self.derived_keys.is_empty()
790            && self.agreement_methods.is_empty()
791    }
792}
793
794/// References associated with an `EncryptedKey`.
795#[derive(Debug, Clone, PartialEq, Eq)]
796pub struct ReferenceList {
797    /// URI references to `EncryptedData` elements encrypted with this key.
798    pub data_references: Vec<String>,
799    /// URI references to other `EncryptedKey` elements encrypted with this key.
800    pub key_references: Vec<String>,
801}
802
803/// Parsed `EncryptedData` document fragment.
804#[derive(Debug, Clone, PartialEq, Eq)]
805pub struct EncryptedData {
806    /// Optional XML identifier.
807    pub id: Option<String>,
808    /// Optional plaintext representation hint.
809    pub encrypted_type: Option<EncryptedDataType>,
810    /// Optional direct `ds:KeyName` hint from `KeyInfo`.
811    pub key_name: Option<String>,
812    /// Content-encryption method.
813    pub encryption_method: EncryptionMethod,
814    /// Embedded recipient session keys in `KeyInfo` document order.
815    pub encrypted_keys: Vec<EncryptedKey>,
816    /// Ordered XML derivation candidates. Master material remains request-owned.
817    pub derived_keys: Vec<super::DerivedKey>,
818    /// Agreement candidates; trusted private keys remain in request context.
819    pub agreement_methods: Vec<super::AgreementMethod>,
820    /// Content ciphertext in base64 form.
821    pub cipher_data: CipherData,
822}
823
824/// Plaintext returned from XMLEnc decryption.
825#[derive(Debug, Clone, PartialEq, Eq)]
826pub enum DecryptedContent {
827    /// XML plaintext for `Element` and `Content` encrypted data.
828    Xml(String),
829    /// Binary plaintext when the encrypted data has no standard XML type hint.
830    Bytes(Vec<u8>),
831}
832
833/// Errors raised while parsing, encrypting, resolving, or decrypting XMLEnc data.
834#[derive(Debug, thiserror::Error)]
835#[non_exhaustive]
836pub enum XmlEncError {
837    /// URI resolution or ciphertext transforms failed.
838    #[error("ciphertext reference error: {0}")]
839    Transform(#[from] crate::xmldsig::TransformError),
840    /// The compiled encryption or decryption policy rejected an operation input.
841    #[error("XML Encryption policy violation: {0}")]
842    Policy(#[from] crate::policy::PolicyViolation),
843
844    /// The selected cryptographic provider rejected or failed an operation.
845    #[error("cryptographic provider error: {0}")]
846    Provider(#[from] crate::provider::ProviderError),
847
848    /// XML document parsing failed.
849    #[error("XML parsing error: {0}")]
850    XmlParse(#[from] crate::xml::dom::ParseError),
851    /// The owned XML document boundary rejected an identity or mutation.
852    #[error("XML document error: {0}")]
853    Document(#[from] crate::document::XmlDocumentError),
854    /// Required child element or attribute was absent.
855    #[error("missing required {0}")]
856    MissingRequired(&'static str),
857    /// The XML element order or namespace is invalid for the XMLEnc profile.
858    #[error("invalid encrypted structure: {0}")]
859    InvalidStructure(String),
860    /// The compiled operation graph rejected an identity, dependency, or generation invariant.
861    #[error("invalid XML Encryption operation plan: {0}")]
862    OperationPlan(String),
863    /// The selected operation-start node ID is absent or resolves ambiguously.
864    #[error("selected node ID is missing or ambiguous: {id}")]
865    SelectedNodeUnavailable {
866        /// Caller-supplied node identifier.
867        id: String,
868    },
869    /// An algorithm URI is not supported by this build.
870    #[error("unsupported encryption algorithm: {0}")]
871    UnsupportedAlgorithm(String),
872    /// Base64 input is invalid or exceeds the configured input bound.
873    #[error("invalid base64 data: {0}")]
874    Base64(String),
875    /// A decoded cipher value is too short for its algorithm's framing.
876    #[error("{algorithm} ciphertext is too short: need at least {minimum} bytes, got {actual}")]
877    DataTooShort {
878        /// Algorithm name.
879        algorithm: &'static str,
880        /// Minimum valid byte length.
881        minimum: usize,
882        /// Actual byte length.
883        actual: usize,
884    },
885    /// CBC ciphertext is not a non-empty multiple of the selected cipher's block size.
886    #[error(
887        "{algorithm} ciphertext length must be a non-zero multiple of {block} bytes, got {actual}"
888    )]
889    InvalidCbcCiphertextLength {
890        /// Selected content cipher.
891        algorithm: DataEncryptionAlgorithm,
892        /// Cipher's block width in bytes.
893        block: usize,
894        /// Ciphertext body width, excluding the IV.
895        actual: usize,
896    },
897    /// XMLEnc random padding is invalid.
898    ///
899    /// No decrypted padding details are exposed. This does not authenticate CBC
900    /// ciphertexts or make success/failure safe to expose to an attacker.
901    #[error("invalid XMLEnc padding")]
902    InvalidPadding,
903    /// GCM authentication failed.
904    #[error("AES-GCM authentication failed")]
905    AeadAuthenticationFailed,
906    /// A supplied content key is not the expected size.
907    #[error("{algorithm:?} requires a {expected}-byte key, got {actual}")]
908    InvalidKeySize {
909        /// Content algorithm requiring the key.
910        algorithm: DataEncryptionAlgorithm,
911        /// Expected key size.
912        expected: usize,
913        /// Actual key size.
914        actual: usize,
915    },
916    /// An unauthenticated content algorithm cannot safely select among keys.
917    #[error(
918        "{algorithm:?} cannot safely select among {actual} unordered decryption key candidates"
919    )]
920    AmbiguousKeyCandidates {
921        /// Unauthenticated algorithm for which key success is ambiguous.
922        algorithm: DataEncryptionAlgorithm,
923        /// Number of unresolved candidate keys.
924        actual: usize,
925    },
926    /// A supplied AES key-encryption key is not the size declared by EncryptedKey.
927    #[error("{algorithm:?} requires a {expected}-byte KEK, got {actual}")]
928    InvalidKekSize {
929        /// Key-wrap algorithm requiring the KEK.
930        algorithm: KeyWrapAlgorithm,
931        /// Expected KEK size.
932        expected: usize,
933        /// Actual KEK size.
934        actual: usize,
935    },
936    /// A wrapped-key input or provider output has invalid algorithm framing.
937    #[error("wrapped-key value must be {expected} bytes, got {actual}")]
938    InvalidWrappedKeyLength {
939        /// Exact wrapped length required by the algorithm and key context.
940        expected: usize,
941        /// Actual input or provider output length.
942        actual: usize,
943    },
944    /// Encryption configuration is internally inconsistent.
945    #[error("invalid encryption configuration: {0}")]
946    InvalidEncryptionConfig(String),
947    /// No caller-provided resolver could supply a usable key.
948    #[error("no suitable decryption key was resolved")]
949    KeyNotFound,
950    /// No `EncryptedData` matched the requested document selection.
951    #[error("no matching EncryptedData element was found")]
952    EncryptedDataNotFound,
953    /// More than one `EncryptedData` matched the requested document selection.
954    #[error("more than one EncryptedData element matched; select one by Id")]
955    AmbiguousEncryptedData,
956    /// No source element matched the requested encryption target.
957    #[error("no matching element was found for encryption")]
958    EncryptionTargetNotFound,
959    /// More than one source element matched the requested encryption target.
960    #[error("more than one element matched the encryption target")]
961    AmbiguousEncryptionTarget,
962    /// Document replacement requires an XML `Type` declaration.
963    #[error("EncryptedData must declare Element or Content Type for document replacement")]
964    ReplacementRequiresXml,
965    /// RSA-OAEP session-key recovery failed.
966    #[error("RSA-OAEP key unwrap failed: {0}")]
967    Rsa(String),
968    /// RSA-OAEP session-key wrapping failed.
969    #[error("RSA-OAEP key wrap failed: {0}")]
970    RsaEncrypt(String),
971    /// RFC 3394 integrity validation failed while unwrapping a key.
972    #[error("AES key unwrap failed integrity validation")]
973    KeyWrapIntegrity,
974    /// Operating-system randomness was unavailable.
975    #[error("operating-system random number generation failed: {0}")]
976    Rng(String),
977    /// Generated XML could not be serialized.
978    #[error("XML encryption serialization failed: {0}")]
979    XmlSerialize(String),
980    /// XML-declared plaintext could not be decoded as UTF-8.
981    #[error("decrypted XML is not valid UTF-8: {0}")]
982    Utf8(#[from] std::string::FromUtf8Error),
983}
984
985impl From<crate::operation::OperationPlanError> for XmlEncError {
986    fn from(error: crate::operation::OperationPlanError) -> Self {
987        Self::OperationPlan(error.to_string())
988    }
989}
990
991impl fmt::Display for DataEncryptionAlgorithm {
992    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
993        formatter.write_str(match self {
994            #[cfg(feature = "legacy-algorithms")]
995            Self::TripleDesCbc => "Triple DES CBC",
996            #[cfg(feature = "legacy-algorithms")]
997            Self::Aes192Cbc => "AES-192-CBC",
998            #[cfg(feature = "legacy-algorithms")]
999            Self::Aes192Gcm => "AES-192-GCM",
1000            Self::Aes128Cbc => "AES-128-CBC",
1001            Self::Aes256Cbc => "AES-256-CBC",
1002            Self::Aes128Gcm => "AES-128-GCM",
1003            Self::Aes256Gcm => "AES-256-GCM",
1004        })
1005    }
1006}
1007
1008#[cfg(test)]
1009mod framing_tests {
1010    use super::*;
1011
1012    #[test]
1013    fn cbc_framing_error_names_the_selected_cipher() {
1014        // Diagnostics must identify the actual cipher and its block width,
1015        // rather than reporting AES framing for a DES input.
1016        let error = validate_ciphertext_framing(DataEncryptionAlgorithm::Aes128Cbc, 33)
1017            .expect_err("AES ciphertext body is not block-aligned");
1018        assert_eq!(
1019            error.to_string(),
1020            "AES-128-CBC ciphertext length must be a non-zero multiple of 16 bytes, got 17"
1021        );
1022        #[cfg(feature = "legacy-algorithms")]
1023        {
1024            let error = validate_ciphertext_framing(DataEncryptionAlgorithm::TripleDesCbc, 17)
1025                .expect_err("3DES ciphertext body is not block-aligned");
1026            assert_eq!(
1027                error.to_string(),
1028                "Triple DES CBC ciphertext length must be a non-zero multiple of 8 bytes, got 9"
1029            );
1030        }
1031    }
1032}