Skip to content

Commit 609ef1e

Browse files
committed
ssh-key: extract a Comment type
Extracts a type which models the concerns of comments, namely that they can be encoded as an RFC4251 `string` type which can hold arbitrary binary data when stored in the binary serialization of a private key. This replaces the various accessor methods on `PublicKey` and `PrivateKey` for handling the various conversions.
1 parent 05c2bef commit 609ef1e

File tree

8 files changed

+198
-123
lines changed

8 files changed

+198
-123
lines changed

ssh-key/src/comment.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//! SSH key comment support.
2+
3+
use alloc::{borrow::ToOwned, boxed::Box, string::String, vec::Vec};
4+
use core::{
5+
convert::Infallible,
6+
fmt,
7+
str::{self, FromStr},
8+
};
9+
use encoding::{Decode, Encode, Error, Reader, Writer};
10+
11+
/// SSH key comment (e.g. email address of owner)
12+
///
13+
/// Comments may be found in both the binary serialization of [`PrivateKey`] as well as the text
14+
/// serialization of [`PublicKey`].
15+
///
16+
/// The binary serialization of [`PrivateKey`] stores the comment encoded as an [RFC4251]
17+
/// `string` type which can contain arbitrary binary data and does not necessarily represent valid
18+
/// UTF-8. To support round trip encoding of such comments.
19+
///
20+
/// To support round-trip encoding of such comments, this type also supports arbitrary binary data.
21+
///
22+
/// [RFC4251]: https://datatracker.ietf.org/doc/html/rfc4251#section-5
23+
/// [`PrivateKey`]: crate::PrivateKey
24+
/// [`PublicKey`]: crate::PublicKey
25+
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
26+
pub struct Comment(Box<[u8]>);
27+
28+
impl AsRef<[u8]> for Comment {
29+
fn as_ref(&self) -> &[u8] {
30+
self.as_bytes()
31+
}
32+
}
33+
34+
impl AsRef<str> for Comment {
35+
fn as_ref(&self) -> &str {
36+
self.as_str_lossy()
37+
}
38+
}
39+
40+
impl Decode for Comment {
41+
type Error = Error;
42+
43+
fn decode(reader: &mut impl Reader) -> encoding::Result<Self> {
44+
Vec::<u8>::decode(reader).map(Into::into)
45+
}
46+
}
47+
48+
impl Encode for Comment {
49+
fn encoded_len(&self) -> Result<usize, Error> {
50+
self.0.encoded_len()
51+
}
52+
53+
fn encode(&self, writer: &mut impl Writer) -> Result<(), Error> {
54+
self.0.encode(writer)
55+
}
56+
}
57+
58+
impl FromStr for Comment {
59+
type Err = Infallible;
60+
61+
fn from_str(s: &str) -> Result<Comment, Infallible> {
62+
Ok(s.into())
63+
}
64+
}
65+
66+
impl From<&str> for Comment {
67+
fn from(s: &str) -> Comment {
68+
Self::from(s.to_owned())
69+
}
70+
}
71+
72+
impl From<String> for Comment {
73+
fn from(s: String) -> Self {
74+
s.into_bytes().into()
75+
}
76+
}
77+
78+
impl From<Vec<u8>> for Comment {
79+
fn from(vec: Vec<u8>) -> Self {
80+
Self(vec.into_boxed_slice())
81+
}
82+
}
83+
84+
impl From<Comment> for Vec<u8> {
85+
fn from(comment: Comment) -> Vec<u8> {
86+
comment.0.into()
87+
}
88+
}
89+
90+
impl TryFrom<Comment> for String {
91+
type Error = Error;
92+
93+
fn try_from(comment: Comment) -> Result<String, Error> {
94+
comment.as_str().map(ToOwned::to_owned)
95+
}
96+
}
97+
98+
impl fmt::Display for Comment {
99+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100+
f.write_str(self.as_str_lossy())
101+
}
102+
}
103+
104+
impl Comment {
105+
/// Interpret the comment as raw binary data.
106+
pub fn as_bytes(&self) -> &[u8] {
107+
&self.0
108+
}
109+
110+
/// Interpret the comment as a UTF-8 string.
111+
pub fn as_str(&self) -> Result<&str, Error> {
112+
Ok(str::from_utf8(&self.0)?)
113+
}
114+
115+
/// Interpret the comment as a UTF-8 string.
116+
///
117+
/// This is the maximal prefix of the
118+
#[cfg(feature = "alloc")]
119+
pub fn as_str_lossy(&self) -> &str {
120+
for i in (1..=self.len()).rev() {
121+
if let Ok(s) = str::from_utf8(&self.0[..i]) {
122+
return s;
123+
}
124+
}
125+
126+
""
127+
}
128+
129+
/// Is the comment empty?
130+
pub fn is_empty(&self) -> bool {
131+
self.0.is_empty()
132+
}
133+
134+
/// Get the length of this comment in bytes.
135+
pub fn len(&self) -> usize {
136+
self.0.len()
137+
}
138+
}
139+
140+
#[cfg(test)]
141+
mod tests {}

ssh-key/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
//!
5252
//! // Key attributes
5353
//! assert_eq!(public_key.algorithm(), ssh_key::Algorithm::Ed25519);
54-
//! assert_eq!(public_key.comment(), "[email protected]");
54+
//! assert_eq!(public_key.comment().as_bytes(), b"[email protected]");
5555
//!
5656
//! // Key data: in this example an Ed25519 key
5757
//! if let Some(ed25519_public_key) = public_key.key_data().ed25519() {
@@ -100,7 +100,7 @@
100100
//!
101101
//! // Key attributes
102102
//! assert_eq!(private_key.algorithm(), ssh_key::Algorithm::Ed25519);
103-
//! assert_eq!(private_key.comment(), "[email protected]");
103+
//! assert_eq!(private_key.comment().as_bytes(), b"[email protected]");
104104
//!
105105
//! // Key data: in this example an Ed25519 key
106106
//! if let Some(ed25519_keypair) = private_key.key_data().ed25519() {
@@ -156,6 +156,8 @@ mod error;
156156
mod fingerprint;
157157
mod kdf;
158158

159+
#[cfg(feature = "alloc")]
160+
mod comment;
159161
#[cfg(feature = "std")]
160162
mod dot_ssh;
161163
#[cfg(feature = "ppk")]
@@ -183,6 +185,7 @@ pub use {
183185
crate::{
184186
algorithm::AlgorithmName,
185187
certificate::Certificate,
188+
comment::Comment,
186189
known_hosts::KnownHosts,
187190
signature::{Signature, SigningKey},
188191
sshsig::SshSig,

ssh-key/src/private.rs

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ pub use self::{
124124

125125
#[cfg(feature = "alloc")]
126126
pub use crate::{
127-
SshSig,
127+
Comment, SshSig,
128128
private::{
129129
dsa::{DsaKeypair, DsaPrivateKey},
130130
opaque::{OpaqueKeypair, OpaqueKeypairBytes, OpaquePrivateKeyBytes},
@@ -212,7 +212,7 @@ impl PrivateKey {
212212
///
213213
/// On `no_std` platforms, use `PrivateKey::from(key_data)` instead.
214214
#[cfg(feature = "alloc")]
215-
pub fn new(key_data: KeypairData, comment: impl Into<Vec<u8>>) -> Result<Self> {
215+
pub fn new(key_data: KeypairData, comment: impl Into<Comment>) -> Result<Self> {
216216
if key_data.is_encrypted() {
217217
return Err(Error::Encrypted);
218218
}
@@ -465,43 +465,8 @@ impl PrivateKey {
465465

466466
/// Comment on the key (e.g. email address).
467467
#[cfg(feature = "alloc")]
468-
#[deprecated(
469-
since = "0.7.0",
470-
note = "please use `comment_bytes`, `comment_str`, or `comment_str_lossy` instead"
471-
)]
472-
pub fn comment(&self) -> &str {
473-
self.comment_str_lossy()
474-
}
475-
476-
/// Comment on the key (e.g. email address).
477-
#[cfg(not(feature = "alloc"))]
478-
pub fn comment_bytes(&self) -> &[u8] {
479-
b""
480-
}
481-
482-
/// Comment on the key (e.g. email address).
483-
///
484-
/// Since comments can contain arbitrary binary data when decoded from a
485-
/// private key, this returns the raw bytes of the comment.
486-
#[cfg(feature = "alloc")]
487-
pub fn comment_bytes(&self) -> &[u8] {
488-
self.public_key.comment_bytes()
489-
}
490-
491-
/// Comment on the key (e.g. email address).
492-
///
493-
/// This returns a UTF-8 interpretation of the comment when valid.
494-
#[cfg(feature = "alloc")]
495-
pub fn comment_str(&self) -> core::result::Result<&str, str::Utf8Error> {
496-
self.public_key.comment_str()
497-
}
498-
499-
/// Comment on the key (e.g. email address).
500-
///
501-
/// This returns as much data as can be interpreted as valid UTF-8.
502-
#[cfg(feature = "alloc")]
503-
pub fn comment_str_lossy(&self) -> &str {
504-
self.public_key.comment_str_lossy()
468+
pub fn comment(&self) -> &Comment {
469+
self.public_key.comment()
505470
}
506471

507472
/// Cipher algorithm (a.k.a. `ciphername`).
@@ -575,7 +540,7 @@ impl PrivateKey {
575540

576541
/// Set the comment on the key.
577542
#[cfg(feature = "alloc")]
578-
pub fn set_comment(&mut self, comment: impl Into<Vec<u8>>) {
543+
pub fn set_comment(&mut self, comment: impl Into<Comment>) {
579544
self.public_key.set_comment(comment);
580545
}
581546

@@ -681,7 +646,7 @@ impl PrivateKey {
681646
checkint.encode(writer)?;
682647
checkint.encode(writer)?;
683648
self.key_data.encode(writer)?;
684-
self.comment_bytes().encode(writer)?;
649+
self.comment().encode(writer)?;
685650
writer.write(&PADDING_BYTES[..padding_len])?;
686651
Ok(())
687652
}
@@ -704,7 +669,7 @@ impl PrivateKey {
704669
[
705670
8, // 2 x uint32 checkints,
706671
self.key_data.encoded_len()?,
707-
self.comment_bytes().encoded_len()?,
672+
self.comment().encoded_len()?,
708673
]
709674
.checked_sum()
710675
}

ssh-key/src/public.rs

Lines changed: 10 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use encoding::{Base64Reader, Decode, Reader};
3535

3636
#[cfg(feature = "alloc")]
3737
use {
38-
crate::SshSig,
38+
crate::{Comment, SshSig},
3939
alloc::{
4040
borrow::ToOwned,
4141
string::{String, ToString},
@@ -67,7 +67,7 @@ use crate::PrivateKey;
6767
///
6868
/// 1. Algorithm identifier (in this example `ssh-ed25519`)
6969
/// 2. Key data encoded as Base64
70-
/// 3. Comment (optional): arbitrary label describing a key. Usually an email address
70+
/// 3. [`Comment`] (optional): arbitrary label describing a key. Usually an email address
7171
///
7272
/// The [`PublicKey::from_openssh`] and [`PublicKey::to_openssh`] methods can be
7373
/// used to decode/encode public keys, or alternatively, the [`FromStr`] and
@@ -99,15 +99,15 @@ pub struct PublicKey {
9999
/// binary data, so `Vec<u8>` is used to store the comment to ensure keys
100100
/// containing such comments successfully round-trip.
101101
#[cfg(feature = "alloc")]
102-
pub(crate) comment: Vec<u8>,
102+
pub(crate) comment: Comment,
103103
}
104104

105105
impl PublicKey {
106106
/// Create a new public key with the given comment.
107107
///
108108
/// On `no_std` platforms, use `PublicKey::from(key_data)` instead.
109109
#[cfg(feature = "alloc")]
110-
pub fn new(key_data: KeyData, comment: impl Into<Vec<u8>>) -> Self {
110+
pub fn new(key_data: KeyData, comment: impl Into<Comment>) -> Self {
111111
Self {
112112
key_data,
113113
comment: comment.into(),
@@ -152,7 +152,7 @@ impl PublicKey {
152152
#[cfg(not(feature = "alloc"))]
153153
let comment = "";
154154
#[cfg(feature = "alloc")]
155-
let comment = self.comment_str_lossy();
155+
let comment = self.comment.as_str_lossy();
156156

157157
SshFormat::encode(self.algorithm().as_str(), &self.key_data, comment, out)
158158
}
@@ -164,7 +164,7 @@ impl PublicKey {
164164
SshFormat::encode_string(
165165
self.algorithm().as_str(),
166166
&self.key_data,
167-
self.comment_str_lossy(),
167+
self.comment.as_str_lossy(),
168168
)
169169
}
170170

@@ -258,48 +258,11 @@ impl PublicKey {
258258
}
259259

260260
/// Comment on the key (e.g. email address).
261-
///
262-
/// This is a deprecated alias for [`PublicKey::comment_str_lossy`].
263261
#[cfg(feature = "alloc")]
264-
#[deprecated(
265-
since = "0.7.0",
266-
note = "please use `comment_bytes`, `comment_str`, or `comment_str_lossy` instead"
267-
)]
268-
pub fn comment(&self) -> &str {
269-
self.comment_str_lossy()
270-
}
271-
272-
/// Comment on the key (e.g. email address).
273-
///
274-
/// Since comments can contain arbitrary binary data when decoded from a
275-
/// private key, this returns the raw bytes of the comment.
276-
#[cfg(feature = "alloc")]
277-
pub fn comment_bytes(&self) -> &[u8] {
262+
pub fn comment(&self) -> &Comment {
278263
&self.comment
279264
}
280265

281-
/// Comment on the key (e.g. email address).
282-
///
283-
/// This returns a UTF-8 interpretation of the comment when valid.
284-
#[cfg(feature = "alloc")]
285-
pub fn comment_str(&self) -> core::result::Result<&str, str::Utf8Error> {
286-
str::from_utf8(&self.comment)
287-
}
288-
289-
/// Comment on the key (e.g. email address).
290-
///
291-
/// This returns as much data as can be interpreted as valid UTF-8.
292-
#[cfg(feature = "alloc")]
293-
pub fn comment_str_lossy(&self) -> &str {
294-
for i in (1..=self.comment.len()).rev() {
295-
if let Ok(s) = str::from_utf8(&self.comment[..i]) {
296-
return s;
297-
}
298-
}
299-
300-
""
301-
}
302-
303266
/// Public key data.
304267
pub fn key_data(&self) -> &KeyData {
305268
&self.key_data
@@ -314,7 +277,7 @@ impl PublicKey {
314277

315278
/// Set the comment on the key.
316279
#[cfg(feature = "alloc")]
317-
pub fn set_comment(&mut self, comment: impl Into<Vec<u8>>) {
280+
pub fn set_comment(&mut self, comment: impl Into<Comment>) {
318281
self.comment = comment.into();
319282
}
320283

@@ -330,7 +293,7 @@ impl PublicKey {
330293
/// Decode comment (e.g. email address)
331294
#[cfg(feature = "alloc")]
332295
pub(crate) fn decode_comment(&mut self, reader: &mut impl Reader) -> Result<()> {
333-
self.comment = Vec::decode(reader)?;
296+
self.comment = Comment::decode(reader)?;
334297
Ok(())
335298
}
336299
}
@@ -340,7 +303,7 @@ impl From<KeyData> for PublicKey {
340303
PublicKey {
341304
key_data,
342305
#[cfg(feature = "alloc")]
343-
comment: Vec::new(),
306+
comment: Comment::default(),
344307
}
345308
}
346309
}

0 commit comments

Comments
 (0)