Skip to content

Commit

Permalink
ppk v2 format support
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugeny committed Jan 6, 2025
1 parent 602c53c commit 98e181c
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 29 deletions.
2 changes: 1 addition & 1 deletion ssh-key/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ getrandom = ["rand_core/getrandom"]
p256 = ["dep:p256", "ecdsa"]
p384 = ["dep:p384", "ecdsa"]
p521 = ["dep:p521", "ecdsa"]
ppk = ["dep:hex", "alloc", "dep:hmac", "dep:argon2", "encryption"]
ppk = ["dep:hex", "alloc", "cipher/aes-cbc", "dep:hmac", "dep:argon2", "dep:sha1"]
rsa = ["dep:bigint", "dep:rsa", "alloc", "rand_core"]
tdes = ["cipher/tdes", "encryption"]

Expand Down
122 changes: 94 additions & 28 deletions ssh-key/src/ppk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use core::str::FromStr;
use hex::FromHex;
use hmac::digest::KeyInit;
use hmac::{Hmac, Mac};
use sha1::{Digest, Sha1};
use sha2::Sha256;

use crate::private::KeypairData;
Expand All @@ -23,10 +24,15 @@ use subtle::ConstantTimeEq;
#[derive(Debug)]
pub enum Kdf {
Argon2 { kdf: Argon2<'static>, salt: Vec<u8> },
PpkV2,
}

impl Kdf {
pub fn new(algorithm: &str, ppk: &PpkWrapper) -> Result<Self, PpkParseError> {
pub fn new_v2() -> Self {
Self::PpkV2
}

pub fn new_v3(algorithm: &str, ppk: &PpkWrapper) -> Result<Self, PpkParseError> {
let argon_algorithm = match algorithm {
"Argon2i" => Ok(argon2::Algorithm::Argon2i),
"Argon2d" => Ok(argon2::Algorithm::Argon2d),
Expand Down Expand Up @@ -66,6 +72,7 @@ impl Kdf {
pub fn derive(&self, password: &[u8], output: &mut [u8]) -> Result<(), argon2::Error> {
match self {
Kdf::Argon2 { kdf, salt } => kdf.hash_password_into(password, salt, output),
Kdf::PpkV2 => Ok(()),
}
}
}
Expand All @@ -77,27 +84,60 @@ pub enum Cipher {

type Aes256CbcKey = [u8; 32];
type Aes256CbcIv = [u8; 16];
type HmacKey = [u8; 32];
type HmacKey = Vec<u8>;

const PPK_V2_MAC_PREFIX: &str = "putty-private-key-file-mac-key";

impl Cipher {
fn derive_aes_params(
kdf: &Kdf,
password: &str,
) -> Result<(Aes256CbcKey, Aes256CbcIv, HmacKey), Error> {
let mut key_iv_mac = vec![0; 80];
kdf.derive(password.as_bytes(), &mut key_iv_mac)
.map_err(PpkParseError::Argon2)?;
let key = &key_iv_mac[..32];
let iv = &key_iv_mac[32..48];
let mac_key = &key_iv_mac[48..80];
Ok((
#[allow(clippy::unwrap_used)] // const size
key.try_into().unwrap(),
#[allow(clippy::unwrap_used)] // const size
iv.try_into().unwrap(),
#[allow(clippy::unwrap_used)] // const size
mac_key.try_into().unwrap(),
))
match &kdf {
Kdf::Argon2 { .. } => {
let mut key_iv_mac = vec![0; 80];
kdf.derive(password.as_bytes(), &mut key_iv_mac)
.map_err(PpkParseError::Argon2)?;
let key = &key_iv_mac[..32];
let iv = &key_iv_mac[32..48];
let mac_key = &key_iv_mac[48..80];
Ok((
#[allow(clippy::unwrap_used)] // const size
key.try_into().unwrap(),
#[allow(clippy::unwrap_used)] // const size
iv.try_into().unwrap(),
#[allow(clippy::unwrap_used)] // const size
mac_key.try_into().unwrap(),
))
}
Kdf::PpkV2 => {
let mut hashes = {
let mut hash = Sha1::default();
hash.update(&[0, 0, 0, 0]);
hash.update(password.as_bytes());
hash.finalize().to_vec()
};
hashes.extend_from_slice({
let mut hash = Sha1::default();
hash.update(&[0, 0, 0, 1]);
hash.update(password.as_bytes());
hash.finalize().as_slice()
});

let aes_key = hashes[..32].try_into().unwrap();
let iv = [0; 16];

let mac_key = {
let mut hash = Sha1::default();
hash.update(PPK_V2_MAC_PREFIX.as_bytes());
hash.update(password.as_bytes());
hash.finalize()
}
.to_vec();

Ok((aes_key, iv, mac_key))
}
}
}

pub fn derive_mac_key(&self, kdf: &Kdf, password: &str) -> Result<HmacKey, Error> {
Expand Down Expand Up @@ -248,7 +288,7 @@ impl TryFrom<&str> for PpkWrapper {
let version = header_version
.parse()
.map_err(|_| PpkParseError::Header(header.into()))?;
if version != 3 {
if version != 3 && version != 2 {
return Err(PpkParseError::UnsupportedFormatVersion(version));
}

Expand Down Expand Up @@ -311,15 +351,23 @@ impl PpkContainer {
let Some(passphrase) = passphrase else {
return Err(PpkParseError::Encrypted.into());
};
match ppk.values.get(&PpkKey::KeyDerivation).map(String::as_str) {
None => {
return Err(PpkParseError::MissingValue(PpkKey::KeyDerivation).into());
}
Some(kdf) => Some(PpkEncryption {
kdf: Kdf::new(kdf, &ppk)?,
match ppk.version {
2 => Some(PpkEncryption {
kdf: Kdf::new_v2(),
cipher: Cipher::Aes256Cbc,
passphrase,
}),
3 => match ppk.values.get(&PpkKey::KeyDerivation).map(String::as_str) {
None => {
return Err(PpkParseError::MissingValue(PpkKey::KeyDerivation).into());
}
Some(kdf) => Some(PpkEncryption {
kdf: Kdf::new_v3(kdf, &ppk)?,
cipher: Cipher::Aes256Cbc,
passphrase,
}),
},
v => return Err(PpkParseError::UnsupportedFormatVersion(v).into()),
}
}
Some(v) => return Err(PpkParseError::UnsupportedEncryption(v.into()).into()),
Expand Down Expand Up @@ -360,18 +408,36 @@ impl PpkContainer {
};

let hmac_key = match &encryption {
None => HmacKey::default(),
None => match ppk.version {
2 => {
let mut hash = Sha1::new();
hash.update(PPK_V2_MAC_PREFIX.as_bytes());
hash.finalize().to_vec()
}
3 => HmacKey::default(),
_ => unreachable!(),
},
Some(enc) => enc.cipher.derive_mac_key(&enc.kdf, &enc.passphrase)?,
};

let expected_mac = {
#[allow(clippy::unwrap_used)] // const key length
let mut hmac = Hmac::<Sha256>::new_from_slice(&hmac_key).unwrap();
hmac.update(&mac_buffer);
hmac.finalize()
match ppk.version {
3 => {
let mut hmac = Hmac::<Sha256>::new_from_slice(&hmac_key).unwrap();
hmac.update(&mac_buffer);
hmac.finalize().into_bytes().to_vec()
}
2 => {
let mut hmac = Hmac::<Sha1>::new_from_slice(&hmac_key).unwrap();
hmac.update(&mac_buffer);
hmac.finalize().into_bytes().to_vec()
}
_ => unreachable!(),
}
};

if expected_mac.into_bytes().ct_ne(&mac).into() {
if expected_mac.ct_ne(&mac).into() {
return Err(Error::Ppk(PpkParseError::IncorrectMac));
}

Expand Down
36 changes: 36 additions & 0 deletions ssh-key/tests/examples/id_rsa_3072.ppk2
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
PuTTY-User-Key-File-2: ssh-rsa
Encryption: none
Comment: [email protected]
Public-Lines: 9
AAAAB3NzaC1yc2EAAAADAQABAAABgQCmjkeMm8k3JkNrf16eb5pG4bc77B6Mt3VN
4saltsRV8vASpyWa/PlBgdaeldOaNJ5NK0gqU3KyiUNzHbdcc8572e7IUBDJS/rl
aWARiSL4aos2VbNX0k56Z5zYp9m/bq5m9/mlb+PQkNBjIhimgpYNiq2TwBiYeA6t
Lb79cPtHA0cX5BLk/a5oUpLsiR4kI/f+Q98vVDKasKXXVh5YLkLobrruDB6er2A9
fOcIUF0O4JCRLh/Dc161gE3fQrYTMQenbppZzfxrZfQ8YwLPvKjnqm+XRX+pbTta
Juj0EgTSzUK+EZxoSw8CNwiZpxrjwecTMVQ8w/srQmh4ABGuTqk0wP8HcI7hg+fp
Bv7kiejh5X/Oehxt+Puu85u9GVXb1a0av/vhJvUCBcuISvCA/z1wVJ0xdLhb1/Zi
TDdTzyNbZQ0OQijzK+e1SlkNhp+3eGVZu3pNZvnTppwIXv3wg6kV1HodkWGgh1ay
Y7Buc52Z8okDYqvJat5CzOj5OaQNr/k=
Private-Lines: 21
AAABgGtjCxDGlQrA2fFicxA2JsOS3sB88gmKc9Ce6bOIzrgX5eAw8tcmSlOJMmaX
dZJUYMiiomnf2fDw/ZMoUsQCStyh3Ao9TUVsfr0RnwZPZEPE9jM3OGXkTAMx8Pfj
6Uo7Q6lSMx0OslUUObfhEQGy6qqagmXkEjekGNphx2XDRdA4dcsam3AXfC75Jo/p
rIxiwI+pFSp/4AzK3nKjrPbwBOW2F0JKgCeSLbwXXyKGJinkcnGYypQLO8JMkmjj
q19eWWW4OH4UcGebPqaAll+BWTyxQTENTEFWniWzdqLcTtkvkUm3XpcOgiRzCUbM
IPNR+BFbG7/Ls49r0GxiBHK3bWQdNYAq3vFSIKubKlfjWRj+J+E4EZzKVqmMzzwP
xoOhnychqHZuzdnFdndmJlbz0+BTJfP7NzJmI9u+xjs9mEgwst0nvrtr0u1TRd//
GN8YBq3rztqYRYBJaJMGgaw+UjE4xSFssTWZfj4UOngWrMPYdB6s7H4V9T2g8IEG
kXCNnQAAAMEA0R564khkDTsgKTaRiGVEzf4HeamqtWyPlia/HmZIv9mIvbCsfRGn
PjQFYzbUrTkA/3GE7kBLhLrrEaKjAvmC2U7vt1cDDsbXfZEV6u+Aq1dJoPW1kLKZ
/96U+ZMN7bqyrzMwlbCKUEubMPERLc5R837QDQQzQ9Qg0uL7iL1/iBt8iZDki5P9
HShPzIwcB/vvwE0CklsvFZqan1Zwc+HJT9xuRy9IljvhbFxUU4Vq0r95FuQsNuda
UBiRDY2tA41zAAAAwQDL5Q5+zfXiyG52ypS+iwwFsJBB0rzd7rRnLnEg6syDgOXW
t3yFWDxQj47o1VfKvLbfroxyOF8PaTRevBWl3+yUnAdw0C15Rd01klYtpziGYuBT
xUVNJpDeKmPMVV4aAQ4toK4wfRwR+FKpx1aOAvk9SbKo+Se3mUOykgytMhqiCEEJ
0TbQhcHQXDn0w2z4n9w8ZqdV5j9EbhYwKxNZlADwqDMhoua5FT3wLwPeMY6gkDko
KFPyAR4JBdEVdmfK8eMAAADAVEBapmOunggANacQAvTDUdfQAsNSAHJebcD/bZAa
MEsQOi6gFlB5ltMZNYtb6k/rQJj1MFKPErmMUMfd/IX8Svkle6+apyNc30Z3NJt3
5SpApeL0QSLRjOQJQZFOmRacSLcIiY0phpZWYHt+LrY1QeC71Wjk93S+wxN9AqWR
yMd7LhiN1vcu71z/GSfN5XOkyg1DwrbGqVchRFEi4c9qpfBbZcuchhJPn3n6KfBe
PwbzuD7cqZQfVxZQ4PtGiq5M
Private-MAC: c74d3a1f3aa0e626832ef79c401fb93831a7c7f5
36 changes: 36 additions & 0 deletions ssh-key/tests/examples/id_rsa_3072_enc.ppk2
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
PuTTY-User-Key-File-2: ssh-rsa
Encryption: aes256-cbc
Comment: [email protected]
Public-Lines: 9
AAAAB3NzaC1yc2EAAAADAQABAAABgQCmjkeMm8k3JkNrf16eb5pG4bc77B6Mt3VN
4saltsRV8vASpyWa/PlBgdaeldOaNJ5NK0gqU3KyiUNzHbdcc8572e7IUBDJS/rl
aWARiSL4aos2VbNX0k56Z5zYp9m/bq5m9/mlb+PQkNBjIhimgpYNiq2TwBiYeA6t
Lb79cPtHA0cX5BLk/a5oUpLsiR4kI/f+Q98vVDKasKXXVh5YLkLobrruDB6er2A9
fOcIUF0O4JCRLh/Dc161gE3fQrYTMQenbppZzfxrZfQ8YwLPvKjnqm+XRX+pbTta
Juj0EgTSzUK+EZxoSw8CNwiZpxrjwecTMVQ8w/srQmh4ABGuTqk0wP8HcI7hg+fp
Bv7kiejh5X/Oehxt+Puu85u9GVXb1a0av/vhJvUCBcuISvCA/z1wVJ0xdLhb1/Zi
TDdTzyNbZQ0OQijzK+e1SlkNhp+3eGVZu3pNZvnTppwIXv3wg6kV1HodkWGgh1ay
Y7Buc52Z8okDYqvJat5CzOj5OaQNr/k=
Private-Lines: 21
bRFyGRUcw1sl1Yip86E4zjIBt6Z0wLysSneBbUwqzzL7W+S2CNf/jljoVcWEGKnq
4lFz1HuCwZI7YXzm+RzLIukL+pL5oK3UC47zVbtuvVtI+Wn4x4t8BiB3KJ5GP5/o
VDLiph9qs+8Fi5a29hdK4vySwAUyD91rNmYON/bjNeVK8U0CGMnsyoSi+/e4fGJ/
F5Roly2r/7SxxSQw+sqjnWAPX0Wqk11AsaU72shabZwJkv7Ro9D4a7alh52NMGzd
Cw7tyXU/z701cOyfjQSjLKmDkrHJqt4atIM4THMr+JJz4PVC/WQYnifeIxFGSWiu
SKXjCqnXjpeoDkqqee5gsmNrDK7Km0bzGGS7akvOQB8j8Rl5QL7Yu4ab1cdfciuI
3CUlF94SMwoR2bSxOkq12YnNTln4gnTIoGnpBfLstTa2OJFlzzU8OCaQOstrBCOy
xwS49/f5wl3lZIkaSGkahMAAas3egLk/NohPgFYqdrjqssUAChzZ8JgVVVhG2tbN
dhQgJqjCVm1YzIh27v+sNUviQhKGJ4jWuwT1bwdmFn1uW2mLgZuast9xcTiFXlNR
0ir5rAblRQPuRxjkpVrrsnYs6pHY/U6Q/5sS9zLz/ku5Z1yBrPX10I4d/t0QOfu1
KphWOTuakUYWaEBhQcrOrISLH6MsAC/Tc8fHqATYOKaCQO/TE53euh8GikP2B5FJ
o2tQFteRtixgCN/fO+xwxyC0PdIteQgCcRNtiYLCOcoZvlsOGtEGNkgUjlOSRZ58
9NB4OtQlBzUxRs7FvCmSUFwSO/azmJGdNIxEzTXinWnQ5qr8Y5cMU6wB+QUZZZxV
kjM/kHfP9eG1hapkIZaV18ql0Bu6nOoaP5oFTGPurMnkneoytL30qU6zr6hBCQs4
SrXIo+WwraI8EJ1Rp6Fv2JNS+U50YGKTz3m93Eytus8KKsYQN1dJ9o8jPacXpJQE
BJTTeE38wn/+0SWuPeRBf+L7MYE00fKDQ89xHjFWAVoM1aty55nvfs2zVtDvsWen
SfJi1/3fSgbY07MY75nFGIT5dIj+EmFXCAPnMgTKUNvVK7hrcj0uBXDRdkKWpI2D
34XsD8oIc+QoGeG28cywfbJUveCnnehUCTrCJ0acIeh89v1jHTHMV8BF8WdBO5+i
UTkdNZsrWCqn76e1p9AOLRSwJMNgJIq71Ahdw2ynxb6NTxW85b3tMVMcT1G1X6Xv
+oAKPs9ru0hwZCeFJ7oUiuzOgpnKN2veKvcVk2x6KPojwpuBn1PCoqSgccNDE0RV
Ga59u+c7C0TK6WTfehxm12VnNf0PWObsZ0gA2ukwEpY=
Private-MAC: 967a19c88c37946ddb771c48ab5ad0e82159b47f
22 changes: 22 additions & 0 deletions ssh-key/tests/private_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ const PPK_RSA_3072_EXAMPLE: &str = include_str!("examples/id_rsa_3072.ppk");
#[cfg(all(feature = "ppk", feature = "rsa", feature = "encryption"))]
const PPK_RSA_3072_EXAMPLE_ENCRYPTED: &str = include_str!("examples/id_rsa_3072_enc.ppk");

/// Same key, converted by puttygen
#[cfg(all(feature = "ppk", feature = "rsa"))]
const PPK_V2_RSA_3072_EXAMPLE: &str = include_str!("examples/id_rsa_3072.ppk2");

/// Same key, converted and encrypted by puttygen
#[cfg(all(feature = "ppk", feature = "rsa", feature = "encryption"))]
const PPK_V2_RSA_3072_EXAMPLE_ENCRYPTED: &str = include_str!("examples/id_rsa_3072_enc.ppk2");

/// RSA (4096-bit) OpenSSH-formatted public key
#[cfg(feature = "alloc")]
const OPENSSH_RSA_4096_EXAMPLE: &str = include_str!("examples/id_rsa_4096");
Expand Down Expand Up @@ -361,6 +369,20 @@ fn decode_rsa_3072_ppk_encrypted() {
);
}

#[test]
#[cfg(all(feature = "rsa", feature = "ppk"))]
fn decode_rsa_3072_ppk_v2() {
validate_rsa_3072(PrivateKey::from_ppk(PPK_V2_RSA_3072_EXAMPLE, None).unwrap());
}

#[test]
#[cfg(all(feature = "rsa", feature = "ppk"))]
fn decode_rsa_3072_ppk_v2_encrypted() {
validate_rsa_3072(
PrivateKey::from_ppk(PPK_V2_RSA_3072_EXAMPLE_ENCRYPTED, Some("123".into())).unwrap(),
);
}

#[cfg(feature = "rsa")]
fn validate_rsa_3072(key: PrivateKey) {
assert_eq!(Algorithm::Rsa { hash: None }, key.algorithm());
Expand Down

0 comments on commit 98e181c

Please sign in to comment.