Skip to main content

codec_pem/
encode.rs

1// SPDX-FileCopyrightText: 2026 ReallyMe LLC
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use base64::{encoded_len, engine::general_purpose::STANDARD, Engine as _};
6use zeroize::Zeroizing;
7
8use crate::{PemEncodeOptions, PemError, PemLabel};
9
10/// Encode DER bytes as PEM text armor.
11pub fn encode_pem(
12    label: PemLabel,
13    der: &[u8],
14    options: PemEncodeOptions,
15) -> Result<Zeroizing<String>, PemError> {
16    let output_length = preflight_pem_encoded_length(label, der.len(), options)?;
17    let encoded_length = encoded_len(der.len(), true).ok_or(PemError::InvalidOptions)?;
18    let mut encoded = Zeroizing::new(vec![0_u8; encoded_length]);
19    let written = STANDARD
20        .encode_slice(der, encoded.as_mut_slice())
21        .map_err(|_| PemError::InvalidOptions)?;
22    if written != encoded_length {
23        return Err(PemError::InvalidOptions);
24    }
25
26    let newline = options.line_ending.as_str();
27    // Allocate the final secret-bearing buffer once. Growing a String after
28    // private-key armor has been written would free prior allocations without
29    // wiping them; an exact capacity prevents that remanence path.
30    let mut output = Zeroizing::new(String::with_capacity(output_length));
31    output.push_str("-----BEGIN ");
32    output.push_str(label.as_str());
33    output.push_str("-----");
34    output.push_str(newline);
35
36    for chunk in encoded.as_slice().chunks(options.line_width) {
37        let line = core::str::from_utf8(chunk).map_err(|_| PemError::InvalidBase64)?;
38        output.push_str(line);
39        output.push_str(newline);
40    }
41
42    output.push_str("-----END ");
43    output.push_str(label.as_str());
44    output.push_str("-----");
45    output.push_str(newline);
46
47    if output.len() != output_length {
48        return Err(PemError::InvalidOptions);
49    }
50
51    Ok(output)
52}
53
54/// Compute the exact PEM output length before allocating or copying DER.
55///
56/// # Errors
57///
58/// Returns a typed error for empty/oversized DER, invalid line width, or
59/// arithmetic overflow.
60pub fn preflight_pem_encoded_length(
61    label: PemLabel,
62    der_length: usize,
63    options: PemEncodeOptions,
64) -> Result<usize, PemError> {
65    if der_length == 0 {
66        return Err(PemError::EmptyDer);
67    }
68    if der_length > options.max_der_len {
69        return Err(PemError::DerTooLarge);
70    }
71    if options.line_width == 0 || options.line_width > 76 {
72        return Err(PemError::InvalidOptions);
73    }
74    let encoded_length = encoded_len(der_length, true).ok_or(PemError::InvalidOptions)?;
75    encoded_pem_length(
76        label.as_str().len(),
77        encoded_length,
78        options.line_width,
79        options.line_ending.as_str().len(),
80    )
81}
82
83fn encoded_pem_length(
84    label_length: usize,
85    encoded_length: usize,
86    line_width: usize,
87    newline_length: usize,
88) -> Result<usize, PemError> {
89    let boundary_length = "-----BEGIN "
90        .len()
91        .checked_add(label_length)
92        .and_then(|length| length.checked_add("-----".len()))
93        .and_then(|length| length.checked_add(newline_length))
94        .ok_or(PemError::InvalidOptions)?;
95    let footer_length = "-----END "
96        .len()
97        .checked_add(label_length)
98        .and_then(|length| length.checked_add("-----".len()))
99        .and_then(|length| length.checked_add(newline_length))
100        .ok_or(PemError::InvalidOptions)?;
101    let line_count = encoded_length
102        .checked_add(line_width.checked_sub(1).ok_or(PemError::InvalidOptions)?)
103        .ok_or(PemError::InvalidOptions)?
104        / line_width;
105    let body_newlines = line_count
106        .checked_mul(newline_length)
107        .ok_or(PemError::InvalidOptions)?;
108
109    boundary_length
110        .checked_add(encoded_length)
111        .and_then(|length| length.checked_add(body_newlines))
112        .and_then(|length| length.checked_add(footer_length))
113        .ok_or(PemError::InvalidOptions)
114}