Skip to content

Commit 21dacb7

Browse files
authored
ssh-key: extract a Comment type (#360)
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 21dacb7

File tree

8 files changed

+226
-123
lines changed

8 files changed

+226
-123
lines changed

ssh-key/src/comment.rs

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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+
s.to_owned().into()
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<&[u8]> for Comment {
79+
fn from(bytes: &[u8]) -> Comment {
80+
bytes.to_owned().into()
81+
}
82+
}
83+
84+
impl From<Vec<u8>> for Comment {
85+
fn from(vec: Vec<u8>) -> Self {
86+
Self(vec.into_boxed_slice())
87+
}
88+
}
89+
90+
impl From<Comment> for Vec<u8> {
91+
fn from(comment: Comment) -> Vec<u8> {
92+
comment.0.into()
93+
}
94+
}
95+
96+
impl TryFrom<Comment> for String {
97+
type Error = Error;
98+
99+
fn try_from(comment: Comment) -> Result<String, Error> {
100+
comment.as_str().map(ToOwned::to_owned)
101+
}
102+
}
103+
104+
impl fmt::Display for Comment {
105+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106+
f.write_str(self.as_str_lossy())
107+
}
108+
}
109+
110+
impl Comment {
111+
/// Interpret the comment as raw binary data.
112+
pub fn as_bytes(&self) -> &[u8] {
113+
&self.0
114+
}
115+
116+
/// Interpret the comment as a UTF-8 string.
117+
pub fn as_str(&self) -> Result<&str, Error> {
118+
Ok(str::from_utf8(&self.0)?)
119+
}
120+
121+
/// Interpret the comment as a UTF-8 string.
122+
///
123+
/// This is the maximal prefix of the comment which can be interpreted as valid UTF-8.
124+
// TODO(tarcieri): precompute and store the offset which represents this prefix?
125+
#[cfg(feature = "alloc")]
126+
pub fn as_str_lossy(&self) -> &str {
127+
for i in (1..=self.len()).rev() {
128+
if let Ok(s) = str::from_utf8(&self.0[..i]) {
129+
return s;
130+
}
131+
}
132+
133+
""
134+
}
135+
136+
/// Is the comment empty?
137+
pub fn is_empty(&self) -> bool {
138+
self.0.is_empty()
139+
}
140+
141+
/// Get the length of this comment in bytes.
142+
pub fn len(&self) -> usize {
143+
self.0.len()
144+
}
145+
}
146+
147+
#[cfg(test)]
148+
mod tests {
149+
use super::Comment;
150+
151+
#[test]
152+
fn as_str_lossy_ignores_non_utf8_data() {
153+
const EXAMPLE: &[u8] = b"hello world\xc3\x28";
154+
155+
let comment = Comment::from(EXAMPLE);
156+
assert_eq!(comment.as_str_lossy(), "hello world");
157+
}
158+
}

ssh-key/src/lib.rs

+5-2
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

+18-42
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,13 @@ impl PrivateKey {
681646
checkint.encode(writer)?;
682647
checkint.encode(writer)?;
683648
self.key_data.encode(writer)?;
684-
self.comment_bytes().encode(writer)?;
649+
650+
// Serialize comment
651+
#[cfg(not(feature = "alloc"))]
652+
b"".encode(writer)?;
653+
#[cfg(feature = "alloc")]
654+
self.comment().encode(writer)?;
655+
685656
writer.write(&PADDING_BYTES[..padding_len])?;
686657
Ok(())
687658
}
@@ -701,10 +672,15 @@ impl PrivateKey {
701672
// This method is intended for use with unencrypted keys only
702673
debug_assert!(!self.is_encrypted(), "called on encrypted key");
703674

675+
#[cfg(not(feature = "alloc"))]
676+
let comment_len = 0;
677+
#[cfg(feature = "alloc")]
678+
let comment_len = self.comment().encoded_len()?;
679+
704680
[
705681
8, // 2 x uint32 checkints,
706682
self.key_data.encoded_len()?,
707-
self.comment_bytes().encoded_len()?,
683+
comment_len,
708684
]
709685
.checked_sum()
710686
}

0 commit comments

Comments
 (0)