ssh_key/public/
ssh_format.rs

1//! Support for OpenSSH-formatted public keys, a.k.a. `SSH-format`.
2//!
3//! These keys have the form:
4//!
5//! ```text
6//! <algorithm id> <base64 key data> <comment>
7//! ```
8//!
9//! ## Example
10//!
11//! ```text
12//! ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com
13//! ```
14
15use crate::Result;
16use core::str;
17use encoding::{Base64Writer, Encode};
18
19#[cfg(feature = "alloc")]
20use {alloc::string::String, encoding::CheckedSum};
21
22/// OpenSSH public key (a.k.a. `SSH-format`) decoder/encoder.
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub(crate) struct SshFormat<'a> {
25    /// Algorithm identifier
26    pub(crate) algorithm_id: &'a str,
27
28    /// Base64-encoded key data
29    pub(crate) base64_data: &'a [u8],
30
31    /// Comment
32    #[cfg_attr(not(feature = "alloc"), allow(dead_code))]
33    pub(crate) comment: &'a str,
34}
35
36impl<'a> SshFormat<'a> {
37    /// Parse the given binary data.
38    pub(crate) fn decode(mut bytes: &'a [u8]) -> Result<Self> {
39        let algorithm_id = decode_segment_str(&mut bytes)?;
40        let base64_data = decode_segment(&mut bytes)?;
41        let comment = str::from_utf8(bytes)?.trim_end();
42
43        if algorithm_id.is_empty() || base64_data.is_empty() {
44            // TODO(tarcieri): better errors for these cases?
45            return Err(encoding::Error::Length.into());
46        }
47
48        Ok(Self {
49            algorithm_id,
50            base64_data,
51            comment,
52        })
53    }
54
55    /// Encode data with OpenSSH public key encapsulation.
56    pub(crate) fn encode<'o, K>(
57        algorithm_id: &str,
58        key: &K,
59        comment: &str,
60        out: &'o mut [u8],
61    ) -> Result<&'o str>
62    where
63        K: Encode,
64    {
65        let mut offset = 0;
66        encode_str(out, &mut offset, algorithm_id)?;
67        encode_str(out, &mut offset, " ")?;
68
69        let mut writer = Base64Writer::new(&mut out[offset..])?;
70        key.encode(&mut writer)?;
71        let base64_len = writer.finish()?.len();
72
73        offset = offset
74            .checked_add(base64_len)
75            .ok_or(encoding::Error::Length)?;
76
77        if !comment.is_empty() {
78            encode_str(out, &mut offset, " ")?;
79            encode_str(out, &mut offset, comment)?;
80        }
81
82        Ok(str::from_utf8(&out[..offset])?)
83    }
84
85    /// Encode string with OpenSSH public key encapsulation.
86    #[cfg(feature = "alloc")]
87    pub(crate) fn encode_string<K>(algorithm_id: &str, key: &K, comment: &str) -> Result<String>
88    where
89        K: Encode,
90    {
91        let encoded_len = [
92            2, // interstitial spaces
93            algorithm_id.len(),
94            base64_len_approx(key.encoded_len()?),
95            comment.len(),
96        ]
97        .checked_sum()?;
98
99        let mut out = vec![0u8; encoded_len];
100        let actual_len = Self::encode(algorithm_id, key, comment, &mut out)?.len();
101        out.truncate(actual_len);
102        Ok(String::from_utf8(out)?)
103    }
104}
105
106/// Get the estimated length of data when encoded as Base64.
107///
108/// This is an upper bound where the actual length might be slightly shorter,
109/// and can be used to estimate the capacity of an output buffer. However, the
110/// final result may need to be sliced and should use the actual encoded length
111/// rather than this estimate.
112#[cfg(feature = "alloc")]
113fn base64_len_approx(input_len: usize) -> usize {
114    (((input_len.saturating_mul(4)) / 3).saturating_add(3)) & !3
115}
116
117/// Parse a segment of the public key.
118fn decode_segment<'a>(bytes: &mut &'a [u8]) -> Result<&'a [u8]> {
119    let start = *bytes;
120    let mut len = 0usize;
121
122    loop {
123        match *bytes {
124            [b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'-' | b'/' | b'=' | b'@' | b'.', rest @ ..] =>
125            {
126                // Valid character; continue
127                *bytes = rest;
128                len = len.checked_add(1).ok_or(encoding::Error::Length)?;
129            }
130            [b' ', rest @ ..] => {
131                // Encountered space; we're done
132                *bytes = rest;
133                return start
134                    .get(..len)
135                    .ok_or_else(|| encoding::Error::Length.into());
136            }
137            [_, ..] => {
138                // Invalid character
139                return Err(encoding::Error::CharacterEncoding.into());
140            }
141            [] => {
142                // End of input, could be truncated or could be no comment
143                return start
144                    .get(..len)
145                    .ok_or_else(|| encoding::Error::Length.into());
146            }
147        }
148    }
149}
150
151/// Parse a segment of the public key as a `&str`.
152fn decode_segment_str<'a>(bytes: &mut &'a [u8]) -> Result<&'a str> {
153    str::from_utf8(decode_segment(bytes)?).map_err(|_| encoding::Error::CharacterEncoding.into())
154}
155
156/// Encode a segment of the public key.
157fn encode_str(out: &mut [u8], offset: &mut usize, s: &str) -> Result<()> {
158    let bytes = s.as_bytes();
159
160    if out.len()
161        < offset
162            .checked_add(bytes.len())
163            .ok_or(encoding::Error::Length)?
164    {
165        return Err(encoding::Error::Length.into());
166    }
167
168    out[*offset..][..bytes.len()].copy_from_slice(bytes);
169    *offset = offset
170        .checked_add(bytes.len())
171        .ok_or(encoding::Error::Length)?;
172
173    Ok(())
174}
175
176#[cfg(test)]
177mod tests {
178    use super::SshFormat;
179
180    const EXAMPLE_KEY: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com";
181
182    #[test]
183    fn decode() {
184        let encapsulation = SshFormat::decode(EXAMPLE_KEY.as_bytes()).unwrap();
185        assert_eq!(encapsulation.algorithm_id, "ssh-ed25519");
186        assert_eq!(
187            encapsulation.base64_data,
188            b"AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti"
189        );
190        assert_eq!(encapsulation.comment, "user@example.com");
191    }
192}