1use base64::{decoded_len_estimate, 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
14pub fn decode_pem(input: &str, policy: PemDecodePolicy<'_>) -> Result<PemDocument, PemError> {
16 if input.is_empty() {
17 return Err(PemError::EmptyInput);
18 }
19 if input.len() > policy.max_input_len {
20 return Err(PemError::InputTooLarge);
21 }
22 if policy.max_der_len == 0 || policy.allowed_labels.is_empty() {
23 return Err(PemError::InvalidOptions);
24 }
25
26 let normalized = Zeroizing::new(normalize_line_endings(input)?);
27 let mut lines = normalized.split('\n');
28
29 let begin_line = next_nonempty_line(&mut lines).ok_or(PemError::MissingBegin)?;
30 if !begin_line.starts_with(BEGIN_PREFIX) {
31 return Err(PemError::MissingBegin);
32 }
33 let begin_label = parse_boundary_label(begin_line, BEGIN_PREFIX)?;
34 let label = PemLabel::parse(begin_label)?;
35 if !policy.allowed_labels.contains(&label) {
36 return Err(PemError::UnsupportedLabel);
37 }
38
39 let encoded_limit = encoded_len_limit(policy.max_der_len)?;
40 let body_capacity = input.len().min(encoded_limit);
43 let mut body = Zeroizing::new(String::with_capacity(body_capacity));
44 let mut found_end = false;
45
46 for line in lines {
47 if line.is_empty() {
48 continue;
49 }
50 if found_end {
51 return Err(PemError::InvalidBoundary);
52 }
53 if line.starts_with(END_PREFIX) {
54 let end_label = parse_boundary_label(line, END_PREFIX)?;
55 if end_label != label.as_str() {
56 return Err(PemError::LabelMismatch);
57 }
58 found_end = true;
59 continue;
60 }
61 if line.starts_with(BEGIN_PREFIX) {
62 return Err(PemError::InvalidBoundary);
63 }
64 if !line.bytes().all(is_base64_body_byte) {
65 return Err(PemError::InvalidBody);
66 }
67 let next_len = body
68 .len()
69 .checked_add(line.len())
70 .ok_or(PemError::InvalidOptions)?;
71 if next_len > encoded_limit {
72 return Err(PemError::DerTooLarge);
73 }
74 body.push_str(line);
75 }
76
77 if !found_end {
78 return Err(PemError::MissingEnd);
79 }
80 if body.is_empty() {
81 return Err(PemError::InvalidBody);
82 }
83
84 let mut der = Zeroizing::new(vec![0_u8; decoded_len_estimate(body.len())]);
87 let decoded_length = STANDARD
88 .decode_slice(body.as_bytes(), der.as_mut_slice())
89 .map_err(|_| PemError::InvalidBase64)?;
90 der.truncate(decoded_length);
91 if der.is_empty() || der.len() > policy.max_der_len {
92 return Err(PemError::DerTooLarge);
93 }
94
95 Ok(PemDocument { label, der })
96}
97
98fn normalize_line_endings(input: &str) -> Result<String, PemError> {
99 let mut output = String::with_capacity(input.len());
103 let bytes = input.as_bytes();
104 let mut cursor = 0_usize;
105 while cursor < bytes.len() {
106 let start = cursor;
107 while cursor < bytes.len() && bytes[cursor] != b'\r' {
108 cursor = cursor.checked_add(1).ok_or(PemError::InvalidOptions)?;
109 }
110 output.push_str(&input[start..cursor]);
111 if cursor == bytes.len() {
112 break;
113 }
114 output.push('\n');
115 cursor = cursor.checked_add(1).ok_or(PemError::InvalidOptions)?;
116 if cursor < bytes.len() && bytes[cursor] == b'\n' {
117 cursor = cursor.checked_add(1).ok_or(PemError::InvalidOptions)?;
118 }
119 }
120 Ok(output)
121}
122
123fn next_nonempty_line<'a>(lines: &mut impl Iterator<Item = &'a str>) -> Option<&'a str> {
124 lines.find(|line| !line.is_empty())
125}
126
127fn parse_boundary_label<'a>(line: &'a str, prefix: &str) -> Result<&'a str, PemError> {
128 let remainder = line.strip_prefix(prefix).ok_or(PemError::InvalidBoundary)?;
129 let label = remainder
130 .strip_suffix(BOUNDARY_SUFFIX)
131 .ok_or(PemError::InvalidBoundary)?;
132 if label.is_empty() || label.as_bytes().iter().any(|byte| !is_label_byte(*byte)) {
133 return Err(PemError::InvalidBoundary);
134 }
135 Ok(label)
136}
137
138fn encoded_len_limit(max_der_len: usize) -> Result<usize, PemError> {
139 let groups = max_der_len.checked_add(2).ok_or(PemError::InvalidOptions)? / 3;
140 groups.checked_mul(4).ok_or(PemError::InvalidOptions)
141}
142
143fn is_label_byte(byte: u8) -> bool {
144 byte == b' ' || byte == b'-' || byte.is_ascii_uppercase() || byte.is_ascii_digit()
145}
146
147fn is_base64_body_byte(byte: u8) -> bool {
148 byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'=')
149}