Skip to main content

codec_pem/
decode.rs

1// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use base64::{engine::general_purpose::STANDARD, Engine as _};
6use zeroize::Zeroizing;
7
8use crate::{PemDecodePolicy, PemDocument, PemError, PemLabel};
9
10const BEGIN_PREFIX: &str = "-----BEGIN ";
11const END_PREFIX: &str = "-----END ";
12const BOUNDARY_SUFFIX: &str = "-----";
13
14/// Decode PEM text armor into a label and DER body.
15pub fn decode_pem(input: &str, policy: PemDecodePolicy<'_>) -> Result<PemDocument, PemError> {
16    if input.is_empty() || input.len() > policy.max_input_len {
17        return Err(PemError::InputTooLarge);
18    }
19    if policy.max_der_len == 0 || policy.allowed_labels.is_empty() {
20        return Err(PemError::InvalidOptions);
21    }
22
23    let normalized = Zeroizing::new(normalize_line_endings(input));
24    let mut lines = normalized.split('\n');
25
26    let begin_line = next_nonempty_line(&mut lines).ok_or(PemError::MissingBegin)?;
27    let begin_label = parse_boundary_label(begin_line, BEGIN_PREFIX)?;
28    let label = PemLabel::parse(begin_label)?;
29    if !policy.allowed_labels.contains(&label) {
30        return Err(PemError::UnsupportedLabel);
31    }
32
33    let mut body = Zeroizing::new(String::new());
34    let mut found_end = false;
35    let encoded_limit = encoded_len_limit(policy.max_der_len)?;
36
37    for line in lines {
38        if line.is_empty() {
39            continue;
40        }
41        if found_end {
42            return Err(PemError::InvalidBoundary);
43        }
44        if line.starts_with(END_PREFIX) {
45            let end_label = parse_boundary_label(line, END_PREFIX)?;
46            if end_label != label.as_str() {
47                return Err(PemError::LabelMismatch);
48            }
49            found_end = true;
50            continue;
51        }
52        if line.starts_with(BEGIN_PREFIX) {
53            return Err(PemError::InvalidBoundary);
54        }
55        if !line.bytes().all(is_base64_body_byte) {
56            return Err(PemError::InvalidBody);
57        }
58        let next_len = body
59            .len()
60            .checked_add(line.len())
61            .ok_or(PemError::InvalidOptions)?;
62        if next_len > encoded_limit {
63            return Err(PemError::DerTooLarge);
64        }
65        body.push_str(line);
66    }
67
68    if !found_end {
69        return Err(PemError::MissingEnd);
70    }
71    if body.is_empty() {
72        return Err(PemError::InvalidBody);
73    }
74
75    let der = Zeroizing::new(
76        STANDARD
77            .decode(body.as_bytes())
78            .map_err(|_| PemError::InvalidBase64)?,
79    );
80    if der.is_empty() || der.len() > policy.max_der_len {
81        return Err(PemError::DerTooLarge);
82    }
83
84    Ok(PemDocument { label, der })
85}
86
87fn normalize_line_endings(input: &str) -> String {
88    input.replace("\r\n", "\n").replace('\r', "\n")
89}
90
91fn next_nonempty_line<'a>(lines: &mut impl Iterator<Item = &'a str>) -> Option<&'a str> {
92    lines.find(|line| !line.is_empty())
93}
94
95fn parse_boundary_label<'a>(line: &'a str, prefix: &str) -> Result<&'a str, PemError> {
96    let remainder = line.strip_prefix(prefix).ok_or(PemError::InvalidBoundary)?;
97    let label = remainder
98        .strip_suffix(BOUNDARY_SUFFIX)
99        .ok_or(PemError::InvalidBoundary)?;
100    if label.is_empty() || label.as_bytes().iter().any(|byte| !is_label_byte(*byte)) {
101        return Err(PemError::InvalidBoundary);
102    }
103    Ok(label)
104}
105
106fn encoded_len_limit(max_der_len: usize) -> Result<usize, PemError> {
107    let groups = max_der_len.checked_add(2).ok_or(PemError::InvalidOptions)? / 3;
108    groups.checked_mul(4).ok_or(PemError::InvalidOptions)
109}
110
111fn is_label_byte(byte: u8) -> bool {
112    byte == b' ' || byte == b'-' || byte.is_ascii_uppercase() || byte.is_ascii_digit()
113}
114
115fn is_base64_body_byte(byte: u8) -> bool {
116    byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'=')
117}