ssh_key/public/
ssh_format.rs1use crate::Result;
16use core::str;
17use encoding::{Base64Writer, Encode};
18
19#[cfg(feature = "alloc")]
20use {alloc::string::String, encoding::CheckedSum};
21
22#[derive(Clone, Debug, Eq, PartialEq)]
24pub(crate) struct SshFormat<'a> {
25 pub(crate) algorithm_id: &'a str,
27
28 pub(crate) base64_data: &'a [u8],
30
31 #[cfg_attr(not(feature = "alloc"), allow(dead_code))]
33 pub(crate) comment: &'a str,
34}
35
36impl<'a> SshFormat<'a> {
37 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 return Err(encoding::Error::Length.into());
46 }
47
48 Ok(Self {
49 algorithm_id,
50 base64_data,
51 comment,
52 })
53 }
54
55 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 #[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, 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#[cfg(feature = "alloc")]
113fn base64_len_approx(input_len: usize) -> usize {
114 (((input_len.saturating_mul(4)) / 3).saturating_add(3)) & !3
115}
116
117fn 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 *bytes = rest;
128 len = len.checked_add(1).ok_or(encoding::Error::Length)?;
129 }
130 [b' ', rest @ ..] => {
131 *bytes = rest;
133 return start
134 .get(..len)
135 .ok_or_else(|| encoding::Error::Length.into());
136 }
137 [_, ..] => {
138 return Err(encoding::Error::CharacterEncoding.into());
140 }
141 [] => {
142 return start
144 .get(..len)
145 .ok_or_else(|| encoding::Error::Length.into());
146 }
147 }
148 }
149}
150
151fn 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
156fn 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}