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    /// Experimental KEM sources producing this wrapping key.
779    pub encapsulation_methods: Vec<crate::key_establishment::EncapsulationMechanism>,
780    /// Keys transporting the wrapping key, rather than the final content key.
781    pub encrypted_keys: Vec<EncryptedKey>,
782    /// Derivations producing the wrapping key.
783    pub derived_keys: Vec<super::DerivedKey>,
784    /// Agreements producing the wrapping key.
785    pub agreement_methods: Vec<super::AgreementMethod>,
786}
787
788impl EncryptionKeySources {
789    pub(super) fn is_empty(&self) -> bool {
790        self.encrypted_keys.is_empty()
791            && self.encapsulation_methods.is_empty()
792            && self.derived_keys.is_empty()
793            && self.agreement_methods.is_empty()
794    }
795}
796
797/// References associated with an `EncryptedKey`.
798#[derive(Debug, Clone, PartialEq, Eq)]
799pub struct ReferenceList {
800    /// URI references to `EncryptedData` elements encrypted with this key.
801    pub data_references: Vec<String>,
802    /// URI references to other `EncryptedKey` elements encrypted with this key.
803    pub key_references: Vec<String>,
804}
805
806/// Parsed `EncryptedData` document fragment.
807#[derive(Debug, Clone, PartialEq, Eq)]
808pub struct EncryptedData {
809    /// Experimental KEM sources; never silently treated as raw content keys.
810    pub encapsulation_methods: Vec<crate::key_establishment::EncapsulationMechanism>,
811    /// Optional XML identifier.
812    pub id: Option<String>,
813    /// Optional plaintext representation hint.
814    pub encrypted_type: Option<EncryptedDataType>,
815    /// Optional direct `ds:KeyName` hint from `KeyInfo`.
816    pub key_name: Option<String>,
817    /// Content-encryption method.
818    pub encryption_method: EncryptionMethod,
819    /// Embedded recipient session keys in `KeyInfo` document order.
820    pub encrypted_keys: Vec<EncryptedKey>,
821    /// Ordered XML derivation candidates. Master material remains request-owned.
822    pub derived_keys: Vec<super::DerivedKey>,
823    /// Agreement candidates; trusted private keys remain in request context.
824    pub agreement_methods: Vec<super::AgreementMethod>,
825    /// Content ciphertext in base64 form.
826    pub cipher_data: CipherData,
827}
828
829/// Plaintext returned from XMLEnc decryption.
830#[derive(Debug, Clone, PartialEq, Eq)]
831pub enum DecryptedContent {
832    /// XML plaintext for `Element` and `Content` encrypted data.
833    Xml(String),
834    /// Binary plaintext when the encrypted data has no standard XML type hint.
835    Bytes(Vec<u8>),
836}
837
838/// Errors raised while parsing, encrypting, resolving, or decrypting XMLEnc data.
839#[derive(Debug, thiserror::Error)]
840#[non_exhaustive]
841pub enum XmlEncError {
842    /// Shared key establishment failed before content encryption/decryption.
843    #[error(transparent)]
844    KeyEstablishment(#[from] crate::key_establishment::KeyEstablishmentError),
845    /// URI resolution or ciphertext transforms failed.
846    #[error("ciphertext reference error: {0}")]
847    Transform(#[from] crate::xmldsig::TransformError),
848    /// The compiled encryption or decryption policy rejected an operation input.
849    #[error("XML Encryption policy violation: {0}")]
850    Policy(#[from] crate::policy::PolicyViolation),
851
852    /// The selected cryptographic provider rejected or failed an operation.
853    #[error("cryptographic provider error: {0}")]
854    Provider(#[from] crate::provider::ProviderError),
855
856    /// XML document parsing failed.
857    #[error("XML parsing error: {0}")]
858    XmlParse(#[from] crate::xml::dom::ParseError),
859    /// The owned XML document boundary rejected an identity or mutation.
860    #[error("XML document error: {0}")]
861    Document(#[from] crate::document::XmlDocumentError),
862    /// Required child element or attribute was absent.
863    #[error("missing required {0}")]
864    MissingRequired(&'static str),
865    /// The XML element order or namespace is invalid for the XMLEnc profile.
866    #[error("invalid encrypted structure: {0}")]
867    InvalidStructure(String),
868    /// The compiled operation graph rejected an identity, dependency, or generation invariant.
869    #[error("invalid XML Encryption operation plan: {0}")]
870    OperationPlan(String),
871    /// The selected operation-start node ID is absent or resolves ambiguously.
872    #[error("selected node ID is missing or ambiguous: {id}")]
873    SelectedNodeUnavailable {
874        /// Caller-supplied node identifier.
875        id: String,
876    },
877    /// An algorithm URI is not supported by this build.
878    #[error("unsupported encryption algorithm: {0}")]
879    UnsupportedAlgorithm(String),
880    /// Base64 input is invalid or exceeds the configured input bound.
881    #[error("invalid base64 data: {0}")]
882    Base64(String),
883    /// A decoded cipher value is too short for its algorithm's framing.
884    #[error("{algorithm} ciphertext is too short: need at least {minimum} bytes, got {actual}")]
885    DataTooShort {
886        /// Algorithm name.
887        algorithm: &'static str,
888        /// Minimum valid byte length.
889        minimum: usize,
890        /// Actual byte length.
891        actual: usize,
892    },
893    /// CBC ciphertext is not a non-empty multiple of the selected cipher's block size.
894    #[error(
895        "{algorithm} ciphertext length must be a non-zero multiple of {block} bytes, got {actual}"
896    )]
897    InvalidCbcCiphertextLength {
898        /// Selected content cipher.
899        algorithm: DataEncryptionAlgorithm,
900        /// Cipher's block width in bytes.
901        block: usize,
902        /// Ciphertext body width, excluding the IV.
903        actual: usize,
904    },
905    /// XMLEnc random padding is invalid.
906    ///
907    /// No decrypted padding details are exposed. This does not authenticate CBC
908    /// ciphertexts or make success/failure safe to expose to an attacker.
909    #[error("invalid XMLEnc padding")]
910    InvalidPadding,
911    /// GCM authentication failed.
912    #[error("AES-GCM authentication failed")]
913    AeadAuthenticationFailed,
914    /// A supplied content key is not the expected size.
915    #[error("{algorithm:?} requires a {expected}-byte key, got {actual}")]
916    InvalidKeySize {
917        /// Content algorithm requiring the key.
918        algorithm: DataEncryptionAlgorithm,
919        /// Expected key size.
920        expected: usize,
921        /// Actual key size.
922        actual: usize,
923    },
924    /// An unauthenticated content algorithm cannot safely select among keys.
925    #[error(
926        "{algorithm:?} cannot safely select among {actual} unordered decryption key candidates"
927    )]
928    AmbiguousKeyCandidates {
929        /// Unauthenticated algorithm for which key success is ambiguous.
930        algorithm: DataEncryptionAlgorithm,
931        /// Number of unresolved candidate keys.
932        actual: usize,
933    },
934    /// A supplied AES key-encryption key is not the size declared by EncryptedKey.
935    #[error("{algorithm:?} requires a {expected}-byte KEK, got {actual}")]
936    InvalidKekSize {
937        /// Key-wrap algorithm requiring the KEK.
938        algorithm: KeyWrapAlgorithm,
939        /// Expected KEK size.
940        expected: usize,
941        /// Actual KEK size.
942        actual: usize,
943    },
944    /// A wrapped-key input or provider output has invalid algorithm framing.
945    #[error("wrapped-key value must be {expected} bytes, got {actual}")]
946    InvalidWrappedKeyLength {
947        /// Exact wrapped length required by the algorithm and key context.
948        expected: usize,
949        /// Actual input or provider output length.
950        actual: usize,
951    },
952    /// Encryption configuration is internally inconsistent.
953    #[error("invalid encryption configuration: {0}")]
954    InvalidEncryptionConfig(String),
955    /// No caller-provided resolver could supply a usable key.
956    #[error("no suitable decryption key was resolved")]
957    KeyNotFound,
958    /// No `EncryptedData` matched the requested document selection.
959    #[error("no matching EncryptedData element was found")]
960    EncryptedDataNotFound,
961    /// More than one `EncryptedData` matched the requested document selection.
962    #[error("more than one EncryptedData element matched; select one by Id")]
963    AmbiguousEncryptedData,
964    /// No source element matched the requested encryption target.
965    #[error("no matching element was found for encryption")]
966    EncryptionTargetNotFound,
967    /// More than one source element matched the requested encryption target.
968    #[error("more than one element matched the encryption target")]
969    AmbiguousEncryptionTarget,
970    /// Document replacement requires an XML `Type` declaration.
971    #[error("EncryptedData must declare Element or Content Type for document replacement")]
972    ReplacementRequiresXml,
973    /// RSA-OAEP session-key recovery failed.
974    #[error("RSA-OAEP key unwrap failed: {0}")]
975    Rsa(String),
976    /// RSA-OAEP session-key wrapping failed.
977    #[error("RSA-OAEP key wrap failed: {0}")]
978    RsaEncrypt(String),
979    /// RFC 3394 integrity validation failed while unwrapping a key.
980    #[error("AES key unwrap failed integrity validation")]
981    KeyWrapIntegrity,
982    /// Operating-system randomness was unavailable.
983    #[error("operating-system random number generation failed: {0}")]
984    Rng(String),
985    /// Generated XML could not be serialized.
986    #[error("XML encryption serialization failed: {0}")]
987    XmlSerialize(String),
988    /// XML-declared plaintext could not be decoded as UTF-8.
989    #[error("decrypted XML is not valid UTF-8: {0}")]
990    Utf8(#[from] std::string::FromUtf8Error),
991}
992
993impl From<crate::operation::OperationPlanError> for XmlEncError {
994    fn from(error: crate::operation::OperationPlanError) -> Self {
995        Self::OperationPlan(error.to_string())
996    }
997}
998
999impl fmt::Display for DataEncryptionAlgorithm {
1000    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1001        formatter.write_str(match self {
1002            #[cfg(feature = "legacy-algorithms")]
1003            Self::TripleDesCbc => "Triple DES CBC",
1004            #[cfg(feature = "legacy-algorithms")]
1005            Self::Aes192Cbc => "AES-192-CBC",
1006            #[cfg(feature = "legacy-algorithms")]
1007            Self::Aes192Gcm => "AES-192-GCM",
1008            Self::Aes128Cbc => "AES-128-CBC",
1009            Self::Aes256Cbc => "AES-256-CBC",
1010            Self::Aes128Gcm => "AES-128-GCM",
1011            Self::Aes256Gcm => "AES-256-GCM",
1012        })
1013    }
1014}
1015
1016#[cfg(test)]
1017mod framing_tests {
1018    use super::*;
1019
1020    #[test]
1021    fn cbc_framing_error_names_the_selected_cipher() {
1022        // Diagnostics must identify the actual cipher and its block width,
1023        // rather than reporting AES framing for a DES input.
1024        let error = validate_ciphertext_framing(DataEncryptionAlgorithm::Aes128Cbc, 33)
1025            .expect_err("AES ciphertext body is not block-aligned");
1026        assert_eq!(
1027            error.to_string(),
1028            "AES-128-CBC ciphertext length must be a non-zero multiple of 16 bytes, got 17"
1029        );
1030        #[cfg(feature = "legacy-algorithms")]
1031        {
1032            let error = validate_ciphertext_framing(DataEncryptionAlgorithm::TripleDesCbc, 17)
1033                .expect_err("3DES ciphertext body is not block-aligned");
1034            assert_eq!(
1035                error.to_string(),
1036                "Triple DES CBC ciphertext length must be a non-zero multiple of 8 bytes, got 9"
1037            );
1038        }
1039    }
1040}