mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
good stuff
This commit is contained in:
parent
d340ff0861
commit
c4bb37e570
8 changed files with 342 additions and 54 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -408,6 +408,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"ed25519",
|
"ed25519",
|
||||||
|
"rand_core",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
"subtle",
|
"subtle",
|
||||||
|
|
@ -1257,6 +1258,7 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
"eyre",
|
"eyre",
|
||||||
|
"pem",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
"ssh-keys",
|
"ssh-keys",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -1269,7 +1271,9 @@ dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"bcrypt-pbkdf",
|
"bcrypt-pbkdf",
|
||||||
"ctr",
|
"ctr",
|
||||||
|
"ed25519-dalek",
|
||||||
"pem",
|
"pem",
|
||||||
|
"rand",
|
||||||
"ssh-transport",
|
"ssh-transport",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@ tracing.workspace = true
|
||||||
ssh-keys = { path = "../../lib/ssh-keys" }
|
ssh-keys = { path = "../../lib/ssh-keys" }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
rpassword = "7.3.1"
|
rpassword = "7.3.1"
|
||||||
|
pem = "3.0.4"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
use std::path::PathBuf;
|
use std::{
|
||||||
|
io::Write,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use eyre::{bail, Context};
|
use eyre::{bail, Context};
|
||||||
use ssh_keys::PrivateKeyType;
|
use ssh_keys::{KeyEncryptionParams, PrivateKeyType};
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
|
@ -13,6 +16,10 @@ struct Args {
|
||||||
|
|
||||||
#[derive(clap::Subcommand)]
|
#[derive(clap::Subcommand)]
|
||||||
enum Subcommand {
|
enum Subcommand {
|
||||||
|
/// Strips PEM armor
|
||||||
|
Unpem { id_file: PathBuf },
|
||||||
|
/// Extract the encrypted part of the private key
|
||||||
|
ExtractEncrypted { id_file: PathBuf },
|
||||||
Info {
|
Info {
|
||||||
/// Decrypt the key to get more information. Will not display private information unless --show-private is used
|
/// Decrypt the key to get more information. Will not display private information unless --show-private is used
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
|
|
@ -22,55 +29,127 @@ enum Subcommand {
|
||||||
show_private: bool,
|
show_private: bool,
|
||||||
id_file: PathBuf,
|
id_file: PathBuf,
|
||||||
},
|
},
|
||||||
|
Generate {
|
||||||
|
#[arg(short, long = "type")]
|
||||||
|
type_: KeyType,
|
||||||
|
#[arg(short, long)]
|
||||||
|
comment: String,
|
||||||
|
#[arg(short, long)]
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::ValueEnum, Clone)]
|
||||||
|
enum KeyType {
|
||||||
|
Ed25519,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> eyre::Result<()> {
|
fn main() -> eyre::Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
|
Subcommand::Unpem { id_file } => {
|
||||||
|
let file = std::fs::read(&id_file)
|
||||||
|
.wrap_err_with(|| format!("reading file {}", id_file.display()))?;
|
||||||
|
let raw = pem::parse(&file)?;
|
||||||
|
std::io::stdout().lock().write_all(raw.contents())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Subcommand::ExtractEncrypted { id_file } => {
|
||||||
|
let file = std::fs::read(&id_file)
|
||||||
|
.wrap_err_with(|| format!("reading file {}", id_file.display()))?;
|
||||||
|
let keys = ssh_keys::EncryptedPrivateKeys::parse(&file)?;
|
||||||
|
let passphrase = if keys.requires_passphrase() {
|
||||||
|
let phrase = rpassword::prompt_password("passphrase: ")?;
|
||||||
|
Some(phrase)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = keys.decrypt_encrypted_part(passphrase.as_deref())?;
|
||||||
|
std::io::stdout().lock().write_all(&data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Subcommand::Info {
|
Subcommand::Info {
|
||||||
id_file,
|
id_file,
|
||||||
decrypt,
|
decrypt,
|
||||||
show_private,
|
show_private,
|
||||||
} => {
|
} => info(&id_file, decrypt, show_private),
|
||||||
if show_private && !decrypt {
|
Subcommand::Generate {
|
||||||
bail!("cannot --show-private without --decrypt");
|
type_,
|
||||||
}
|
comment,
|
||||||
|
path,
|
||||||
|
} => generate(type_, comment, &path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let file = std::fs::read(&id_file)
|
fn info(id_file: &Path, decrypt: bool, show_private: bool) -> eyre::Result<()> {
|
||||||
.wrap_err_with(|| format!("reading file {}", id_file.display()))?;
|
if show_private && !decrypt {
|
||||||
|
bail!("cannot --show-private without --decrypt");
|
||||||
|
}
|
||||||
|
|
||||||
let keys = ssh_keys::EncryptedPrivateKeys::parse_unencrypted(&file)?;
|
let file =
|
||||||
|
std::fs::read(&id_file).wrap_err_with(|| format!("reading file {}", id_file.display()))?;
|
||||||
|
|
||||||
if decrypt {
|
let keys = ssh_keys::EncryptedPrivateKeys::parse(&file)?;
|
||||||
let passphrase = if keys.requires_passphrase() {
|
|
||||||
let phrase = rpassword::prompt_password("passphrase: ")?;
|
|
||||||
Some(phrase)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let keys = keys.parse_private(passphrase.as_deref())?;
|
if decrypt {
|
||||||
for key in keys {
|
let passphrase = if keys.requires_passphrase() {
|
||||||
println!("{} {}", key.private_key.public_key(), key.comment);
|
let phrase = rpassword::prompt_password("passphrase: ")?;
|
||||||
if show_private {
|
Some(phrase)
|
||||||
match key.private_key {
|
} else {
|
||||||
PrivateKeyType::Ed25519 { private_key, .. } => {
|
None
|
||||||
println!(
|
};
|
||||||
" private key: {}",
|
|
||||||
base64::prelude::BASE64_STANDARD_NO_PAD.encode(private_key)
|
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}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for key in keys.public_keys {
|
||||||
|
println!("{key}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate(type_: KeyType, comment: String, path: &Path) -> eyre::Result<()> {
|
||||||
|
let type_ = match type_ {
|
||||||
|
KeyType::Ed25519 => ssh_keys::KeyType::Ed25519,
|
||||||
|
};
|
||||||
|
|
||||||
|
let passphrase = rpassword::prompt_password("Enter passphrase (empty for no passphrase): ")?;
|
||||||
|
|
||||||
|
let key = ssh_keys::PlaintextPrivateKey::generate(
|
||||||
|
comment,
|
||||||
|
ssh_keys::KeyGenerationParams { key_type: type_ },
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("{} {}", key.private_key.public_key(), key.comment);
|
||||||
|
|
||||||
|
let keys = key.encrypt(KeyEncryptionParams::secure_or_none(passphrase))?;
|
||||||
|
|
||||||
|
let mut pubkey_path = path.to_path_buf().into_os_string();
|
||||||
|
pubkey_path.push(".pub");
|
||||||
|
std::fs::write(
|
||||||
|
&pubkey_path,
|
||||||
|
format!("{} {}\n", key.private_key.public_key(), key.comment),
|
||||||
|
)
|
||||||
|
.wrap_err_with(|| format!("writing to {:?}", pubkey_path))?;
|
||||||
|
|
||||||
|
let privkey = keys.to_bytes_armored();
|
||||||
|
|
||||||
|
std::fs::write(path, privkey).wrap_err_with(|| format!("writing to {}", path.display()))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ impl Request {
|
||||||
} => {
|
} => {
|
||||||
p.u8(numbers::SSH_AGENTC_ADD_IDENTITY);
|
p.u8(numbers::SSH_AGENTC_ADD_IDENTITY);
|
||||||
p.string(key_type.as_bytes());
|
p.string(key_type.as_bytes());
|
||||||
p.write(&key_contents);
|
p.raw(&key_contents);
|
||||||
p.string(key_comment.as_bytes());
|
p.string(key_comment.as_bytes());
|
||||||
}
|
}
|
||||||
Self::RemoveAllIdentities => p.u8(numbers::SSH_AGENTC_REMOVE_ALL_IDENTITIES),
|
Self::RemoveAllIdentities => p.u8(numbers::SSH_AGENTC_REMOVE_ALL_IDENTITIES),
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,7 @@ edition = "2021"
|
||||||
aes = "0.8.4"
|
aes = "0.8.4"
|
||||||
bcrypt-pbkdf = "0.10.0"
|
bcrypt-pbkdf = "0.10.0"
|
||||||
ctr = "0.9.2"
|
ctr = "0.9.2"
|
||||||
|
ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
|
||||||
pem = "3.0.4"
|
pem = "3.0.4"
|
||||||
|
rand = "0.8.5"
|
||||||
ssh-transport = { path = "../ssh-transport" }
|
ssh-transport = { path = "../ssh-transport" }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use aes::cipher::{KeySizeUser, StreamCipher};
|
use aes::cipher::{KeySizeUser, StreamCipher};
|
||||||
use ssh_transport::parse::{self, Parser};
|
use ssh_transport::parse::{self, Parser, Writer};
|
||||||
|
|
||||||
|
use crate::PrivateKeyType;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Cipher {
|
pub enum Cipher {
|
||||||
|
|
@ -27,6 +29,13 @@ impl FromStr for Cipher {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cipher {
|
impl Cipher {
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::None => "none",
|
||||||
|
Self::Aes256Ctr => "aes256-ctr",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn key_iv_size(&self) -> (usize, usize) {
|
pub(crate) fn key_iv_size(&self) -> (usize, usize) {
|
||||||
match self {
|
match self {
|
||||||
Cipher::None => (0, 0),
|
Cipher::None => (0, 0),
|
||||||
|
|
@ -34,7 +43,8 @@ impl Cipher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn decrypt_in_place(&self, data: &mut [u8], key: &[u8], iv: &[u8]) {
|
/// Decrypt or encrypt a buffer in place (the same operation due to stream ciphers).
|
||||||
|
pub(crate) fn crypt_in_place(&self, data: &mut [u8], key: &[u8], iv: &[u8]) {
|
||||||
match self {
|
match self {
|
||||||
Cipher::None => unreachable!("cannot decrypt none cipher"),
|
Cipher::None => unreachable!("cannot decrypt none cipher"),
|
||||||
Cipher::Aes256Ctr => {
|
Cipher::Aes256Ctr => {
|
||||||
|
|
@ -84,6 +94,25 @@ impl Kdf {
|
||||||
Ok(kdf)
|
Ok(kdf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::None => "none",
|
||||||
|
Self::BCrypt { .. } => "bcrypt",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn options(&self) -> Vec<u8> {
|
||||||
|
match self {
|
||||||
|
Self::None => Vec::new(),
|
||||||
|
Self::BCrypt { salt, rounds } => {
|
||||||
|
let mut opts = Writer::new();
|
||||||
|
opts.string(salt);
|
||||||
|
opts.u32(*rounds);
|
||||||
|
opts.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn derive(&self, passphrase: &str, output: &mut [u8]) -> parse::Result<()> {
|
pub(crate) fn derive(&self, passphrase: &str, output: &mut [u8]) -> parse::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Self::None => unreachable!("should not attempt to derive passphrase from none"),
|
Self::None => unreachable!("should not attempt to derive passphrase from none"),
|
||||||
|
|
@ -95,3 +124,24 @@ impl Kdf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum KeyType {
|
||||||
|
Ed25519,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct KeyGenerationParams {
|
||||||
|
pub key_type: KeyType,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn generate_private_key(params: KeyGenerationParams) -> PrivateKeyType {
|
||||||
|
match params.key_type {
|
||||||
|
KeyType::Ed25519 => {
|
||||||
|
let private_key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
|
||||||
|
|
||||||
|
PrivateKeyType::Ed25519 {
|
||||||
|
public_key: private_key.verifying_key().to_bytes(),
|
||||||
|
private_key: private_key.to_bytes(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,13 @@ mod crypto;
|
||||||
use crypto::{Cipher, Kdf};
|
use crypto::{Cipher, Kdf};
|
||||||
use ssh_transport::{
|
use ssh_transport::{
|
||||||
key::PublicKey,
|
key::PublicKey,
|
||||||
parse::{self, Parser},
|
parse::{self, Parser, Writer},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: good typed error messages so the user knows what's going on
|
||||||
|
|
||||||
|
pub use crypto::{KeyGenerationParams, KeyType};
|
||||||
|
|
||||||
pub struct EncryptedPrivateKeys {
|
pub struct EncryptedPrivateKeys {
|
||||||
pub public_keys: Vec<PublicKey>,
|
pub public_keys: Vec<PublicKey>,
|
||||||
pub cipher: Cipher,
|
pub cipher: Cipher,
|
||||||
|
|
@ -16,6 +20,7 @@ pub struct EncryptedPrivateKeys {
|
||||||
pub struct PlaintextPrivateKey {
|
pub struct PlaintextPrivateKey {
|
||||||
pub private_key: PrivateKeyType,
|
pub private_key: PrivateKeyType,
|
||||||
pub comment: String,
|
pub comment: String,
|
||||||
|
checkint: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum PrivateKeyType {
|
pub enum PrivateKeyType {
|
||||||
|
|
@ -29,7 +34,7 @@ const MAGIC: &[u8; 15] = b"openssh-key-v1\0";
|
||||||
|
|
||||||
impl EncryptedPrivateKeys {
|
impl EncryptedPrivateKeys {
|
||||||
/// Parse OpenSSH private keys, either armored or not.
|
/// Parse OpenSSH private keys, either armored or not.
|
||||||
pub fn parse_unencrypted(content: &[u8]) -> parse::Result<Self> {
|
pub fn parse(content: &[u8]) -> parse::Result<Self> {
|
||||||
// https://github.com/openssh/openssh-portable/blob/a76a6b85108e3032c8175611ecc5746e7131f876/PROTOCOL.key
|
// https://github.com/openssh/openssh-portable/blob/a76a6b85108e3032c8175611ecc5746e7131f876/PROTOCOL.key
|
||||||
let pem: pem::Pem; // lifetime extension
|
let pem: pem::Pem; // lifetime extension
|
||||||
let content = if content.starts_with(b"openssh-key-v1") {
|
let content = if content.starts_with(b"openssh-key-v1") {
|
||||||
|
|
@ -76,14 +81,35 @@ impl EncryptedPrivateKeys {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_bytes_armored(&self) -> String {
|
||||||
|
let content = self.to_bytes();
|
||||||
|
let pem = pem::Pem::new("OPENSSH PRIVATE KEY", content);
|
||||||
|
pem::encode(&pem)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
|
let mut p = Writer::new();
|
||||||
|
p.array(*MAGIC);
|
||||||
|
p.string(self.cipher.name().as_bytes());
|
||||||
|
p.string(self.kdf.name().as_bytes());
|
||||||
|
p.string(&self.kdf.options());
|
||||||
|
|
||||||
|
p.u32(self.public_keys.len() as u32);
|
||||||
|
|
||||||
|
for pubkey in &self.public_keys {
|
||||||
|
p.string(&pubkey.to_wire_encoding());
|
||||||
|
}
|
||||||
|
|
||||||
|
p.string(&self.encrypted_private_keys);
|
||||||
|
|
||||||
|
p.finish()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn requires_passphrase(&self) -> bool {
|
pub fn requires_passphrase(&self) -> bool {
|
||||||
(!matches!(self.kdf, Kdf::None)) && (!matches!(self.cipher, Cipher::None))
|
(!matches!(self.kdf, Kdf::None)) && (!matches!(self.cipher, Cipher::None))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_private(
|
pub fn decrypt_encrypted_part(&self, passphrase: Option<&str>) -> parse::Result<Vec<u8>> {
|
||||||
&self,
|
|
||||||
passphrase: Option<&str>,
|
|
||||||
) -> parse::Result<Vec<PlaintextPrivateKey>> {
|
|
||||||
let mut data = self.encrypted_private_keys.clone();
|
let mut data = self.encrypted_private_keys.clone();
|
||||||
if self.requires_passphrase() {
|
if self.requires_passphrase() {
|
||||||
let Some(passphrase) = passphrase else {
|
let Some(passphrase) = passphrase else {
|
||||||
|
|
@ -98,16 +124,22 @@ impl EncryptedPrivateKeys {
|
||||||
let mut output = vec![0; key_size + iv_size];
|
let mut output = vec![0; key_size + iv_size];
|
||||||
self.kdf.derive(passphrase, &mut output)?;
|
self.kdf.derive(passphrase, &mut output)?;
|
||||||
let (key, iv) = output.split_at(key_size);
|
let (key, iv) = output.split_at(key_size);
|
||||||
self.cipher.decrypt_in_place(&mut data, &key, &iv);
|
self.cipher.crypt_in_place(&mut data, &key, &iv);
|
||||||
}
|
}
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_private(
|
||||||
|
&self,
|
||||||
|
passphrase: Option<&str>,
|
||||||
|
) -> parse::Result<Vec<PlaintextPrivateKey>> {
|
||||||
|
let data = self.decrypt_encrypted_part(passphrase)?;
|
||||||
|
|
||||||
let mut p = Parser::new(&data);
|
let mut p = Parser::new(&data);
|
||||||
let checkint1 = p.u32()?;
|
let checkint1 = p.u32()?;
|
||||||
let checkint2 = p.u32()?;
|
let checkint2 = p.u32()?;
|
||||||
if checkint1 != checkint2 {
|
if checkint1 != checkint2 {
|
||||||
return Err(parse::ParseError(format!(
|
return Err(parse::ParseError(format!("invalid key or password")));
|
||||||
"failed sanity check, invalid key or password ({checkint1}!={checkint2})"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut result_keys = Vec::new();
|
let mut result_keys = Vec::new();
|
||||||
|
|
@ -115,6 +147,7 @@ impl EncryptedPrivateKeys {
|
||||||
for pubkey in &self.public_keys {
|
for pubkey in &self.public_keys {
|
||||||
let keytype = match pubkey {
|
let keytype = match pubkey {
|
||||||
PublicKey::Ed25519 { public_key } => {
|
PublicKey::Ed25519 { public_key } => {
|
||||||
|
// <https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-3.2.3>
|
||||||
let alg = p.utf8_string()?;
|
let alg = p.utf8_string()?;
|
||||||
if alg != "ssh-ed25519" {
|
if alg != "ssh-ed25519" {
|
||||||
return Err(parse::ParseError(format!(
|
return Err(parse::ParseError(format!(
|
||||||
|
|
@ -150,6 +183,7 @@ impl EncryptedPrivateKeys {
|
||||||
result_keys.push(PlaintextPrivateKey {
|
result_keys.push(PlaintextPrivateKey {
|
||||||
private_key: keytype,
|
private_key: keytype,
|
||||||
comment: comment.to_owned(),
|
comment: comment.to_owned(),
|
||||||
|
checkint: checkint1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,6 +203,98 @@ impl EncryptedPrivateKeys {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct KeyEncryptionParams {
|
||||||
|
pub cipher: Cipher,
|
||||||
|
pub kdf: Kdf,
|
||||||
|
pub passphrase: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyEncryptionParams {
|
||||||
|
pub fn secure_or_none(passphrase: String) -> Self {
|
||||||
|
if passphrase.is_empty() {
|
||||||
|
Self {
|
||||||
|
cipher: Cipher::None,
|
||||||
|
kdf: Kdf::None,
|
||||||
|
passphrase: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self {
|
||||||
|
cipher: Cipher::Aes256Ctr,
|
||||||
|
kdf: Kdf::BCrypt {
|
||||||
|
salt: rand::random(),
|
||||||
|
rounds: 24,
|
||||||
|
},
|
||||||
|
passphrase: Some(passphrase),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaintextPrivateKey {
|
||||||
|
pub fn generate(comment: String, params: KeyGenerationParams) -> Self {
|
||||||
|
let keytype = crypto::generate_private_key(params);
|
||||||
|
Self {
|
||||||
|
comment,
|
||||||
|
private_key: keytype,
|
||||||
|
checkint: rand::random(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(&self, params: KeyEncryptionParams) -> parse::Result<EncryptedPrivateKeys> {
|
||||||
|
let public_keys = vec![self.private_key.public_key()];
|
||||||
|
|
||||||
|
let mut enc = Writer::new();
|
||||||
|
enc.u32(self.checkint);
|
||||||
|
enc.u32(self.checkint);
|
||||||
|
|
||||||
|
match self.private_key {
|
||||||
|
PrivateKeyType::Ed25519 {
|
||||||
|
public_key,
|
||||||
|
private_key,
|
||||||
|
} => {
|
||||||
|
// <https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-3.2.3>
|
||||||
|
enc.string(b"ssh-ed25519");
|
||||||
|
enc.string(&public_key);
|
||||||
|
let combined = private_key.len() + public_key.len();
|
||||||
|
enc.u32(combined as u32);
|
||||||
|
enc.raw(&private_key);
|
||||||
|
enc.raw(&public_key);
|
||||||
|
enc.string(&self.comment.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// uh..., i don't really now how much i need to pad so YOLO this here
|
||||||
|
// TODO: pad properly.
|
||||||
|
enc.u8(1);
|
||||||
|
enc.u8(2);
|
||||||
|
|
||||||
|
let mut encrypted_private_keys = enc.finish();
|
||||||
|
|
||||||
|
match params.cipher {
|
||||||
|
Cipher::None => {}
|
||||||
|
Cipher::Aes256Ctr => {
|
||||||
|
let (key_size, iv_size) = params.cipher.key_iv_size();
|
||||||
|
|
||||||
|
let mut output = vec![0; key_size + iv_size];
|
||||||
|
params
|
||||||
|
.kdf
|
||||||
|
.derive(¶ms.passphrase.unwrap(), &mut output)?;
|
||||||
|
let (key, iv) = output.split_at(key_size);
|
||||||
|
params
|
||||||
|
.cipher
|
||||||
|
.crypt_in_place(&mut encrypted_private_keys, &key, &iv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(EncryptedPrivateKeys {
|
||||||
|
public_keys,
|
||||||
|
cipher: params.cipher,
|
||||||
|
kdf: params.kdf,
|
||||||
|
encrypted_private_keys,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PrivateKeyType {
|
impl PrivateKeyType {
|
||||||
pub fn public_key(&self) -> PublicKey {
|
pub fn public_key(&self) -> PublicKey {
|
||||||
match *self {
|
match *self {
|
||||||
|
|
@ -179,7 +305,7 @@ impl PrivateKeyType {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{Cipher, EncryptedPrivateKeys, Kdf, PrivateKeyType};
|
use crate::{Cipher, EncryptedPrivateKeys, Kdf, KeyEncryptionParams, PrivateKeyType};
|
||||||
|
|
||||||
// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHPaiIO6MePXM/QCJWVge1k4dsiefPr4taP9VJbCtXdx uwu
|
// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHPaiIO6MePXM/QCJWVge1k4dsiefPr4taP9VJbCtXdx uwu
|
||||||
// Password: 'test'
|
// Password: 'test'
|
||||||
|
|
@ -206,7 +332,7 @@ zukcSwhnKrg+wzw7/JZQAAAAA3V3dQEC
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ed25519_none() {
|
fn ed25519_none() {
|
||||||
let keys = EncryptedPrivateKeys::parse_unencrypted(TEST_ED25519_NONE).unwrap();
|
let keys = EncryptedPrivateKeys::parse(TEST_ED25519_NONE).unwrap();
|
||||||
assert_eq!(keys.public_keys.len(), 1);
|
assert_eq!(keys.public_keys.len(), 1);
|
||||||
assert_eq!(keys.cipher, Cipher::None);
|
assert_eq!(keys.cipher, Cipher::None);
|
||||||
assert_eq!(keys.kdf, Kdf::None);
|
assert_eq!(keys.kdf, Kdf::None);
|
||||||
|
|
@ -218,9 +344,22 @@ zukcSwhnKrg+wzw7/JZQAAAAA3V3dQEC
|
||||||
assert!(matches!(key.private_key, PrivateKeyType::Ed25519 { .. }));
|
assert!(matches!(key.private_key, PrivateKeyType::Ed25519 { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_ed25519_none() {
|
||||||
|
let keys = EncryptedPrivateKeys::parse(TEST_ED25519_NONE).unwrap();
|
||||||
|
let decrypted = keys.parse_private(None).unwrap();
|
||||||
|
|
||||||
|
let encrypted = decrypted[0]
|
||||||
|
.encrypt(KeyEncryptionParams::secure_or_none("".to_owned()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let bytes = encrypted.to_bytes();
|
||||||
|
assert_eq!(pem::parse(TEST_ED25519_NONE).unwrap().contents(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ed25519_aes256ctr() {
|
fn ed25519_aes256ctr() {
|
||||||
let keys = EncryptedPrivateKeys::parse_unencrypted(TEST_ED25519_AES256_CTR).unwrap();
|
let keys = EncryptedPrivateKeys::parse(TEST_ED25519_AES256_CTR).unwrap();
|
||||||
assert_eq!(keys.public_keys.len(), 1);
|
assert_eq!(keys.public_keys.len(), 1);
|
||||||
assert_eq!(keys.cipher, Cipher::Aes256Ctr);
|
assert_eq!(keys.cipher, Cipher::Aes256Ctr);
|
||||||
assert!(matches!(keys.kdf, Kdf::BCrypt { .. }));
|
assert!(matches!(keys.kdf, Kdf::BCrypt { .. }));
|
||||||
|
|
@ -231,4 +370,17 @@ zukcSwhnKrg+wzw7/JZQAAAAA3V3dQEC
|
||||||
assert_eq!(key.comment, "uwu");
|
assert_eq!(key.comment, "uwu");
|
||||||
assert!(matches!(key.private_key, PrivateKeyType::Ed25519 { .. }));
|
assert!(matches!(key.private_key, PrivateKeyType::Ed25519 { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_aes256ctr() {
|
||||||
|
let keys = EncryptedPrivateKeys::parse(TEST_ED25519_NONE).unwrap();
|
||||||
|
let decrypted = keys.parse_private(None).unwrap();
|
||||||
|
|
||||||
|
let encrypted = decrypted[0]
|
||||||
|
.encrypt(KeyEncryptionParams::secure_or_none("".to_owned()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let bytes = encrypted.to_bytes();
|
||||||
|
assert_eq!(pem::parse(TEST_ED25519_NONE).unwrap().contents(), bytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,19 +116,19 @@ impl Writer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn u8(&mut self, v: u8) {
|
pub fn u8(&mut self, v: u8) {
|
||||||
self.write(&[v]);
|
self.raw(&[v]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn u32(&mut self, v: u32) {
|
pub fn u32(&mut self, v: u32) {
|
||||||
self.write(&u32::to_be_bytes(v));
|
self.raw(&u32::to_be_bytes(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&mut self, v: &[u8]) {
|
pub fn raw(&mut self, v: &[u8]) {
|
||||||
self.0.extend_from_slice(v);
|
self.0.extend_from_slice(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn array<const N: usize>(&mut self, arr: [u8; N]) {
|
pub fn array<const N: usize>(&mut self, arr: [u8; N]) {
|
||||||
self.write(&arr);
|
self.raw(&arr);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name_list(&mut self, list: NameList<'_>) {
|
pub fn name_list(&mut self, list: NameList<'_>) {
|
||||||
|
|
@ -146,12 +146,12 @@ impl Writer {
|
||||||
if pad_zero {
|
if pad_zero {
|
||||||
self.u8(0);
|
self.u8(0);
|
||||||
}
|
}
|
||||||
self.write(bytes);
|
self.raw(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn string(&mut self, data: &[u8]) {
|
pub fn string(&mut self, data: &[u8]) {
|
||||||
self.u32(data.len() as u32);
|
self.u32(data.len() as u32);
|
||||||
self.write(data);
|
self.raw(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bool(&mut self, v: bool) {
|
pub fn bool(&mut self, v: bool) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue