Skip to main content

structured_cms/personal/
protection.rs

1//! Original password-protection parameters; no KDF, password or cipher engine.
2//!
3//! Wide iteration/key-length values stay borrowed INTEGERs. A selected backend
4//! must admit its complete KDF/cipher cost before work, including dependencies.
5//! Recognized parameter syntax is checked even for an unsupported backend.
6
7use super::{algorithm, encoding, end, equal_oid, invalid, number, positive, sequence};
8use crate::{Error, ErrorCode, Octets, Result, signer::required};
9use structured_asn1::{Integer, Oid, Reader, Tag, universal};
10use structured_pkix::AlgorithmIdentifier;
11
12const FIELD: &str = "personal.protection";
13/// PBES2, RFC8018 Appendix A.4.
14pub const PBES2: Oid<'static> = Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 1, 5, 13]);
15/// PBMAC1, RFC8018 Appendix A.5 / RFC9879.
16pub const PBMAC1: Oid<'static> = Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 1, 5, 14]);
17/// PBKDF2, RFC8018 Appendix A.2.
18pub const PBKDF2: Oid<'static> = Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 1, 5, 12]);
19const SHA1: Oid<'static> = Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 2, 7]);
20
21/// PBKDF2 salt CHOICE; unknown source remains a real original identifier.
22pub enum Salt<'a> {
23    /// Explicit logical salt octets, possibly BER segmented.
24    Specified(Octets<'a>),
25    /// Explicit source algorithm, never replaced by an empty/default salt.
26    Source(AlgorithmIdentifier<'a>),
27}
28/// PBKDF2 PRF with honest DEFAULT provenance.
29#[derive(Debug, Clone, Copy)]
30pub enum Prf<'a> {
31    /// The ASN.1 default HMAC-SHA1 with NULL parameters, not fabricated DER.
32    DefaultSha1,
33    /// Original written PRF and parameters.
34    Written(AlgorithmIdentifier<'a>),
35}
36/// RFC8018 PBKDF2-params, independent of a backend's numeric limits.
37pub struct Pbkdf2<'a> {
38    /// The original specified salt or salt-source choice.
39    pub salt: Salt<'a>,
40    /// Strictly positive, arbitrary-width iterationCount.
41    pub iterations: Integer<'a>,
42    /// Optional strictly positive output length in octets.
43    pub key_length: Option<Integer<'a>>,
44    /// Default or original written PRF identity.
45    pub prf: Prf<'a>,
46}
47impl<'a> Pbkdf2<'a> {
48    /// Reads parameters through their original `AlgorithmIdentifier` provenance.
49    /// A different Parser is a terminal processing error before parameter work.
50    pub fn read(identifier: AlgorithmIdentifier<'a>, parent: &Reader<'a>) -> Result<Self> {
51        session(identifier, parent)?;
52        if !equal_oid(parent, identifier.algorithm(), PBKDF2)? {
53            return Err(Error::schema(
54                ErrorCode::AlgorithmSyntax,
55                FIELD,
56                identifier.offset(),
57            ));
58        }
59        let mut reader = parameters(identifier, parent)?;
60        let value = required(&mut reader, FIELD)?;
61        let mut inside = sequence(&reader, &value)?;
62        end(&reader)?;
63        let salt_element = required(&mut inside, FIELD)?;
64        let salt = if salt_element.tag() == Tag::constructed(universal::SEQUENCE) {
65            Salt::Source(
66                structured_pkix::algorithm::read(&inside, &salt_element, FIELD)
67                    .map_err(Error::pkix)?,
68            )
69        } else {
70            Salt::Specified(Octets::read(&inside, &salt_element, FIELD)?)
71        };
72        let iterations = number(&mut inside)?;
73        positive(iterations, &value)?;
74        let mut next = inside.next_element().map_err(encoding)?;
75        let key_length =
76            if let Some(element) = next.filter(|v| v.tag() == Tag::primitive(universal::INTEGER)) {
77                let length = inside.integer(&element).map_err(encoding)?;
78                positive(length, &element)?;
79                next = inside.next_element().map_err(encoding)?;
80                Some(length)
81            } else {
82                None
83            };
84        let prf = if let Some(element) = next {
85            let written =
86                structured_pkix::algorithm::read(&inside, &element, FIELD).map_err(Error::pkix)?;
87            hmac_width(written, &inside)?;
88            if equal_oid(&inside, written.algorithm(), SHA1)? && written.has_null_parameter() {
89                // RFC8018 A.2's DEFAULT contains a present NULL. Appendix C
90                // makes parameters OPTIONAL: a written absent parameter is
91                // a distinct ASN.1 value, not an invented NULL/default.
92                if inside.encoding().is_der() {
93                    return Err(invalid(ErrorCode::DefaultWritten, &element));
94                }
95            }
96            Prf::Written(written)
97        } else {
98            Prf::DefaultSha1
99        };
100        end(&inside)?;
101        Ok(Self {
102            salt,
103            iterations,
104            key_length,
105            prf,
106        })
107    }
108}
109
110/// PBES2 / PBMAC1's common two-algorithm parameter grammar.
111pub struct Pair<'a> {
112    /// Original keyDerivationFunc; an unknown mechanism stays explicit.
113    pub derivation: AlgorithmIdentifier<'a>,
114    /// Original encryptionScheme or messageAuthScheme.
115    pub scheme: AlgorithmIdentifier<'a>,
116    /// Parsed PBKDF2 parameters when that KDF is actually written.
117    pub pbkdf2: Option<Pbkdf2<'a>>,
118}
119impl<'a> Pair<'a> {
120    /// Reads the pair belonging to this original PBES2/PBMAC1 identifier.
121    pub fn read(identifier: AlgorithmIdentifier<'a>, parent: &Reader<'a>) -> Result<Self> {
122        session(identifier, parent)?;
123        if !equal_oid(parent, identifier.algorithm(), PBES2)?
124            && !equal_oid(parent, identifier.algorithm(), PBMAC1)?
125        {
126            return Err(Error::schema(
127                ErrorCode::AlgorithmSyntax,
128                FIELD,
129                identifier.offset(),
130            ));
131        }
132        let mut reader = parameters(identifier, parent)?;
133        let value = required(&mut reader, FIELD)?;
134        let mut inside = sequence(&reader, &value)?;
135        end(&reader)?;
136        let derivation = algorithm(&mut inside)?;
137        let scheme = algorithm(&mut inside)?;
138        end(&inside)?;
139        let pbkdf2 = if equal_oid(&inside, derivation.algorithm(), PBKDF2)? {
140            Some(Pbkdf2::read(derivation, &inside)?)
141        } else {
142            None
143        };
144        Ok(Self {
145            derivation,
146            scheme,
147            pbkdf2,
148        })
149    }
150}
151
152/// Salt/iteration parameter grammar shared by PBES1 and legacy PKCS#12 PBE.
153pub struct Legacy<'a> {
154    /// Original salt; PBES1 alone requires exactly eight octets.
155    pub salt: Octets<'a>,
156    /// Strictly positive original work factor, with no machine-width clamp.
157    pub iterations: Integer<'a>,
158}
159/// Structural protection classification, independent of algorithm permission.
160#[expect(
161    clippy::large_enum_variant,
162    reason = "one protection descriptor is consumed inline; boxing PBES2 would add an allocation for every password operation"
163)]
164pub enum Protection<'a> {
165    /// PBES2 key derivation and encryption scheme.
166    Pbes2(Pair<'a>),
167    /// PKCS#5 PBES1 (MD2/MD5/SHA1 with DES/RC2).
168    Pbes1(Legacy<'a>),
169    /// RFC7292 Appendix C legacy algorithms 1 through 6.
170    Pkcs12(Legacy<'a>),
171    /// Original unknown algorithm; no fallback or implicit PBES2.
172    Other(AlgorithmIdentifier<'a>),
173}
174impl<'a> Protection<'a> {
175    /// Reads recognized parameters before any password/provider side effects.
176    pub fn read(identifier: AlgorithmIdentifier<'a>, parent: &Reader<'a>) -> Result<Self> {
177        session(identifier, parent)?;
178        if equal_oid(parent, identifier.algorithm(), PBES2)? {
179            return Pair::read(identifier, parent).map(Self::Pbes2);
180        }
181        let mut pbes1 = false;
182        for last in [1, 3, 4, 6, 10, 11] {
183            pbes1 |= equal_oid(
184                parent,
185                identifier.algorithm(),
186                Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 1, 5, last]),
187            )?;
188        }
189        let mut pkcs12 = false;
190        for last in 1..=6 {
191            pkcs12 |= equal_oid(
192                parent,
193                identifier.algorithm(),
194                Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 1, 12, 1, last]),
195            )?;
196        }
197        if !pbes1 && !pkcs12 {
198            // Session was checked before dispatch. An unknown mechanism needs
199            // no second parameter cursor; its original open value stays intact.
200            return Ok(Self::Other(identifier));
201        }
202        let mut reader = parameters(identifier, parent)?;
203        let value = required(&mut reader, FIELD)?;
204        let mut inside = sequence(&reader, &value)?;
205        end(&reader)?;
206        let salt_value = required(&mut inside, FIELD)?;
207        let salt = Octets::read(&inside, &salt_value, FIELD)?;
208        let iterations = number(&mut inside)?;
209        positive(iterations, &value)?;
210        end(&inside)?;
211        if pbes1 && salt.len() != 8 {
212            return Err(invalid(ErrorCode::MalformedValue, &salt_value));
213        }
214        let result = Legacy { salt, iterations };
215        Ok(if pbes1 {
216            Self::Pbes1(result)
217        } else {
218            Self::Pkcs12(result)
219        })
220    }
221}
222
223fn parameters<'a>(identifier: AlgorithmIdentifier<'a>, parent: &Reader<'a>) -> Result<Reader<'a>> {
224    identifier
225        .parameters_reader_in(parent, FIELD)
226        .map_err(Error::pkix)?
227        .ok_or_else(|| Error::schema(ErrorCode::MissingField, FIELD, identifier.offset()))
228}
229
230fn session(identifier: AlgorithmIdentifier<'_>, parent: &Reader<'_>) -> Result<()> {
231    if !identifier.same_session(parent) {
232        return Err(Error::pkix(structured_pkix::Error::new(
233            structured_pkix::ErrorCode::ForeignReader,
234            FIELD,
235            identifier.offset(),
236        )));
237    }
238    Ok(())
239}
240
241pub(super) fn hmac_width(
242    identifier: AlgorithmIdentifier<'_>,
243    parent: &Reader<'_>,
244) -> Result<Option<usize>> {
245    session(identifier, parent)?;
246    let bytes = identifier.algorithm().as_bytes();
247    parent.charge(bytes.len() as u64 + 1).map_err(encoding)?;
248    let width = match bytes {
249        [42, 134, 72, 134, 247, 13, 2, 7] => 20,
250        [42, 134, 72, 134, 247, 13, 2, 8 | 12] => 28,
251        [42, 134, 72, 134, 247, 13, 2, 9 | 13] => 32,
252        [42, 134, 72, 134, 247, 13, 2, 10] => 48,
253        [42, 134, 72, 134, 247, 13, 2, 11] => 64,
254        _ => return Ok(None),
255    };
256    // RFC8018 A.2 registers NULL as the parameter type, while Appendix C's
257    // AlgorithmIdentifier makes the field OPTIONAL. Validate a present value
258    // without fabricating NULL when it is absent.
259    // https://www.rfc-editor.org/rfc/rfc8018#appendix-C
260    if identifier.parameters().is_some() && !identifier.has_null_parameter() {
261        return Err(Error::schema(
262            ErrorCode::AlgorithmSyntax,
263            FIELD,
264            identifier.offset(),
265        ));
266    }
267    Ok(Some(width))
268}