Skip to content

Commit 78acac2

Browse files
committed
ssh-key: Add support for decoding ChaCha20-Poly1305 & 3DES-CBC private keys
1 parent 97b45f7 commit 78acac2

4 files changed

Lines changed: 95 additions & 5 deletions

File tree

ssh-key/src/cipher.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ const AES128_GCM: &str = "[email protected]";
4141
/// AES-256 in Galois/Counter Mode (GCM).
4242
const AES256_GCM: &str = "[email protected]";
4343

44+
/// ChaCha20-Poly1305
45+
const CHACHA20_POLY1305: &str = "[email protected]";
46+
47+
/// Triple-DES in block chaining (CBC) mode
48+
const TDES_CBC: &str = "3des-cbc";
49+
4450
/// Nonces for AEAD modes.
4551
#[cfg(feature = "aes-gcm")]
4652
type AeadNonce = [u8; 12];
@@ -84,6 +90,12 @@ pub enum Cipher {
8490

8591
/// AES-256 in Galois/Counter Mode (GCM).
8692
Aes256Gcm,
93+
94+
/// ChaCha20-Poly1305
95+
ChaCha20Poly1305,
96+
97+
/// TripleDES in block chaining (CBC) mode
98+
TDesCbc,
8799
}
88100

89101
impl Cipher {
@@ -102,6 +114,8 @@ impl Cipher {
102114
AES256_CTR => Ok(Self::Aes256Ctr),
103115
AES128_GCM => Ok(Self::Aes128Gcm),
104116
AES256_GCM => Ok(Self::Aes256Gcm),
117+
CHACHA20_POLY1305 => Ok(Self::ChaCha20Poly1305),
118+
TDES_CBC => Ok(Self::TDesCbc),
105119
_ => Err(Error::AlgorithmUnknown),
106120
}
107121
}
@@ -118,6 +132,8 @@ impl Cipher {
118132
Self::Aes256Ctr => AES256_CTR,
119133
Self::Aes128Gcm => AES128_GCM,
120134
Self::Aes256Gcm => AES256_GCM,
135+
Self::ChaCha20Poly1305 => CHACHA20_POLY1305,
136+
Self::TDesCbc => TDES_CBC,
121137
}
122138
}
123139

@@ -133,13 +149,15 @@ impl Cipher {
133149
Self::Aes256Ctr => Some((32, 16)),
134150
Self::Aes128Gcm => Some((16, 12)),
135151
Self::Aes256Gcm => Some((32, 12)),
152+
Self::ChaCha20Poly1305 => Some((64, 0)),
153+
Self::TDesCbc => Some((24, 8)),
136154
}
137155
}
138156

139157
/// Get the block size for this cipher in bytes.
140158
pub fn block_size(self) -> usize {
141159
match self {
142-
Self::None => 8,
160+
Self::None | Self::ChaCha20Poly1305 | Self::TDesCbc => 8,
143161
Self::Aes128Cbc
144162
| Self::Aes192Cbc
145163
| Self::Aes256Cbc
@@ -163,7 +181,10 @@ impl Cipher {
163181

164182
/// Does this cipher have an authentication tag? (i.e. is it an AEAD mode?)
165183
pub fn has_tag(self) -> bool {
166-
matches!(self, Self::Aes128Gcm | Self::Aes256Gcm)
184+
matches!(
185+
self,
186+
Self::Aes128Gcm | Self::Aes256Gcm | Self::ChaCha20Poly1305
187+
)
167188
}
168189

169190
/// Is this cipher `none`?
@@ -347,10 +368,9 @@ where
347368
#[cfg(feature = "encryption")]
348369
fn ctr_encrypt<C>(key: &[u8], iv: &[u8], buffer: &mut [u8]) -> Result<()>
349370
where
350-
C: StreamCipherCore + KeyIvInit
371+
C: StreamCipherCore + KeyIvInit,
351372
{
352-
let cipher = C::new_from_slices(key, iv)
353-
.map_err(|_| Error::Crypto)?;
373+
let cipher = C::new_from_slices(key, iv).map_err(|_| Error::Crypto)?;
354374

355375
cipher
356376
.try_apply_keystream_partial(buffer.into())

ssh-key/tests/encrypted_private_key.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ const OPENSSH_AES128_GCM_ED25519_EXAMPLE: &str = include_str!("examples/id_ed255
4949
/// Plaintext is `OPENSSH_ED25519_EXAMPLE`.
5050
const OPENSSH_AES256_GCM_ED25519_EXAMPLE: &str = include_str!("examples/id_ed25519.aes256-gcm.enc");
5151

52+
/// ChaCha20-Poly1305 encrypted Ed25519 OpenSSH-formatted private key.
53+
///
54+
/// Plaintext is `OPENSSH_ED25519_EXAMPLE`.
55+
const OPENSSH_CHACHA20_POLY1305_ED25519_EXAMPLE: &str = include_str!("examples/id_ed25519.chacha20-poly1305.enc");
56+
57+
/// TripleDES-CBC encrypted Ed25519 OpenSSH-formatted private key.
58+
///
59+
/// Plaintext is `OPENSSH_ED25519_EXAMPLE`.
60+
const OPENSSH_3DES_CBC_ED25519_EXAMPLE: &str = include_str!("examples/id_ed25519.3des-cbc.enc");
61+
5262
/// Bad password; don't actually use outside tests!
5363
#[cfg(all(feature = "encryption"))]
5464
const PASSWORD: &[u8] = b"hunter42";
@@ -94,6 +104,49 @@ fn decode_openssh_aes256_gcm() {
94104
key.public_key().key_data().ed25519().unwrap().as_ref(),
95105
);
96106
}
107+
108+
#[test]
109+
fn decode_openssh_chacha20_poly1305() {
110+
let key = PrivateKey::from_openssh(OPENSSH_CHACHA20_POLY1305_ED25519_EXAMPLE).unwrap();
111+
assert_eq!(Algorithm::Ed25519, key.algorithm());
112+
assert_eq!(Cipher::ChaCha20Poly1305, key.cipher());
113+
assert_eq!(KdfAlg::Bcrypt, key.kdf().algorithm());
114+
115+
match key.kdf() {
116+
Kdf::Bcrypt { salt, rounds } => {
117+
assert_eq!(salt, &hex!("f651ca3efb15904d05c216a5041ea89a"));
118+
assert_eq!(*rounds, 16);
119+
}
120+
other => panic!("unexpected KDF algorithm: {:?}", other),
121+
}
122+
123+
assert_eq!(
124+
&hex!("b33eaef37ea2df7caa010defdea34e241f65f1b529a4f43ed14327f5c54aab62"),
125+
key.public_key().key_data().ed25519().unwrap().as_ref(),
126+
);
127+
}
128+
129+
#[test]
130+
fn decode_openssh_3des_cbc() {
131+
let key = PrivateKey::from_openssh(OPENSSH_3DES_CBC_ED25519_EXAMPLE).unwrap();
132+
assert_eq!(Algorithm::Ed25519, key.algorithm());
133+
assert_eq!(Cipher::TDesCbc, key.cipher());
134+
assert_eq!(KdfAlg::Bcrypt, key.kdf().algorithm());
135+
136+
match key.kdf() {
137+
Kdf::Bcrypt { salt, rounds } => {
138+
assert_eq!(salt, &hex!("1afcebea3c598c277e7edc2b78db1e94"));
139+
assert_eq!(*rounds, 16);
140+
}
141+
other => panic!("unexpected KDF algorithm: {:?}", other),
142+
}
143+
144+
assert_eq!(
145+
&hex!("b33eaef37ea2df7caa010defdea34e241f65f1b529a4f43ed14327f5c54aab62"),
146+
key.public_key().key_data().ed25519().unwrap().as_ref(),
147+
);
148+
}
149+
97150
#[cfg(all(feature = "encryption"))]
98151
#[test]
99152
fn decrypt_openssh_aes128_ctr() {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
b3BlbnNzaC1rZXktdjEAAAAACDNkZXMtY2JjAAAABmJjcnlwdAAAABgAAAAQGvzr6jxZjC
3+
d+ftwreNselAAAABAAAAABAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCzPq7zfqLffKoBDe/e
4+
o04kH2XxtSmk9D7RQyf1xUqrYgAAAJg/4GdFw1boDlKQK5tYsHQUUIyHIYR91BH6LfVhG1
5+
gj+b6Cj3dm1ESAUdg1qhKac4oZS6AYx6L3m7M4rHXYi2SncRqPoa+DODCxcvUqUNQOrT54
6+
6PjbIx6P7Ewam4PtXENekmx0gOsBshNkQyzP8XA86DtorA+kW0VU1YUTmJrqwAzZrrg2Nf
7+
Rueif44KC5+spnhieuaDf6Lg==
8+
-----END OPENSSH PRIVATE KEY-----
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
b3BlbnNzaC1rZXktdjEAAAAAHWNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc2guY29tAAAABm
3+
JjcnlwdAAAABgAAAAQ9lHKPvsVkE0FwhalBB6omgAAABAAAAABAAAAMwAAAAtzc2gtZWQy
4+
NTUxOQAAACCzPq7zfqLffKoBDe/eo04kH2XxtSmk9D7RQyf1xUqrYgAAAJiRvYDd00XU/W
5+
BkZ93ZW52HNwvM2m3z/MHuqD8q/tk16rKKtBNOc95wo4gyRzkdGYhKnF1RFCJYcdvlw6zo
6+
kctfmmhQ6W54G6u9Eh9bIJtHt3l4FQgzriuIsBTUKZIlvvk6Fo5ItNPHM00r2ehuX81lcZ
7+
QHMaims6Blw8Esl6G3NYCAa2NKyqlmM5LIfkga/Ymydvrbc7EQmN2hbii0c0aMUdYQclyk
8+
F4o=
9+
-----END OPENSSH PRIVATE KEY-----

0 commit comments

Comments
 (0)