dotenvx-rs 0.2.0

Dotenvx is a Rust command-line/library to encrypt your .env files - limiting their attack vector while retaining their benefits
Documentation
use crate::common::{get_profile_name_from_env, get_profile_name_from_file};
use base64::engine::general_purpose;
use base64::Engine;
use std::env;
use std::env::home_dir;
use std::path::{Path, PathBuf};

pub fn dotenv() -> Result<(), Box<dyn std::error::Error>> {
    // load profile env
    let profile_name = get_profile_name_from_env();
    let env_file = if let Some(name) = &profile_name {
        format!(".env.{}", name)
    } else {
        ".env".to_owned()
    };
    from_path(&env_file)
}

pub fn from_path<P: AsRef<Path>>(env_file: P) -> Result<(), Box<dyn std::error::Error>> {
    if env_file.as_ref().exists() {
        let dotenv_content = std::fs::read_to_string(&env_file)?;
        if dotenv_content.contains("=encrypted:") {
            let profile_name = get_profile_name_from_file(
                env_file.as_ref().file_name().unwrap().to_str().unwrap(),
            );
            let private_key = get_private_key(&profile_name)?;
            for item in dotenvy::from_filename_iter(&env_file)? {
                let (key, value) = item?;
                let env_value = if value.starts_with("encrypted:") {
                    decrypt_env_item(&private_key, &value)?
                } else {
                    value
                };
                unsafe {
                    env::set_var(&key, env_value);
                }
            }
        } else {
            dotenvy::from_filename_override(&env_file)?;
        }
    }
    Ok(())
}

pub fn get_private_key(
    profile_name: &Option<String>,
) -> Result<String, Box<dyn std::error::Error>> {
    let env_key_name = if let Some(name) = profile_name {
        format!("DOTENV_PRIVATE_KEY_{}", name.to_uppercase())
    } else {
        "DOTENV_PRIVATE_KEY".to_string()
    };
    if let Ok(private_key) = env::var(&env_key_name) {
        return Ok(private_key);
    }
    let env_key_prefix = format!("{}=", env_key_name);
    let mut dotenv_file_path = PathBuf::from(".env.keys");
    if !dotenv_file_path.exists() {
        dotenv_file_path = home_dir().unwrap().join(".env.keys");
    }
    if dotenv_file_path.exists() {
        let dotenv_content = std::fs::read_to_string(dotenv_file_path)?;
        if let Some(dotenv_vault) = dotenv_content
            .lines()
            .find(|line| line.starts_with(&env_key_prefix))
        {
            return Ok(dotenv_vault[env_key_prefix.len()..]
                .trim_matches('"')
                .to_owned());
        }
    }
    Err("Private key not found".into())
}

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:") {
        general_purpose::STANDARD
            .decode(&encrypted_text[10..])
            .unwrap()
    } else {
        general_purpose::STANDARD.decode(encrypted_text).unwrap()
    };
    let sk = hex::decode(private_key).unwrap();
    let decrypted_bytes = ecies::decrypt(&sk, &encrypted_bytes).unwrap();
    Ok(String::from_utf8(decrypted_bytes)?)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ecies_decrypt() {
        let encrypted_text = "encrypted:BNexEwjKwt87k9aEgaSng1JY6uW8OkwMYEFTwEy/xyzDrQwQSDIUEXNlcwWi6rnvR1Q60G35NO4NWwhUYAaAON1LOnvMk+tJjTQJaM8DPeX2AJ8IzoTV44FLJsbOiMa77RLrnBv7";
        let private_key = get_private_key(&None).unwrap();
        println!("private_key: {}", private_key);
        let text = decrypt_env_item(&private_key, encrypted_text).unwrap();
        println!("{}", text);
    }
}