use crate::commands::trim_private_key;
use aes_gcm::aead::OsRng;
use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use argon2::password_hash::SaltString;
use argon2::{self, Argon2, PasswordHasher};
use base64ct::{Base64, Base64UrlUnpadded, Encoding};
use colored::Colorize;
use dotenvx_rs::dotenvx::get_private_key;
use ecies::utils::generate_keypair;
use ecies::{PublicKey, SecretKey};
use k256::ecdsa::{SigningKey, VerifyingKey, Signature as EcdsaSignature, signature::hazmat::PrehashVerifier};
use native_tls::{HandshakeError, TlsConnector};
use serde_json::json;
use sha2::{Digest, Sha256};
use std::fs;
use std::fs::File;
use std::io::Write;
use std::net::TcpStream;
use std::path::Path;
use totp_rs::TOTP;
pub struct EcKeyPair {
pub public_key: PublicKey,
pub secret_key: SecretKey,
}
impl EcKeyPair {
pub fn generate() -> Self {
let (sk, pk) = generate_keypair();
EcKeyPair {
public_key: pk,
secret_key: sk,
}
}
pub fn from_secret_key(sk_hex: &str) -> Self {
let sk_bytes = hex::decode(check_sk_hex(&trim_private_key(sk_hex.to_string()))).unwrap();
let sk = SecretKey::parse_slice(&sk_bytes).unwrap();
let pk = PublicKey::from_secret_key(&sk);
EcKeyPair {
public_key: pk,
secret_key: sk,
}
}
pub fn from_input(sk_hex: &str) -> anyhow::Result<Self> {
let sk_bytes = hex::decode(trim_private_key(sk_hex.to_string()))?;
let sk =
SecretKey::parse_slice(&sk_bytes).map_err(|_| anyhow::anyhow!("Invalid secret key"))?;
let pk = PublicKey::from_secret_key(&sk);
Ok(EcKeyPair {
public_key: pk,
secret_key: sk,
})
}
pub fn get_pk_hex(&self) -> String {
let pk_compressed_bytes = self.public_key.serialize_compressed();
hex::encode(pk_compressed_bytes)
}
pub fn get_sk_hex(&self) -> String {
let sk_bytes = self.secret_key.serialize();
hex::encode(sk_bytes)
}
}
pub fn check_sk_hex(sk_hex: &str) -> &str {
let key_len = sk_hex.len();
if key_len > 64 {
&sk_hex[(key_len - 64)..]
} else {
sk_hex
}
}
pub fn encrypt_env_item(
public_key: &str,
value_plain: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let pk_bytes = hex::decode(public_key).unwrap();
let encrypted_bytes = ecies::encrypt(&pk_bytes, value_plain.as_bytes()).unwrap();
let base64_text = Base64::encode_string(&encrypted_bytes);
Ok(format!("encrypted:{base64_text}"))
}
pub fn decrypt_env_item(
private_key: &str,
encrypted_text: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let encrypted_bytes = if encrypted_text.starts_with("encrypted:") {
Base64::decode_vec(encrypted_text.strip_prefix("encrypted:").unwrap()).unwrap()
} else {
Base64::decode_vec(encrypted_text).unwrap()
};
let sk = hex::decode(check_sk_hex(private_key)).unwrap();
let decrypted_bytes = ecies::decrypt(&sk, &encrypted_bytes).unwrap();
Ok(String::from_utf8(decrypted_bytes)?)
}
pub fn decrypt_value(profile: &Option<String>, encrypted_value: &str) {
if let Ok(private_key) = get_private_key(&None, profile) {
if let Ok(plain_text) = decrypt_env_item(check_sk_hex(&private_key), encrypted_value) {
println!("{plain_text}");
} else {
eprintln!(
"{}",
"Failed to decrypt the value, please check the private key and profile.".red()
);
}
} else {
eprintln!("{}",
"Private key not found, please check the DOTENV_PRIVATE_KEY environment variable or '.env.key' file.".red()
);
}
}
pub fn sha256(input: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(input);
let result = hasher.finalize();
hex::encode(result)
}
#[allow(dead_code)]
fn get_https_cert_sha256(host: &str, port: u16) -> anyhow::Result<String> {
let connector = TlsConnector::new()?;
let stream = TcpStream::connect(format!("{host}:{port}"))?;
match connector.connect(host, stream) {
Ok(tls_stream) => {
if let Some(cert) = tls_stream.peer_certificate()? {
let der = cert.to_der()?;
return Ok(sha256(&der));
}
}
Err(HandshakeError::Failure(e)) => {
return Err(anyhow::anyhow!(e.to_string()));
}
Err(HandshakeError::WouldBlock(_)) => {
return Err(anyhow::anyhow!("Failed to get certificate"));
}
}
Err(anyhow::anyhow!("Failed to get certificate"))
}
pub fn sign_message(private_key: &str, message: &str) -> anyhow::Result<String> {
let mut hasher = Sha256::new();
hasher.update(message.trim());
let message_hash = hasher.finalize();
let sk_bytes = hex::decode(check_sk_hex(private_key))?;
let signing_key = SigningKey::from_slice(&sk_bytes)
.map_err(|_| anyhow::anyhow!("Invalid private key format"))?;
let (signature, _) = signing_key.sign_prehash_recoverable(&message_hash)
.map_err(|_| anyhow::anyhow!("Signing failed"))?;
Ok(Base64::encode_string(&signature.to_bytes()))
}
pub fn sign_message_bytes(private_key: &str, message: &str) -> anyhow::Result<Vec<u8>> {
let mut hasher = Sha256::new();
hasher.update(message.trim());
let message_hash = hasher.finalize();
let sk_bytes = hex::decode(check_sk_hex(private_key))?;
let signing_key = SigningKey::from_slice(&sk_bytes)
.map_err(|_| anyhow::anyhow!("Invalid private key format"))?;
let (signature, _) = signing_key.sign_prehash_recoverable(&message_hash)
.map_err(|_| anyhow::anyhow!("Signing failed"))?;
Ok(signature.to_bytes().to_vec())
}
pub fn verify_signature(public_key: &str, message: &str, signature: &str) -> anyhow::Result<bool> {
let mut hasher = Sha256::new();
hasher.update(message.trim());
let message_hash = hasher.finalize();
let pk_bytes = hex::decode(public_key)?;
let verifying_key = VerifyingKey::from_sec1_bytes(&pk_bytes)
.map_err(|_| anyhow::anyhow!("Invalid public key format"))?;
let signature_bytes = Base64::decode_vec(signature)?;
let sig = EcdsaSignature::from_slice(&signature_bytes)
.map_err(|_| anyhow::anyhow!("Invalid signature format"))?;
verifying_key.verify_prehash(&message_hash, &sig)
.map_err(|_| anyhow::anyhow!("Signature verification failed"))?;
Ok(true)
}
pub fn generate_jwt_token(
private_key_hex: &str,
claims: serde_json::Value,
) -> anyhow::Result<String> {
let header_obj = json!({"typ": "JWT","alg": "ES256K"});
let header = Base64UrlUnpadded::encode_string(serde_json::to_string(&header_obj)?.as_bytes());
let payload = Base64UrlUnpadded::encode_string(serde_json::to_string(&claims)?.as_bytes());
let message = format!("{header}.{payload}");
let signature_bytes = sign_message_bytes(private_key_hex, &message)?;
let signature = Base64UrlUnpadded::encode_string(signature_bytes.as_slice());
Ok(format!("{header}.{payload}.{signature}"))
}
pub fn encrypt_file<P: AsRef<Path>>(
input_file: P,
output_file: P,
password: &str,
) -> anyhow::Result<()> {
let plain_bytes = std::fs::read(input_file)?;
let argon2 = Argon2::default();
let salt = SaltString::generate(&mut OsRng);
let password_hash = argon2.hash_password(password.as_bytes(), &salt).unwrap();
let hash = password_hash.hash.unwrap();
let aes_key = Key::<Aes256Gcm>::from_slice(hash.as_bytes()); let cipher = Aes256Gcm::new(aes_key);
let random_nonce = rand::random::<[u8; 12]>();
let ciphertext = cipher
.encrypt(Nonce::from_slice(&random_nonce), plain_bytes.as_ref())
.expect("encryption failure!");
let mut output = File::create(output_file)?;
let mut salt_bytes: [u8; 16] = [0; 16];
salt.decode_b64(&mut salt_bytes).unwrap();
output.write_all(&salt_bytes)?; output.write_all(&random_nonce)?; output.write_all(&ciphertext)?; Ok(())
}
pub fn decrypt_file<P: AsRef<Path>>(
encrypted_file: P,
output_file: P,
password: &str,
) -> anyhow::Result<()> {
let encrypted_file_content = fs::read(encrypted_file)?;
let salt_bytes = &encrypted_file_content[0..16]; let salt = SaltString::encode_b64(salt_bytes).unwrap();
let nonce_bytes = &encrypted_file_content[16..28]; let ciphertext = &encrypted_file_content[28..];
let argon2 = Argon2::default();
let password_hash = argon2.hash_password(password.as_bytes(), &salt).unwrap();
let hash = password_hash.hash.unwrap();
let aes_key = Key::<Aes256Gcm>::from_slice(hash.as_bytes());
let cipher = Aes256Gcm::new(aes_key);
let plain_bytes = cipher
.decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
.expect("decryption failure!");
fs::write(output_file, plain_bytes)?;
Ok(())
}
pub fn generate_totp_password(totp_url: &str) -> anyhow::Result<String> {
let totp = TOTP::from_url(totp_url)?;
totp.generate_current()
.map_err(|e| anyhow::anyhow!(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use testresult::TestResult;
#[test]
fn test_signature_and_verify() {
let public_key = "02f6e5c1a348cd70ee9ebcdf271892878d83d7bb9c1cd0644ae0da0a04904b83e4";
let private_key = "00d3a1bf3a9a989e3ae11a58e89d95e26d32e0445d825d7fe7ef162ffad2706580";
let message = "Hello, secp256k1!";
let signature = sign_message(private_key, message).unwrap();
println!("Signature: {signature}");
let verify_result = verify_signature(public_key, message, &signature).unwrap();
assert!(verify_result, "Signature verification failed");
}
#[test]
fn test_jwt_generate() {
use chrono::Utc;
let now = Utc::now().timestamp();
let private_key = "c81efd721a711661296a53b768c780e0d9ec9d597e49d8ed53eed0b638b958cf";
let claims = json!({
"sub": "linux-china",
"kid": "b895c58f944855010fa88f7a76642e2005d51705cb27597c7a85347628ac5dcf",
"exp": now + 60*60*24*365, "iat": now, "iss": "dotenvx"
});
let jwt_token = generate_jwt_token(private_key, claims).unwrap();
println!("JWT: {jwt_token}");
}
#[test]
fn test_encrypt_file() -> TestResult {
let input_file = "tests/example.txt";
let output_file = "tests/example.txt.aes";
let password = "your_secure_password";
encrypt_file(input_file, output_file, password).unwrap();
Ok(())
}
#[test]
fn test_decrypt_file() -> TestResult {
let encrypted_file = "tests/example.txt.aes";
let output_file = "tests/example.txt";
let password = "your_secure_password";
decrypt_file(encrypted_file, output_file, password).unwrap();
Ok(())
}
#[test]
fn test_https_cert() {
let finger_print = get_https_cert_sha256("dotenvx.microservices.club", 443).unwrap();
println!("finger_print : {}", finger_print);
}
}