ssh_key/public/
openssh.rs

1//! Support for OpenSSH-formatted public keys.
2//!
3//! These keys have the form:
4//!
5//! ```text
6//! <algorithm id> <base64 data> <comment>
7//! ```
8//!
9//! ## Example
10//!
11//! ```text
12//! ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com
13//! ```
14
15use crate::{writer::Base64Writer, Error, Result};
16use core::str;
17
18/// OpenSSH public key encapsulation parser.
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub(crate) struct Encapsulation<'a> {
21    /// Algorithm identifier
22    pub(crate) algorithm_id: &'a str,
23
24    /// Base64-encoded key data
25    pub(crate) base64_data: &'a [u8],
26
27    /// Comment
28    #[cfg_attr(not(feature = "alloc"), allow(dead_code))]
29    pub(crate) comment: &'a str,
30}
31
32impl<'a> Encapsulation<'a> {
33    /// Parse the given binary data.
34    pub(crate) fn decode(mut bytes: &'a [u8]) -> Result<Self> {
35        let algorithm_id = decode_segment_str(&mut bytes)?;
36        let base64_data = decode_segment(&mut bytes)?;
37        let comment = str::from_utf8(bytes)
38            .map_err(|_| Error::CharacterEncoding)?
39            .trim_end();
40        if algorithm_id.is_empty() || base64_data.is_empty() {
41            // TODO(tarcieri): better errors for these cases?
42            return Err(Error::Length);
43        }
44        Ok(Self {
45            algorithm_id,
46            base64_data,
47            comment,
48        })
49    }
50
51    /// Encode data with OpenSSH public key encapsulation.
52    pub(crate) fn encode<'o, F>(
53        out: &'o mut [u8],
54        algorithm_id: &str,
55        comment: &str,
56        f: F,
57    ) -> Result<&'o str>
58    where
59        F: FnOnce(&mut Base64Writer<'_>) -> Result<()>,
60    {
61        let mut offset = 0;
62        encode_str(out, &mut offset, algorithm_id)?;
63        encode_str(out, &mut offset, " ")?;
64
65        let mut writer = Base64Writer::new(&mut out[offset..])?;
66        f(&mut writer)?;
67        let base64_len = writer.finish()?.len();
68
69        offset = offset.checked_add(base64_len).ok_or(Error::Length)?;
70        if !comment.is_empty() {
71            encode_str(out, &mut offset, " ")?;
72            encode_str(out, &mut offset, comment)?;
73        }
74        Ok(str::from_utf8(&out[..offset])?)
75    }
76}
77
78/// Parse a segment of the public key.
79fn decode_segment<'a>(bytes: &mut &'a [u8]) -> Result<&'a [u8]> {
80    let start = *bytes;
81    let mut len = 0usize;
82
83    loop {
84        match *bytes {
85            [b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'-' | b'/' | b'=' | b'@' | b'.', rest @ ..] =>
86            {
87                // Valid character; continue
88                *bytes = rest;
89                len = len.checked_add(1).ok_or(Error::Length)?;
90            }
91            [b' ', rest @ ..] => {
92                // Encountered space; we're done
93                *bytes = rest;
94                return start.get(..len).ok_or(Error::Length);
95            }
96            [_, ..] => {
97                // Invalid character
98                return Err(Error::CharacterEncoding);
99            }
100            [] => {
101                // End of input, could be truncated or could be no comment
102                return start.get(..len).ok_or(Error::Length);
103            }
104        }
105    }
106}
107
108/// Parse a segment of the public key as a `&str`.
109fn decode_segment_str<'a>(bytes: &mut &'a [u8]) -> Result<&'a str> {
110    str::from_utf8(decode_segment(bytes)?).map_err(|_| Error::CharacterEncoding)
111}
112
113/// Encode a segment of the public key.
114fn encode_str(out: &mut [u8], offset: &mut usize, s: &str) -> Result<()> {
115    let bytes = s.as_bytes();
116
117    if out.len() < offset.checked_add(bytes.len()).ok_or(Error::Length)? {
118        return Err(Error::Length);
119    }
120
121    out[*offset..][..bytes.len()].copy_from_slice(bytes);
122    *offset = offset.checked_add(bytes.len()).ok_or(Error::Length)?;
123    Ok(())
124}
125
126#[cfg(test)]
127mod tests {
128    use super::Encapsulation;
129
130    const EXAMPLE_KEY: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com";
131
132    #[test]
133    fn decode() {
134        let encapsulation = Encapsulation::decode(EXAMPLE_KEY.as_bytes()).unwrap();
135        assert_eq!(encapsulation.algorithm_id, "ssh-ed25519");
136        assert_eq!(
137            encapsulation.base64_data,
138            b"AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti"
139        );
140        assert_eq!(encapsulation.comment, "user@example.com");
141    }
142}