1
0
mirror of https://github.com/ssh-vault/ssh-vault.git synced 2025-04-19 07:42:18 +03:00
This commit is contained in:
nbari 2025-04-07 21:23:22 +02:00
parent 38e6f85d34
commit 57dfcf9632
No known key found for this signature in database
23 changed files with 777 additions and 299 deletions

14
.justfile Normal file
View File

@ -0,0 +1,14 @@
test: clippy fmt
cargo test
clippy:
cargo clippy --all -- -W clippy::all -W clippy::nursery -D warnings
fmt:
cargo fmt --all -- --check
coverage:
CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='coverage-%p-%m.profraw' cargo test
grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html
firefox target/coverage/html/index.html
rm -rf *.profraw

View File

@ -1,3 +1,9 @@
# Changelog
## 1.1.0
* using `rsa::RsaPrivatekey::from_components` to create the private key from `ssh_key::PrivateKey::read_openssh_file`
* edition 2024
## 1.0.13
* bump versions, cargo update

872
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "ssh-vault"
version = "1.0.14"
version = "1.1.0"
authors = ["Nicolas Embriz <nbari@tequila.io>"]
description = "encrypt/decrypt using ssh keys"
documentation = "https://ssh-vault.com/"
@ -10,27 +10,27 @@ readme = "README.md"
keywords = ["ssh", "encryption", "fingerprint"]
categories = ["command-line-utilities", "cryptography"]
license = "BSD-3-Clause"
edition = "2021"
edition = "2024"
[dependencies]
aes-gcm = "0.10.3"
anyhow = "1"
base58 = "0.2.0"
base64ct = { version = "1.6.0", features = ["alloc"] }
base64ct = { version = "1.7.3", features = ["alloc"] }
chacha20poly1305 = "0.10.1"
clap = { version = "4.5", features = ["env", "color"] }
config = { version = "0.14", default-features = false, features = ["yaml"] }
ed25519-dalek = { version = "2.1.1", features = ["pkcs8"] }
hex-literal = "0.4.1"
hex-literal = "1.0.0"
hkdf = "0.12.4"
home = "0.5.9"
home = "0.5.11"
md5 = "0.7.0"
openssl = { version = "0.10", optional = true, features = ["vendored"] }
rand = "0.8.5"
regex = "1.11"
reqwest = { version = "0.12", features = ["blocking"] }
rpassword = "7.3"
rsa = { version = "0.9.6", features = ["sha2"] }
rsa = { version = "0.9.8", features = ["sha2"] }
secrecy = "0.10.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@ -38,7 +38,7 @@ sha2 = "0.10.8"
shell-words = "1.1.0"
ssh-key = { version = "0.6.7", features = ["ed25519", "rsa", "encryption"] }
temp-env = "0.3.6"
tempfile = "3.13"
tempfile = "3.19"
url = "2.5"
x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] }
zeroize = "1.8.1"

View File

@ -1,5 +1,5 @@
use crate::tools::get_home;
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use std::{
fs,
path::{Path, PathBuf},

View File

@ -1,6 +1,6 @@
use crate::cli::actions::{process_input, Action};
use crate::vault::{crypto, dio, find, online, remote, SshVault};
use anyhow::{anyhow, Result};
use crate::cli::actions::{Action, process_input};
use crate::vault::{SshVault, crypto, dio, find, online, remote};
use anyhow::{Result, anyhow};
use secrecy::SecretSlice;
use serde::{Deserialize, Serialize};
use ssh_key::PublicKey;
@ -60,7 +60,7 @@ pub fn handle(action: Action) -> Result<()> {
let mut buffer = Vec::new();
// check if we need to skip the editor filename == "-"
let skip_editor = input.as_ref().map_or(false, |stdin| stdin == "-");
let skip_editor = input.as_ref().is_some_and(|stdin| stdin == "-");
// setup Reader(input) and Writer (output)
let (mut input, output) = dio::setup_io(input, vault)?;

View File

@ -1,5 +1,5 @@
use crate::cli::actions::{process_input, Action};
use crate::vault::{crypto, dio, find, parse, ssh::decrypt_private_key, SshVault};
use crate::cli::actions::{Action, process_input};
use crate::vault::{SshVault, crypto, dio, find, parse, ssh::decrypt_private_key};
use anyhow::Result;
use secrecy::{SecretSlice, SecretString};
use std::io::{Read, Write};

View File

@ -4,7 +4,7 @@ pub mod fingerprint;
pub mod view;
use crate::tools;
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use secrecy::{ExposeSecret, SecretString};
use std::{
env,
@ -79,7 +79,7 @@ pub fn process_input(buf: &mut Vec<u8>, data: Option<SecretString>) -> Result<us
#[cfg(test)]
mod tests {
use crate::cli::actions::{create, edit, fingerprint, view, Action};
use crate::cli::actions::{Action, create, edit, fingerprint, view};
use serde_json::Value;
use std::io::Write;
use tempfile::NamedTempFile;
@ -93,24 +93,24 @@ mod tests {
#[test]
fn test_create_view_edit_with_input() {
let tests =[
let tests = [
Test {
input: "Machs na",
public_key: "test_data/ed25519.pub",
private_key: "test_data/ed25519",
header: "SSH-VAULT;CHACHA20-POLY1305"
header: "SSH-VAULT;CHACHA20-POLY1305",
},
Test {
input: "Machs na",
public_key: "test_data/id_rsa.pub",
private_key: "test_data/id_rsa",
header: "SSH-VAULT;AES256"
header: "SSH-VAULT;AES256",
},
Test {
input: "Arrachera is a Mexican dish made from marinated and grilled skirt steak. The steak is seasoned with a mixture of spices and marinades, giving it a rich and savory flavor. Commonly served in tacos or fajitas, arrachera is known for its tenderness and versatility in Mexican cuisine",
public_key: "test_data/ed25519.pub",
private_key: "test_data/ed25519",
header: "SSH-VAULT;CHACHA20-POLY1305"
header: "SSH-VAULT;CHACHA20-POLY1305",
},
];

View File

@ -1,5 +1,5 @@
use crate::cli::actions::Action;
use crate::vault::{dio, find, parse, ssh::decrypt_private_key, SshVault};
use crate::vault::{SshVault, dio, find, parse, ssh::decrypt_private_key};
use anyhow::Result;
use std::io::{Read, Write};
use zeroize::Zeroize;

View File

@ -1,4 +1,4 @@
use clap::{builder::ValueParser, Arg, Command};
use clap::{Arg, Command, builder::ValueParser};
use regex::Regex;
const REGEX_MD5_FINGERPRINT: &str = r"^([0-9a-f]{2}:){15}([0-9a-f]{2})$";

View File

@ -1,4 +1,4 @@
use clap::{builder::ValueParser, Arg, Command};
use clap::{Arg, Command, builder::ValueParser};
pub fn validator_user() -> ValueParser {
ValueParser::from(move |s: &str| -> std::result::Result<String, String> {

View File

@ -4,8 +4,8 @@ pub mod fingerprint;
pub mod view;
use clap::{
builder::styling::{AnsiColor, Effects, Styles},
ColorChoice, Command,
builder::styling::{AnsiColor, Effects, Styles},
};
use std::env;

View File

@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use std::path::PathBuf;
pub fn get_home() -> Result<PathBuf> {

View File

@ -1,8 +1,8 @@
use aes_gcm::{
aead::{generic_array::GenericArray, Aead, AeadCore, KeyInit, OsRng, Payload},
Aes256Gcm,
aead::{Aead, AeadCore, KeyInit, OsRng, Payload, generic_array::GenericArray},
};
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use secrecy::{ExposeSecret, SecretSlice};
pub struct Aes256Crypto {
@ -55,7 +55,7 @@ impl super::Crypto for Aes256Crypto {
mod tests {
use super::*;
use crate::vault::crypto::Crypto;
use rand::{rngs::OsRng, RngCore};
use rand::{RngCore, rngs::OsRng};
use std::collections::HashSet;
const TEST_DATA: &str = "The quick brown fox jumps over the lazy dog";

View File

@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use chacha20poly1305::{
aead::{Aead, AeadCore, KeyInit, OsRng, Payload},
ChaCha20Poly1305,
aead::{Aead, AeadCore, KeyInit, OsRng, Payload},
};
use secrecy::{ExposeSecret, SecretSlice};
@ -56,7 +56,7 @@ impl super::Crypto for ChaCha20Poly1305Crypto {
mod tests {
use super::*;
use crate::vault::crypto::Crypto;
use rand::{rngs::OsRng, RngCore};
use rand::{RngCore, rngs::OsRng};
use std::collections::HashSet;
const TEST_DATA: &str = "The quick brown fox jumps over the lazy dog";

View File

@ -1,9 +1,9 @@
pub mod aes256;
pub mod chacha20poly1305;
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use hkdf::Hkdf;
use rand::{rngs::OsRng, RngCore};
use rand::{RngCore, rngs::OsRng};
use rsa::sha2;
use secrecy::SecretSlice;
use sha2::Sha256;

View File

@ -1,8 +1,8 @@
use crate::{
tools,
vault::{remote, SshKeyType},
vault::{SshKeyType, remote},
};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result, anyhow};
use ssh_key::{Algorithm, PrivateKey, PublicKey};
use std::{
fs::File,
@ -80,7 +80,9 @@ pub fn private_key(key: Option<String>, ssh_type: &SshKeyType) -> Result<Private
// check if it's a legacy rsa key
if private_key.starts_with("-----BEGIN RSA PRIVATE KEY-----") {
return Err(anyhow!("Legacy RSA key not supported, use ssh-keygen -p -f <key> to convert it to openssh format"));
return Err(anyhow!(
"Legacy RSA key not supported, use ssh-keygen -p -f <key> to convert it to openssh format"
));
}
// read openssh key and return it as a PrivateKey

View File

@ -1,6 +1,6 @@
use crate::tools;
use anyhow::{Context, Result};
use rsa::{pkcs8::EncodePublicKey, RsaPublicKey};
use rsa::{RsaPublicKey, pkcs8::EncodePublicKey};
use ssh_key::{HashAlg, PublicKey};
use std::{fmt, fs, path::Path};
@ -200,13 +200,12 @@ mod tests {
},
Test {
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKdb5/i8sIEZ84k+LpJCAxRwxUZsP2MHFWApeB2TSUux ssh-vault",
fingerprint: "SHA256:HcSHlMDnxnmeh6dsxdTrqOGUPp8Ei78VaF9t3ED21S8"
fingerprint: "SHA256:HcSHlMDnxnmeh6dsxdTrqOGUPp8Ei78VaF9t3ED21S8",
},
Test {
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINixf2m2nj8TDeazbWuemUY8ZHNg7znA7hVPN8TJLr2W",
fingerprint: "SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM"
}
fingerprint: "SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM",
},
];
for test in tests.iter() {

View File

@ -61,8 +61,8 @@ pub trait Vault {
mod tests {
use super::*;
use crate::vault::{
crypto, parse, ssh::decrypt_private_key, ssh::ed25519::Ed25519Vault, ssh::rsa::RsaVault,
Vault,
Vault, crypto, parse, ssh::decrypt_private_key, ssh::ed25519::Ed25519Vault,
ssh::rsa::RsaVault,
};
use secrecy::{SecretSlice, SecretString};
use ssh_key::PublicKey;
@ -192,9 +192,11 @@ mod tests {
private_key =
decrypt_private_key(&private_key, Some(SecretString::from(test.passphrase)))?;
}
let key_type = find::key_type(&private_key.algorithm())?;
let v = SshVault::new(&key_type, None, Some(private_key))?;
let vault = v.view(&password, &data, &fingerprint)?;
assert_eq!(vault, SECRET);

View File

@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use base64ct::{Base64, Encoding};
// check if it's a valid SSH-VAULT file and return the data

View File

@ -1,5 +1,5 @@
use crate::{cache, config, tools, vault::fingerprint};
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use reqwest::header::HeaderMap;
use rsa::RsaPublicKey;
use ssh_key::{HashAlg, PublicKey};
@ -135,8 +135,8 @@ pub fn get_user_key(
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::fingerprint::get_remote_fingerprints;
use crate::vault::fingerprint::Fingerprint;
use crate::vault::fingerprint::get_remote_fingerprints;
const KEYS: &str = "
# random comment
@ -176,13 +176,17 @@ Fin
},
Fingerprint {
key: "ID: 3".to_string(),
fingerprints: vec!["SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM".to_string()],
fingerprints: vec![
"SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM".to_string(),
],
comment: "".to_string(),
algorithm: "ssh-ed25519".to_string(),
},
Fingerprint {
key: "ID: 4".to_string(),
fingerprints: vec!["SHA256:HcSHlMDnxnmeh6dsxdTrqOGUPp8Ei78VaF9t3ED21S8".to_string()],
fingerprints: vec![
"SHA256:HcSHlMDnxnmeh6dsxdTrqOGUPp8Ei78VaF9t3ED21S8".to_string(),
],
comment: "ssh-vault".to_string(),
algorithm: "ssh-ed25519".to_string(),
},

View File

@ -1,14 +1,14 @@
use crate::vault::{
crypto, crypto::chacha20poly1305::ChaCha20Poly1305Crypto, crypto::Crypto, Vault,
Vault, crypto, crypto::Crypto, crypto::chacha20poly1305::ChaCha20Poly1305Crypto,
};
use anyhow::{Context, Result};
use base64ct::{Base64, Encoding};
use secrecy::{ExposeSecret, SecretSlice};
use sha2::{Digest, Sha512};
use ssh_key::{
HashAlg, PrivateKey, PublicKey,
private::{Ed25519PrivateKey, KeypairData},
public::KeyData,
HashAlg, PrivateKey, PublicKey,
};
use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey, StaticSecret};
use zeroize::Zeroize;

View File

@ -1,15 +1,16 @@
use crate::vault::{
crypto::aes256::Aes256Crypto, crypto::Crypto, fingerprint::md5_fingerprint, Vault,
Vault, crypto::Crypto, crypto::aes256::Aes256Crypto, fingerprint::md5_fingerprint,
};
use anyhow::{Context, Result};
use base64ct::{Base64, Encoding};
use rand::rngs::OsRng;
use rsa::{Oaep, RsaPrivateKey, RsaPublicKey};
use rsa::{BigUint, Oaep, RsaPrivateKey, RsaPublicKey};
use secrecy::{ExposeSecret, SecretSlice};
use sha2::Sha256;
use ssh_key::{private::KeypairData, public::KeyData, PrivateKey, PublicKey};
use ssh_key::{PrivateKey, PublicKey, private::KeypairData, public::KeyData};
use zeroize::Zeroize;
#[derive(Debug)]
pub struct RsaVault {
public_key: RsaPublicKey,
private_key: Option<RsaPrivateKey>,
@ -22,6 +23,7 @@ impl Vault for RsaVault {
KeyData::Rsa(key_data) => {
let public_key =
RsaPublicKey::try_from(key_data).context("Could not load key")?;
Ok(Self {
public_key,
private_key: None,
@ -31,12 +33,48 @@ impl Vault for RsaVault {
},
(None, Some(private)) => match private.key_data() {
KeypairData::Rsa(key_data) => {
KeypairData::Rsa(rsa_keypair) => {
if private.is_encrypted() {
return Err(anyhow::anyhow!("Private key is encrypted"));
}
let private_key = RsaPrivateKey::try_from(key_data)?;
// Extract components from ssh-key's RSA representation
// Use as_bytes() or a similar method to get the &[u8] from Mpint
//
// <https://docs.rs/ssh-key/latest/ssh_key/private/struct.RsaPrivateKey.html>
//
// pub struct RsaPrivateKey {
// pub d: Mpint,
// pub iqmp: Mpint,
// pub p: Mpint,
// pub q: Mpint,
// }
let n = BigUint::from_bytes_be(rsa_keypair.public.n.as_ref());
let e = BigUint::from_bytes_be(rsa_keypair.public.e.as_ref());
let d = BigUint::from_bytes_be(rsa_keypair.private.d.as_ref());
let p = BigUint::from_bytes_be(rsa_keypair.private.p.as_ref());
let q = BigUint::from_bytes_be(rsa_keypair.private.q.as_ref());
// Create the RSA private key
//
// Constructs an RSA key pair from individual components:
//
// n: RSA modulus
// e: public exponent (i.e. encrypting exponent)
// d: private exponent (i.e. decrypting exponent)
// primes: prime factors of n: typically two primes p and q. More than two
// primes can be provided for multiprime RSA, however this is generally not
// recommended. If no primes are provided, a prime factor recovery algorithm
// will be employed to attempt to recover the factors (as described in NIST SP
// 800-56B Revision 2 Appendix C.2). This algorithm only works if there are
// just two prime factors p and q (as opposed to multiprime), and e is between
// 2^16 and 2^256.
let private_key = RsaPrivateKey::from_components(n, e, d, vec![p, q])?;
// let private_key = RsaPrivateKey::try_from(key_data)?;
let public_key = private_key.to_public_key();
Ok(Self {
public_key,
private_key: Some(private_key),
@ -44,9 +82,11 @@ impl Vault for RsaVault {
}
_ => Err(anyhow::anyhow!("Invalid key type for RsaVault")),
},
(Some(_), Some(_)) => Err(anyhow::anyhow!(
"Only one of public and private key is required"
)),
_ => Err(anyhow::anyhow!("Missing public and private key")),
}
}
@ -119,6 +159,33 @@ mod tests {
let private_key = PrivateKey::read_openssh_file(&private_key_file)?;
let vault = RsaVault::new(Some(public_key), Some(private_key));
assert_eq!(vault.is_err(), true);
let err = vault.unwrap_err();
// Convert the error to a string and check the message
assert_eq!(
err.to_string(),
"Only one of public and private key is required"
);
Ok(())
}
#[test]
fn test_rsa_vault_using_public_key() -> Result<()> {
let public_key_file = Path::new("test_data/id_rsa.pub");
let public_key = PublicKey::read_openssh_file(&public_key_file)?;
let vault = RsaVault::new(Some(public_key), None);
assert_eq!(vault.is_ok(), true);
Ok(())
}
#[test]
fn test_rsa_vault_using_private_key() -> Result<()> {
let private_key_file = Path::new("test_data/id_rsa");
let private_key = PrivateKey::read_openssh_file(&private_key_file)?;
let vault = RsaVault::new(None, Some(private_key));
assert_eq!(vault.is_ok(), true);
Ok(())
}
}