mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-15 00:45:06 +01:00
encryption
This commit is contained in:
parent
de8f5dde21
commit
e35ff86a12
10 changed files with 494 additions and 7 deletions
53
Cargo.lock
generated
53
Cargo.lock
generated
|
|
@ -149,6 +149,17 @@ version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bcrypt-pbkdf"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
|
||||||
|
dependencies = [
|
||||||
|
"blowfish",
|
||||||
|
"pbkdf2",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
|
|
@ -164,6 +175,16 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blowfish"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -796,6 +817,15 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbkdf2"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pem"
|
name = "pem"
|
||||||
version = "3.0.4"
|
version = "3.0.4"
|
||||||
|
|
@ -1220,6 +1250,29 @@ dependencies = [
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssh-key"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"clap",
|
||||||
|
"eyre",
|
||||||
|
"rpassword",
|
||||||
|
"ssh-keys",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssh-keys"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"bcrypt-pbkdf",
|
||||||
|
"ctr",
|
||||||
|
"pem",
|
||||||
|
"ssh-transport",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ssh-protocol"
|
name = "ssh-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use std::{io::Write, path::PathBuf};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use eyre::{bail, Context};
|
use eyre::{bail, Context};
|
||||||
use ssh_agent_client::{IdentityAnswer, SocketAgentConnection};
|
use ssh_agent_client::{IdentityAnswer, SocketAgentConnection};
|
||||||
use ssh_transport::key::SshPubkey;
|
use ssh_transport::key::PublicKey;
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
|
@ -109,6 +109,7 @@ async fn main() -> eyre::Result<()> {
|
||||||
|
|
||||||
let signature = agent.sign(&key.key_blob, &file, 0).await?;
|
let signature = agent.sign(&key.key_blob, &file, 0).await?;
|
||||||
|
|
||||||
|
// TODO: https://github.com/openssh/openssh-portable/blob/a76a6b85108e3032c8175611ecc5746e7131f876/PROTOCOL.sshsig
|
||||||
let signature = pem::encode(&pem::Pem::new("SSH SIGNATURE", signature));
|
let signature = pem::encode(&pem::Pem::new("SSH SIGNATURE", signature));
|
||||||
std::io::stdout().write_all(signature.as_bytes())?;
|
std::io::stdout().write_all(signature.as_bytes())?;
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +149,7 @@ async fn list_ids(agent: &mut SocketAgentConnection, print_key_id: bool) -> eyre
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_key(id: IdentityAnswer, show_key_id: bool) {
|
fn print_key(id: IdentityAnswer, show_key_id: bool) {
|
||||||
let key = SshPubkey::from_wire_encoding(&id.key_blob);
|
let key = PublicKey::from_wire_encoding(&id.key_blob);
|
||||||
match key {
|
match key {
|
||||||
Ok(key) => {
|
Ok(key) => {
|
||||||
if show_key_id {
|
if show_key_id {
|
||||||
|
|
|
||||||
13
bin/ssh-key/Cargo.toml
Normal file
13
bin/ssh-key/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "ssh-key"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5.16", features = ["derive"] }
|
||||||
|
eyre = "0.6.12"
|
||||||
|
tracing.workspace = true
|
||||||
|
|
||||||
|
ssh-keys = { path = "../../lib/ssh-keys" }
|
||||||
|
base64 = "0.22.1"
|
||||||
|
rpassword = "7.3.1"
|
||||||
76
bin/ssh-key/src/main.rs
Normal file
76
bin/ssh-key/src/main.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use base64::Engine;
|
||||||
|
use clap::Parser;
|
||||||
|
use eyre::{bail, Context};
|
||||||
|
use ssh_keys::PrivateKeyType;
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
struct Args {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Subcommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand)]
|
||||||
|
enum Subcommand {
|
||||||
|
Info {
|
||||||
|
/// Decrypt the key to get more information. Will not display private information unless --show-private is used
|
||||||
|
#[arg(short, long)]
|
||||||
|
decrypt: bool,
|
||||||
|
/// Show the private key. WARNING: This will display the private key
|
||||||
|
#[arg(long)]
|
||||||
|
show_private: bool,
|
||||||
|
id_file: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> eyre::Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Subcommand::Info {
|
||||||
|
id_file,
|
||||||
|
decrypt,
|
||||||
|
show_private,
|
||||||
|
} => {
|
||||||
|
if show_private && !decrypt {
|
||||||
|
bail!("cannot --show-private without --decrypt");
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = std::fs::read(&id_file)
|
||||||
|
.wrap_err_with(|| format!("reading file {}", id_file.display()))?;
|
||||||
|
|
||||||
|
let keys = ssh_keys::EncryptedPrivateKeys::parse_unencrypted(&file)?;
|
||||||
|
|
||||||
|
if decrypt {
|
||||||
|
let passphrase = if keys.requires_passphrase() {
|
||||||
|
let phrase = rpassword::prompt_password("passphrase: ")?;
|
||||||
|
Some(phrase)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let keys = keys.parse_private(passphrase.as_deref())?;
|
||||||
|
for key in keys {
|
||||||
|
println!("{} {}", key.private_key.public_key(), key.comment);
|
||||||
|
if show_private {
|
||||||
|
match key.private_key {
|
||||||
|
PrivateKeyType::Ed25519 { private_key, .. } => {
|
||||||
|
println!(
|
||||||
|
" private key: {}",
|
||||||
|
base64::prelude::BASE64_STANDARD_NO_PAD.encode(private_key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for key in keys.public_keys {
|
||||||
|
println!("{key}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
11
lib/ssh-keys/Cargo.toml
Normal file
11
lib/ssh-keys/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "ssh-keys"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aes = "0.8.4"
|
||||||
|
bcrypt-pbkdf = "0.10.0"
|
||||||
|
ctr = "0.9.2"
|
||||||
|
pem = "3.0.4"
|
||||||
|
ssh-transport = { path = "../ssh-transport" }
|
||||||
3
lib/ssh-keys/README.md
Normal file
3
lib/ssh-keys/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# ssh-keys
|
||||||
|
|
||||||
|
Library for processing OpenSSH keys according to https://github.com/openssh/openssh-portable/blob/a76a6b85108e3032c8175611ecc5746e7131f876/PROTOCOL.key.
|
||||||
97
lib/ssh-keys/src/crypto.rs
Normal file
97
lib/ssh-keys/src/crypto.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use aes::cipher::{KeySizeUser, StreamCipher};
|
||||||
|
use ssh_transport::parse::{self, Parser};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Cipher {
|
||||||
|
None,
|
||||||
|
Aes256Ctr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Cipher {
|
||||||
|
type Err = parse::ParseError;
|
||||||
|
|
||||||
|
fn from_str(ciphername: &str) -> Result<Self, Self::Err> {
|
||||||
|
let cipher = match ciphername {
|
||||||
|
"none" => Cipher::None,
|
||||||
|
"aes256-ctr" => Cipher::Aes256Ctr,
|
||||||
|
_ => {
|
||||||
|
return Err(parse::ParseError(format!(
|
||||||
|
"unsupported cipher: {ciphername}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(cipher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cipher {
|
||||||
|
pub(crate) fn key_iv_size(&self) -> (usize, usize) {
|
||||||
|
match self {
|
||||||
|
Cipher::None => (0, 0),
|
||||||
|
Cipher::Aes256Ctr => (aes::Aes256::key_size(), 16),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn decrypt_in_place(&self, data: &mut [u8], key: &[u8], iv: &[u8]) {
|
||||||
|
match self {
|
||||||
|
Cipher::None => unreachable!("cannot decrypt none cipher"),
|
||||||
|
Cipher::Aes256Ctr => {
|
||||||
|
type Aes256Ctr = ctr::Ctr128BE<aes::Aes256>;
|
||||||
|
let mut cipher =
|
||||||
|
<Aes256Ctr as aes::cipher::KeyIvInit>::new_from_slices(key, iv).unwrap();
|
||||||
|
cipher.apply_keystream(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Kdf {
|
||||||
|
None,
|
||||||
|
BCrypt { salt: [u8; 16], rounds: u32 },
|
||||||
|
}
|
||||||
|
impl Kdf {
|
||||||
|
pub(crate) fn from_str_and_options(
|
||||||
|
kdfname: &str,
|
||||||
|
kdfoptions: &[u8],
|
||||||
|
) -> Result<Self, parse::ParseError> {
|
||||||
|
let kdf = match kdfname {
|
||||||
|
"none" => {
|
||||||
|
if !kdfoptions.is_empty() {
|
||||||
|
return Err(parse::ParseError(format!(
|
||||||
|
"KDF options must be empty for none KDF"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Kdf::None
|
||||||
|
}
|
||||||
|
"bcrypt" => {
|
||||||
|
let mut opts = Parser::new(kdfoptions);
|
||||||
|
let salt = opts.string()?;
|
||||||
|
let rounds = opts.u32()?;
|
||||||
|
Kdf::BCrypt {
|
||||||
|
salt: salt
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| parse::ParseError(format!("incorrect bcrypt salt len")))?,
|
||||||
|
rounds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(parse::ParseError(format!("unsupported KDF: {kdfname}")));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(kdf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn derive(&self, passphrase: &str, output: &mut [u8]) -> parse::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::None => unreachable!("should not attempt to derive passphrase from none"),
|
||||||
|
Self::BCrypt { salt, rounds } => {
|
||||||
|
bcrypt_pbkdf::bcrypt_pbkdf(passphrase, salt, *rounds, output).map_err(|err| {
|
||||||
|
parse::ParseError(format!("error when performing key derivation: {err}"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
220
lib/ssh-keys/src/lib.rs
Normal file
220
lib/ssh-keys/src/lib.rs
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
mod crypto;
|
||||||
|
|
||||||
|
use crypto::{Cipher, Kdf};
|
||||||
|
use ssh_transport::{
|
||||||
|
key::PublicKey,
|
||||||
|
parse::{self, Parser},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct EncryptedPrivateKeys {
|
||||||
|
pub public_keys: Vec<PublicKey>,
|
||||||
|
pub cipher: Cipher,
|
||||||
|
pub kdf: Kdf,
|
||||||
|
pub encrypted_private_keys: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PlaintextPrivateKey {
|
||||||
|
pub private_key: PrivateKeyType,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PrivateKeyType {
|
||||||
|
Ed25519 {
|
||||||
|
public_key: [u8; 32],
|
||||||
|
private_key: [u8; 32],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAGIC: &[u8; 15] = b"openssh-key-v1\0";
|
||||||
|
|
||||||
|
impl EncryptedPrivateKeys {
|
||||||
|
/// Parse OpenSSH private keys, either armored or not.
|
||||||
|
pub fn parse_unencrypted(content: &[u8]) -> parse::Result<Self> {
|
||||||
|
// https://github.com/openssh/openssh-portable/blob/a76a6b85108e3032c8175611ecc5746e7131f876/PROTOCOL.key
|
||||||
|
let pem: pem::Pem; // lifetime extension
|
||||||
|
let content = if content.starts_with(b"openssh-key-v1") {
|
||||||
|
content
|
||||||
|
} else if content.starts_with(b"-----BEGIN OPENSSH PRIVATE KEY-----") {
|
||||||
|
pem = pem::parse(content)
|
||||||
|
.map_err(|err| parse::ParseError(format!("invalid PEM format: {err}")))?;
|
||||||
|
pem.contents()
|
||||||
|
} else {
|
||||||
|
return Err(parse::ParseError("invalid SSH key".to_owned()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut p = Parser::new(content);
|
||||||
|
|
||||||
|
let magic = p.array::<{ MAGIC.len() }>()?;
|
||||||
|
if magic != *MAGIC {
|
||||||
|
return Err(parse::ParseError(
|
||||||
|
"invalid magic, not an SSH key?".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let ciphername = p.utf8_string()?;
|
||||||
|
let cipher = ciphername.parse::<Cipher>()?;
|
||||||
|
let kdfname = p.utf8_string()?;
|
||||||
|
let kdfoptions = p.string()?;
|
||||||
|
let kdf = Kdf::from_str_and_options(kdfname, kdfoptions)?;
|
||||||
|
let keynum = p.u32()?;
|
||||||
|
|
||||||
|
let mut public_keys = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..keynum {
|
||||||
|
let pubkey = p.string()?;
|
||||||
|
let pubkey = PublicKey::from_wire_encoding(pubkey)?;
|
||||||
|
public_keys.push(pubkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
let priv_keys = p.string()?;
|
||||||
|
|
||||||
|
Ok(EncryptedPrivateKeys {
|
||||||
|
public_keys,
|
||||||
|
cipher,
|
||||||
|
kdf,
|
||||||
|
encrypted_private_keys: priv_keys.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn requires_passphrase(&self) -> bool {
|
||||||
|
(!matches!(self.kdf, Kdf::None)) && (!matches!(self.cipher, Cipher::None))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_private(
|
||||||
|
&self,
|
||||||
|
passphrase: Option<&str>,
|
||||||
|
) -> parse::Result<Vec<PlaintextPrivateKey>> {
|
||||||
|
let mut data = self.encrypted_private_keys.clone();
|
||||||
|
if self.requires_passphrase() {
|
||||||
|
let Some(passphrase) = passphrase else {
|
||||||
|
panic!("missing passphrase for encrypted key");
|
||||||
|
};
|
||||||
|
if passphrase.is_empty() {
|
||||||
|
return Err(parse::ParseError(format!("empty passphrase")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (key_size, iv_size) = self.cipher.key_iv_size();
|
||||||
|
|
||||||
|
let mut output = vec![0; key_size + iv_size];
|
||||||
|
self.kdf.derive(passphrase, &mut output)?;
|
||||||
|
let (key, iv) = output.split_at(key_size);
|
||||||
|
self.cipher.decrypt_in_place(&mut data, &key, &iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut p = Parser::new(&self.encrypted_private_keys);
|
||||||
|
let checkint1 = p.u32()?;
|
||||||
|
let checkint2 = p.u32()?;
|
||||||
|
if checkint1 != checkint2 {
|
||||||
|
return Err(parse::ParseError(format!(
|
||||||
|
"failed sanity check, invalid key or password ({checkint1}!={checkint2})"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result_keys = Vec::new();
|
||||||
|
|
||||||
|
for pubkey in &self.public_keys {
|
||||||
|
let keytype = match pubkey {
|
||||||
|
PublicKey::Ed25519 { public_key } => {
|
||||||
|
let alg = p.utf8_string()?;
|
||||||
|
if alg != "ssh-ed25519" {
|
||||||
|
return Err(parse::ParseError(format!(
|
||||||
|
"algorithm mismatch. pubkey: ssh-ed25519, privkey: {alg}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let enc_a = p.string()?; // ENC(A)
|
||||||
|
if enc_a != public_key {
|
||||||
|
return Err(parse::ParseError(format!("public key mismatch")));
|
||||||
|
}
|
||||||
|
let k_enc_a = p.string()?; // k || ENC(A)
|
||||||
|
if k_enc_a.len() != 64 {
|
||||||
|
return Err(parse::ParseError(format!(
|
||||||
|
"invalid len for ed25519 keypair: {}, expected 64",
|
||||||
|
k_enc_a.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let (k, enc_a) = k_enc_a.split_at(32);
|
||||||
|
if enc_a != public_key {
|
||||||
|
// Yes, ed25519 SSH keys seriously store the public key THREE TIMES.
|
||||||
|
return Err(parse::ParseError(format!("public key mismatch")));
|
||||||
|
}
|
||||||
|
let private_key = k.try_into().unwrap();
|
||||||
|
PrivateKeyType::Ed25519 {
|
||||||
|
public_key: *public_key,
|
||||||
|
private_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let comment = p.utf8_string()?;
|
||||||
|
|
||||||
|
result_keys.push(PlaintextPrivateKey {
|
||||||
|
private_key: keytype,
|
||||||
|
comment: comment.to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify padding
|
||||||
|
for i in 1_u8..=255 {
|
||||||
|
if p.has_data() {
|
||||||
|
let b = p.u8()?;
|
||||||
|
if b != i {
|
||||||
|
return Err(parse::ParseError(format!(
|
||||||
|
"private key padding is incorrect: {b} != {i}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result_keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrivateKeyType {
|
||||||
|
pub fn public_key(&self) -> PublicKey {
|
||||||
|
match *self {
|
||||||
|
Self::Ed25519 { public_key, .. } => PublicKey::Ed25519 { public_key },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{Cipher, EncryptedPrivateKeys, Kdf, PrivateKeyType};
|
||||||
|
|
||||||
|
// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHPaiIO6MePXM/QCJWVge1k4dsiefPr4taP9VJbCtXdx uwu
|
||||||
|
// Password: 'test'
|
||||||
|
const _TEST_ED25519_AES256_CTR: &[u8] = b"-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABA5S8LoGs
|
||||||
|
SYFE1uIAlgK4I/AAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHPaiIO6MePXM/QC
|
||||||
|
JWVge1k4dsiefPr4taP9VJbCtXdxAAAAkB9StlI/JgwhtvDGx7v08RAa76W6aXSgbDJTU/
|
||||||
|
KNPzv0yXhCRleYltud2W2R3G6lElGKBgLfC6U944U8ZFHQQevQIHeSGPkbLGklTXrrrLl7
|
||||||
|
ZdWF8er/J/gA0H1T0QE/NYiHxY4NdBzYc4GKCBItOmIT8K/4bsMmh7VXtO0WmkmhoumnLX
|
||||||
|
rsOKyxcDiMs2J8cg==
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
|
";
|
||||||
|
|
||||||
|
// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP60Q8iOyatiPeJbpQ8JVoZazukcSwhnKrg+wzw7/JZQ uwu
|
||||||
|
// no password
|
||||||
|
const TEST_ED25519_NONE: &[u8] = b"-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACD+tEPIjsmrYj3iW6UPCVaGWs7pHEsIZyq4PsM8O/yWUAAAAIj6bZmH+m2Z
|
||||||
|
hwAAAAtzc2gtZWQyNTUxOQAAACD+tEPIjsmrYj3iW6UPCVaGWs7pHEsIZyq4PsM8O/yWUA
|
||||||
|
AAAEAdSh0yeEtOyIa0mzMH36U77BNkiuQkERT8TVTrOOgPyP60Q8iOyatiPeJbpQ8JVoZa
|
||||||
|
zukcSwhnKrg+wzw7/JZQAAAAA3V3dQEC
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
|
";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unencrypted_ed25519() {
|
||||||
|
let keys = EncryptedPrivateKeys::parse_unencrypted(TEST_ED25519_NONE).unwrap();
|
||||||
|
assert_eq!(keys.public_keys.len(), 1);
|
||||||
|
assert_eq!(keys.cipher, Cipher::None);
|
||||||
|
assert_eq!(keys.kdf, Kdf::None);
|
||||||
|
|
||||||
|
let decrypted = keys.parse_private(None).unwrap();
|
||||||
|
assert_eq!(decrypted.len(), 1);
|
||||||
|
let key = decrypted.first().unwrap();
|
||||||
|
assert_eq!(key.comment, "uwu");
|
||||||
|
assert!(matches!(key.private_key, PrivateKeyType::Ed25519 { .. }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
//! Operations on SSH keys.
|
//! Operations on SSH keys.
|
||||||
|
|
||||||
|
// <https://datatracker.ietf.org/doc/html/rfc4716> exists but is kinda weird
|
||||||
|
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
|
||||||
use crate::parse::{self, ParseError, Parser, Writer};
|
use crate::parse::{self, ParseError, Parser, Writer};
|
||||||
|
|
||||||
pub enum SshPubkey {
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PublicKey {
|
||||||
Ed25519 { public_key: [u8; 32] },
|
Ed25519 { public_key: [u8; 32] },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshPubkey {
|
impl PublicKey {
|
||||||
/// Parses an SSH public key from its wire encoding as specified in
|
/// Parses an SSH public key from its wire encoding as specified in
|
||||||
/// RFC4253, RFC5656, and RFC8709.
|
/// RFC4253, RFC5656, and RFC8709.
|
||||||
pub fn from_wire_encoding(bytes: &[u8]) -> parse::Result<Self> {
|
pub fn from_wire_encoding(bytes: &[u8]) -> parse::Result<Self> {
|
||||||
|
|
@ -43,7 +46,7 @@ impl SshPubkey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for SshPubkey {
|
impl Display for PublicKey {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Ed25519 { .. } => {
|
Self::Ed25519 { .. } => {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ impl<'a> Parser<'a> {
|
||||||
Self(data)
|
Self(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remaining(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_data(&self) -> bool {
|
pub fn has_data(&self) -> bool {
|
||||||
!self.0.is_empty()
|
!self.0.is_empty()
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +49,10 @@ impl<'a> Parser<'a> {
|
||||||
pub fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
|
pub fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
|
||||||
assert!(N < 100_000);
|
assert!(N < 100_000);
|
||||||
if self.0.len() < N {
|
if self.0.len() < N {
|
||||||
return Err(ParseError(format!("packet too short")));
|
return Err(ParseError(format!(
|
||||||
|
"packet too short, expected {N} but found {}",
|
||||||
|
self.0.len()
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
let result = self.0[..N].try_into().unwrap();
|
let result = self.0[..N].try_into().unwrap();
|
||||||
self.0 = &self.0[N..];
|
self.0 = &self.0[N..];
|
||||||
|
|
@ -54,7 +61,10 @@ impl<'a> Parser<'a> {
|
||||||
|
|
||||||
pub fn slice(&mut self, len: usize) -> Result<&'a [u8]> {
|
pub fn slice(&mut self, len: usize) -> Result<&'a [u8]> {
|
||||||
if self.0.len() < len {
|
if self.0.len() < len {
|
||||||
return Err(ParseError(format!("packet too short")));
|
return Err(ParseError(format!(
|
||||||
|
"packet too short, expected {len} but found {}",
|
||||||
|
self.0.len()
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
if len > 100_000 {
|
if len > 100_000 {
|
||||||
return Err(ParseError(format!("bytes too long: {len}")));
|
return Err(ParseError(format!("bytes too long: {len}")));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue