From f288675cdc621e234d2dc561eac53f81e45296e9 Mon Sep 17 00:00:00 2001 From: Mitchell Grenier Date: Mon, 9 Aug 2021 09:08:01 -0700 Subject: [PATCH] Add encrypted private key support --- .github/workflows/rust.yml | 4 +- Cargo.lock | 109 +++++++++++++++++++-- Cargo.toml | 19 +++- examples/ssh-pkey-info.rs | 35 +++++++ src/error.rs | 7 +- src/ssh/privkey.rs | 117 ++++++++++++++++------ tests/privkey_encrypted.rs | 194 +++++++++++++++++++++++++++++++++++++ 7 files changed, 443 insertions(+), 42 deletions(-) create mode 100644 examples/ssh-pkey-info.rs create mode 100644 tests/privkey_encrypted.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ad7efa8..b2002e6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,7 +18,7 @@ jobs: - name: Build run: cargo build --features="yubikey" --examples --verbose - name: Run tests - run: cargo test --verbose + run: cargo test --features="encrypted-keys" --verbose ubuntu-build-test-no-yubikey: runs-on: ubuntu-latest @@ -28,4 +28,4 @@ jobs: - name: Build run: cargo build --examples --verbose - name: Run tests - run: cargo test --verbose + run: cargo test --features="encrypted-keys" --verbose diff --git a/Cargo.lock b/Cargo.lock index 553a84e..b889b78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,19 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aes" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495ee669413bfbe9e8cace80f4d3d78e6d8c8d99579f97fb93bde351b185f2d4" +dependencies = [ + "cfg-if 1.0.0", + "cipher 0.3.0", + "cpufeatures", + "ctr 0.7.0", + "opaque-debug", +] + [[package]] name = "aho-corasick" version = "0.7.15" @@ -46,6 +59,19 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bcrypt-pbkdf" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12621b8e87feb183a6e5dbb315e49026b2229c4398797ee0ae2d1bc00aef41b9" +dependencies = [ + "blowfish", + "crypto-mac 0.11.1", + "pbkdf2 0.8.0", + "sha2", + "zeroize", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -72,6 +98,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3ff3fc1de48c1ac2e3341c4df38b0d1bfb8fdf04632a187c8b75aaa319a7ab" +dependencies = [ + "byteorder", + "cipher 0.3.0", + "opaque-debug", +] + [[package]] name = "bstr" version = "0.2.15" @@ -145,6 +182,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "clap" version = "2.33.3" @@ -206,6 +252,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f21b581d2f0cb891554812435667bb9610d74feb1a4c6415bf09c28ff0381d" +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + [[package]] name = "cpuid-bool" version = "0.1.2" @@ -304,6 +359,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "csv" version = "1.1.5" @@ -326,6 +391,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctr" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481" +dependencies = [ + "cipher 0.3.0", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher 0.3.0", +] + [[package]] name = "data-encoding" version = "2.3.1" @@ -365,7 +448,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b24e7c748888aa2fa8bce21d8c64a52efc810663285315ac7476f7197a982fae" dependencies = [ "byteorder", - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -503,7 +586,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ - "crypto-mac", + "crypto-mac 0.10.0", "digest", ] @@ -580,9 +663,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.82" +version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" [[package]] name = "libm" @@ -766,7 +849,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3b8c0d71734018084da0c0354193a5edfb81b20d2d57a92c5b154aefc554a4a" dependencies = [ "base64", - "crypto-mac", + "crypto-mac 0.10.0", "hmac", "rand", "rand_core", @@ -774,6 +857,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "pbkdf2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +dependencies = [ + "crypto-mac 0.11.1", +] + [[package]] name = "pcsc" version = "2.4.0" @@ -1198,11 +1290,14 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "sshcerts" -version = "0.5.0" +version = "0.6.0" dependencies = [ + "aes", "base64", + "bcrypt-pbkdf", "clap 3.0.0-beta.2", "criterion", + "ctr 0.8.0", "env_logger", "hex", "lexical-core", @@ -1555,7 +1650,7 @@ dependencies = [ "num-traits", "p256", "p384", - "pbkdf2", + "pbkdf2 0.6.0", "pcsc", "rsa", "secrecy", diff --git a/Cargo.toml b/Cargo.toml index 914cfb6..9c66a1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sshcerts" -version = "0.5.0" +version = "0.6.0" authors = ["Mitchell Grenier "] edition = "2018" license-file = "LICENSE" @@ -13,10 +13,11 @@ categories = ["authentication"] [features] default = ["rsa-signing"] -all = ["yubikey", "rsa-signing"] +all = ["encrypted-keys", "rsa-signing", "yubikey"] yubikey = ["yubikey-piv", "log"] rsa-signing = ["simple_asn1", "num-bigint"] +encrypted-keys = ["aes", "bcrypt-pbkdf", "ctr"] [dependencies] base64 = "0.13" @@ -31,6 +32,11 @@ log = {version = "0.4", optional = true} yubikey-piv = {version = "0.1.0", features = ["untested"], optional = true} lexical-core = {version = ">0.7.4", optional = true} +# Dependencies for encrypted keys +aes = {version = "0.7", features = ["ctr"], optional = true} +bcrypt-pbkdf = {version = "0.6", optional = true} +ctr = {version = "0.8", optional = true} + [dev-dependencies] env_logger = "0.8.2" hex = "0.4.2" @@ -49,3 +55,12 @@ required-features = ["yubikey"] [[example]] name = "yk-provision" required-features = ["yubikey"] + +[[example]] +name = "ssh-pkey-info" +required-features = ["encrypted-keys"] + +[[test]] +name = "privkey-encrypted" +path = "tests/privkey_encrypted.rs" +required-features = ["encrypted-keys"] \ No newline at end of file diff --git a/examples/ssh-pkey-info.rs b/examples/ssh-pkey-info.rs new file mode 100644 index 0000000..a9122f3 --- /dev/null +++ b/examples/ssh-pkey-info.rs @@ -0,0 +1,35 @@ +use std::env; + +use sshcerts::ssh::PrivateKey; + +fn help() { + println!("An SSH Private Key reader based on the sshcerts library"); + println!("Usage: ssh-pkey-info "); +} + +fn main() -> Result<(), String> { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + help(); + return Ok(()); + } + + let path = &args[1]; + + let passphrase = if args.len() == 3 { + Some(args[2].clone()) + } else { + None + }; + + match PrivateKey::from_path_with_passphrase(path, passphrase) { + Ok(c) => { + println!("{:#}", c); + Ok(()) + }, + Err(e) => { + Err(format!("{}: Private key at {} not valid", e, &args[1])) + } + } +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 9b67c27..a9f6008 100644 --- a/src/error.rs +++ b/src/error.rs @@ -23,7 +23,9 @@ pub enum Error { CertificateInvalidSignature, /// A cryptographic operation failed. SigningError, - /// An encrypted private key was supplied and is not supported + /// An encrypted private key was provided with no decryption key + EncryptedPrivateKey, + /// An encrypted private key was supplied but the encryption method is not supported EncryptedPrivateKeyNotSupported, /// The key type is unknown UnknownKeyType(String), @@ -47,7 +49,8 @@ impl fmt::Display for Error { Error::KeyTypeMismatch => write!(f, "Key type mismatch"), Error::CertificateInvalidSignature => write!(f, "Certificate is improperly signed"), Error::SigningError => write!(f, "Could not sign data"), - Error::EncryptedPrivateKeyNotSupported => write!(f, "Encrypted private keys are not supported"), + Error::EncryptedPrivateKey => write!(f, "Encountered encrypted private key with no decryption key"), + Error::EncryptedPrivateKeyNotSupported => write!(f, "This method of private key encryption is not supported or sshcerts was not compiled with encrypted private key support"), Error::UnknownKeyType(ref v) => write!(f, "Unknown key type {}", v), Error::UnknownCurve(ref v) => write!(f, "Unknown curve {}", v), #[cfg(feature = "yubikey")] diff --git a/src/ssh/privkey.rs b/src/ssh/privkey.rs index 002ffe1..c5b7de8 100644 --- a/src/ssh/privkey.rs +++ b/src/ssh/privkey.rs @@ -1,15 +1,34 @@ -use super::keytype::{Curve, KeyType, KeyTypeKind}; +use std::convert::TryInto; +use std::fmt; +use std::fs::File; +use std::io::Read; +use std::path::Path; + use crate::{error::Error, Result}; +use super::{ + EcdsaPublicKey, + Ed25519PublicKey, + PublicKey, + PublicKeyKind, + RsaPublicKey, + keytype::{Curve, KeyType, KeyTypeKind}, + reader::Reader, +}; + #[cfg(feature = "rsa-signing")] use num_bigint::{BigInt, BigUint, Sign}; -use super::{PublicKey, PublicKeyKind, EcdsaPublicKey, Ed25519PublicKey, RsaPublicKey}; -use super::reader::Reader; + #[cfg(feature = "rsa-signing")] use simple_asn1::{ASN1Block, ASN1Class, ToASN1}; -use std::fs::File; -use std::io::Read; -use std::path::Path; +#[cfg(feature = "encrypted-keys")] +use aes::{ + Aes256Ctr, + cipher::{NewCipher, generic_array::GenericArray, StreamCipher}, +}; + +#[cfg(feature = "encrypted-keys")] +use bcrypt_pbkdf::bcrypt_pbkdf; /// RSA private key. @@ -216,16 +235,21 @@ fn read_private_key(reader: &mut Reader<'_>) -> Result { } impl PrivateKey { - /// Reads an OpenSSH private key from a given path. - pub fn from_path>(path: P) -> Result { + /// Reads an OpenSSH private key from a given path and passphrase + pub fn from_path_with_passphrase>(path: P, passphrase: Option) -> Result { let mut contents = String::new(); File::open(path)?.read_to_string(&mut contents)?; - PrivateKey::from_string(&contents) + PrivateKey::from_string_with_passphrase(&contents, passphrase) } - /// Reads an OpenSSH private key from a given string. - pub fn from_string(contents: &str) -> Result { + /// Reads an OpenSSH private key from a given path. + pub fn from_path>(path: P) -> Result { + PrivateKey::from_path_with_passphrase(path, None) + } + + /// Reads an OpenSSH private key from a given string and passphrase + pub fn from_string_with_passphrase(contents: &str, passphrase: Option) -> Result { let mut iter = contents.lines(); let header = iter.next().unwrap_or(""); if header != "-----BEGIN OPENSSH PRIVATE KEY-----" { @@ -248,19 +272,24 @@ impl PrivateKey { let decoded = base64::decode(encoded_key)?; let mut reader = Reader::new(&decoded); // Construct a new `PrivateKey` - let k = PrivateKey::from_reader(&mut reader)?; + let k = PrivateKey::from_reader(&mut reader, passphrase)?; Ok(k) } - /// Create a private key from just the private portion of a key file + /// Reads an OpenSSH private key from a given string. + pub fn from_string(contents: &str) -> Result { + PrivateKey::from_string_with_passphrase(contents, None) + } + + /// Create a private key from just the decrypted private bytes pub fn from_bytes>(buffer: &T) -> Result { let mut reader = Reader::new(buffer); read_private_key(&mut reader) } /// This function is used for extracting a private key from an existing reader. - pub(crate) fn from_reader(reader: &mut Reader<'_>) -> Result { + pub(crate) fn from_reader(reader: &mut Reader<'_>, passphrase: Option) -> Result { let preamble = reader.read_cstring()?; if preamble != "openssh-key-v1" { @@ -270,12 +299,9 @@ impl PrivateKey { // These values are for encrypted keys which are not supported let cipher_name = reader.read_string()?; let kdf = reader.read_string()?; - if cipher_name != "none" || kdf != "none" { - return Err(Error::EncryptedPrivateKeyNotSupported); - } - // This appears to be en empty value - reader.read_string()?; + #[allow(unused_variables)] + let encryption_data = reader.read_bytes()?; // This seems to be hardcoded into the standard let number_of_keys = reader.read_u32()?; @@ -288,21 +314,49 @@ impl PrivateKey { .read_bytes() .and_then(|v| PublicKey::from_bytes(&v))?; - // This contains the length of the rest of the bytes in the key - // We could use this to do a read bytes into a new reader but I don't - // think there is an advantage to that right now (other than verifying) - // that this value is correct. - let _remaining_length = reader.read_u32()?; + let remaining_length = match reader.read_u32()?.try_into() { + Ok(rl) => rl, + Err(_) => return Err(Error::InvalidFormat), + }; + + #[allow(unused_mut)] + let mut remaining_bytes = reader.read_raw_bytes(remaining_length)?; + + match (cipher_name.as_str(), kdf.as_str(), passphrase) { + ("none", "none", _) => (), + #[cfg(feature = "encrypted-keys")] + ("aes256-ctr", "bcrypt", Some(passphrase)) => { + let mut enc_reader = Reader::new(&encryption_data); + let salt = enc_reader.read_bytes()?; + let rounds = enc_reader.read_u32()?; + let mut output = [0; 48]; + if let Err(_) = bcrypt_pbkdf(passphrase.as_str(), &salt, rounds, &mut output) { + return Err(Error::InvalidFormat); + } + + let mut cipher = Aes256Ctr::new( + &GenericArray::from_slice(&output[..32]), + &GenericArray::from_slice(&output[32..]), + ); - // These four bytes are repeated and I'm not sure what they do - let c1 = reader.read_u32()?; - let c2 = reader.read_u32()?; + match cipher.try_apply_keystream(&mut remaining_bytes) { + Ok(_) => (), + Err(_) => return Err(Error::InvalidFormat), + } + }, + ("aes256-ctr", "bcrypt", None) => return Err(Error::EncryptedPrivateKey), + _ => return Err(Error::EncryptedPrivateKeyNotSupported), + }; - if c1 != c2 { + let mut reader = Reader::new(&remaining_bytes); + + // These four bytes are repeated and are used to checks that a key has + // been decrypted successfully + if reader.read_u32()? != reader.read_u32()? { return Err(Error::InvalidFormat); } - let private_key = read_private_key(reader)?; + let private_key = read_private_key(&mut reader)?; if private_key.pubkey != pubkey { return Err(Error::InvalidFormat); @@ -312,3 +366,8 @@ impl PrivateKey { } } +impl fmt::Display for PrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", &self.pubkey.fingerprint(), self.comment.as_ref().unwrap_or(&String::new())) + } +} diff --git a/tests/privkey_encrypted.rs b/tests/privkey_encrypted.rs new file mode 100644 index 0000000..bf3d2f1 --- /dev/null +++ b/tests/privkey_encrypted.rs @@ -0,0 +1,194 @@ +use sshcerts::ssh::{PrivateKey, PrivateKeyKind}; + + +#[test] +fn parse_encrypted_ed25519_private_key() { + let privkey = r#"-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAMSH2ak6 ++qM0Od6QYgqk3EAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIJLaNw1wt2GAGxhZ +b4TTQ3m5bWeghg0hVbUBie2IDxb1AAAAoJgXZeSQFgSB0JzfMPBB9l1roV4nZnAVG0aUC4 +oVhmOX/jGK2MRLusepo1tF98kou01dbVTKiZYdxrCffJDYj2H2LrtWqR2sf19mhUY0OrW8 +0inHLPw5CRRPCJuZ8fdmsbtawWlajCmJykrtCLAhiUx4dJ2gYLyaSIFbFhg0B9XhuLHQ09 +gj+HqUxSiAOuRA5cDU+SykIfb7TLvteZOpl2I= +-----END OPENSSH PRIVATE KEY-----"#; + + let privkey = PrivateKey::from_string_with_passphrase(privkey, Some(format!("test"))); + match &privkey { + Ok(_) => (), + Err(e) => println!("{}", e), + }; + assert!(privkey.is_ok()); + let privkey = privkey.unwrap(); + assert_eq!(privkey.pubkey.fingerprint().hash, "bTkq+BEqfkYOgyPk2ziLwtkxDFcj531SfwEpl3IyutU"); + + let key = match privkey.kind { + PrivateKeyKind::Ed25519(key) => key, + _ => panic!("Wrong key type detected"), + }; + assert_eq!( + key.key, + hex::decode("697DEA3B53C8612F87DC06E92A466366866458403D9695040AD341D05D7430E692DA370D70B761801B18596F84D34379B96D67A0860D2155B50189ED880F16F5").unwrap(), + ) +} + +#[test] +fn parse_encrypted_ed25519_private_key_32_rounds() { + let privkey = r#"-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABArNg8tIr +sFX6oNT1jNqVoIAAAAIAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIEofsz/Ssr2U3p81 +fVIYsF8uRX1qKxk5olZhhtWEcK+lAAAAoCJJBzCtLwWY0cx7G1zYbvbjUP4/lIryQEufgZ +DrhYqWabR+nO8Os2U9EumbuqVM81Rrxcc1Qc9k/IDUelhGDubO7kRFDzn3BAirKl2sTADx +JcR8y21R5hqNGhTlx0F8kqAGg2nW3PmsiGCwKl7Iz7IMf4iaUuufHG2RtTaFpN0n9gxbpQ +6xceKSL0Ba+hjMl54kebsfZJfwgdh6fZ8leec= +-----END OPENSSH PRIVATE KEY-----"#; + + let privkey = PrivateKey::from_string_with_passphrase(privkey, Some(format!("test"))); + match &privkey { + Ok(_) => (), + Err(e) => println!("{}", e), + }; + assert!(privkey.is_ok()); + let privkey = privkey.unwrap(); + assert_eq!(privkey.pubkey.fingerprint().hash, "nTCuabo74eqb8zYiqsg8x7sfgzsu1Egv0XGzaiip9XU"); + + let key = match privkey.kind { + PrivateKeyKind::Ed25519(key) => key, + _ => panic!("Wrong key type detected"), + }; + assert_eq!( + key.key, + hex::decode("C9753EE1FE006557E290E5EBBC67D75E9DDFC9F647291E3E89B23A83507CF5774A1FB33FD2B2BD94DE9F357D5218B05F2E457D6A2B1939A2566186D58470AFA5").unwrap(), + ) +} + +#[test] +fn parse_encrypted_ec256_private_key() { + let privkey = r#"-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAgyk4Wlj +Nok6umgT5cd/0lAAAAEAAAAAEAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlz +dHAyNTYAAABBBPsoWTQSrnuRXj3a7232/SuLOIOva1RHTtmVcRpt3+ktr20hXisf4RG9LB +TZkZ9q0JjvJybRpXo4mT8pVS0jQIIAAACwYyMrnR4LZovfZSMEcdxdPgwM/sioaaLyI7eb +Kh4cPIS2TRZ6a8SlF/+ugnz/5kvQyazJhp07fMqusB/v+7x/jmcNs7z1aq6rh39sirf7ll +kA+bCsY/r/A5G8bcYiIUbRpFIY+JXJkvv1aXIsRS+K5OXKb9aySrBddTY3Uddp9WkfG72W +Gd5VYX4HxsQZWQixs9DSZyCexueq7Fw+57AW9z1XFySUhHdiRlgeXSxsF8Q= +-----END OPENSSH PRIVATE KEY-----"#; + + let privkey = PrivateKey::from_string_with_passphrase(privkey, Some(format!("test"))); + match &privkey { + Ok(_) => (), + Err(e) => println!("{}", e), + }; + assert!(privkey.is_ok()); + let privkey = privkey.unwrap(); + assert_eq!(privkey.pubkey.fingerprint().hash, "aeK6cuLIzfIddiLtlP+kaZqA5lo4ExdXM8ksWeJPPp8"); + + match privkey.kind { + PrivateKeyKind::Ecdsa(key) => key, + _ => panic!("Wrong key type detected"), + }; +} + +#[test] +fn parse_encrypted_ec384_private_key() { + let privkey = r#"-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDx+U4gFb +XXB+awGkd75t8qAAAAEAAAAAEAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlz +dHAzODQAAABhBFlMgDDsgzBqyzJoiM0nS0SZ2oxXBGkYcyaZ7Y3m5CO4BsUIWcHIEJ3A8D +pftOVAmlRm2El4szGmwJrfgBLVF0JfqjQdTrQBG4ARRAWZCbSF7E0VWLYZOHKZZqAlrxKV +eAAAAOCiWpA95P/h4YkK1Qm7C04rlstJFrjTqiVe0vg1XZ/j+4oFfKb5lZA8MbKv6QTP2Q +fD4nMprI5/QXpi0jI/Po0FUJecY+xNTLzTohhwaLkg7aztAQAsChsW8txfetTkFsqz0RxI +VPIGbXHVwMdRPbheRu9AOOotSJldENE5jpdQ9PuBj0IIYw/Q3ZVlT+fePQqxsJsfk820X1 +qE7mF1LNXlV52YOTwFvwfEwiUYyDnhVvwiR06q0ojtlz/K/9t7W+yXNWK55LduI7Q7WaDn +N9Rg7yGs2leAkts+8G8w+tgJJQ== +-----END OPENSSH PRIVATE KEY-----"#; + + let privkey = PrivateKey::from_string_with_passphrase(privkey, Some(format!("test"))); + match &privkey { + Ok(_) => (), + Err(e) => println!("{}", e), + }; + assert!(privkey.is_ok()); + let privkey = privkey.unwrap(); + assert_eq!(privkey.pubkey.fingerprint().hash, "SvNs3N/ZVtfktcRjlcgpvOs4qFnQTIVGTt2L2S2nVI8"); + + match privkey.kind { + PrivateKeyKind::Ecdsa(key) => key, + _ => panic!("Wrong key type detected"), + }; +} + +#[test] +fn parse_encrypted_rsa3072_private_key() { + let privkey = r#"-----BEGIN OPENSSH PRIVATE KEY----- +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 +cRts9Ge+2sCv9AYgd/SipxopCqw= +-----END OPENSSH PRIVATE KEY-----"#; + + let privkey = PrivateKey::from_string_with_passphrase(privkey, Some(format!("test"))); + match &privkey { + Ok(_) => (), + Err(e) => println!("{}", e), + }; + assert!(privkey.is_ok()); + let privkey = privkey.unwrap(); + assert_eq!(privkey.pubkey.fingerprint().hash, "+fZGegm7Lmc5SJJQRXZjvWhT25Ybqb8H4Vvq91Z1JEY"); + + match privkey.kind { + PrivateKeyKind::Rsa(key) => key, + _ => panic!("Wrong key type detected"), + }; +} + +#[test] +fn parse_encrypted_ed25519_private_key_bad_passphrase() { + let privkey = r#"-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAMSH2ak6 ++qM0Od6QYgqk3EAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIJLaNw1wt2GAGxhZ +b4TTQ3m5bWeghg0hVbUBie2IDxb1AAAAoJgXZeSQFgSB0JzfMPBB9l1roV4nZnAVG0aUC4 +oVhmOX/jGK2MRLusepo1tF98kou01dbVTKiZYdxrCffJDYj2H2LrtWqR2sf19mhUY0OrW8 +0inHLPw5CRRPCJuZ8fdmsbtawWlajCmJykrtCLAhiUx4dJ2gYLyaSIFbFhg0B9XhuLHQ09 +gj+HqUxSiAOuRA5cDU+SykIfb7TLvteZOpl2I= +-----END OPENSSH PRIVATE KEY-----"#; + + let privkey = PrivateKey::from_string_with_passphrase(privkey, Some(format!("Test"))); + match &privkey { + Ok(_) => (), + Err(e) => println!("{}", e), + }; + assert!(privkey.is_err()); +} \ No newline at end of file