use crate::crypto::{encrypt, Keypair};
use crate::parser::DotenvParser;
use crate::utils::error::{DotenvxError, Result};
use crate::utils::fs::{read_file, write_file};
use std::path::Path;
use tracing::{debug, info};
pub fn encrypt_file(
env_file: &Path,
keys_file: Option<&Path>,
specific_keys: Option<&[String]>,
exclude_keys: Option<&[String]>,
) -> Result<String> {
info!("Encrypting file: {}", env_file.display());
let content = read_file(env_file)?;
let mut parser = DotenvParser::new();
parser.parse(&content)?;
let variables = parser.variables().clone();
let keypair = if variables.contains_key("DOTENV_PUBLIC_KEY") {
debug!("Found existing DOTENV_PUBLIC_KEY");
let private_key = find_private_key(env_file, keys_file, "DOTENV_PRIVATE_KEY")?;
Keypair::from_private_key(&private_key)?
} else {
debug!("Generating new keypair");
Keypair::generate()
};
let public_key = keypair.public_key();
let mut output = String::new();
output.push_str("#/-------------------[DOTENV_PUBLIC_KEY]--------------------/\n");
output.push_str("#/ public-key encryption for .env files /\n");
output.push_str("#/ [how it works](https://dotenvx.com/encryption) /\n");
output.push_str("#/----------------------------------------------------------/\n");
output.push_str(&format!("DOTENV_PUBLIC_KEY=\"{}\"\n\n", public_key));
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
output.push_str(line);
output.push('\n');
continue;
}
if trimmed.starts_with("DOTENV_PUBLIC_KEY=") || trimmed.starts_with("DOTENV_PUBLIC_KEY \"")
{
continue;
}
let (export_prefix, line_content) = if let Some(stripped) = trimmed.strip_prefix("export ")
{
("export ", stripped)
} else {
("", trimmed)
};
if let Some(eq_pos) = line_content.find('=') {
let key = line_content[..eq_pos].trim();
let value_part = line_content[eq_pos + 1..].trim();
let should_encrypt = should_encrypt_key(key, specific_keys, exclude_keys);
if should_encrypt && !value_part.starts_with("encrypted:") {
let value = parse_value(value_part);
let encrypted = encrypt(&value, &public_key)?;
output.push_str(&format!("{}{}=\"{}\"\n", export_prefix, key, encrypted));
debug!("Encrypted key: {}", key);
} else {
output.push_str(line);
output.push('\n');
}
} else {
output.push_str(line);
output.push('\n');
}
}
write_file(env_file, &output)?;
write_keys_file(
env_file,
keys_file,
"DOTENV_PRIVATE_KEY",
&keypair.private_key(),
)?;
info!("✔ encrypted {}", env_file.display());
Ok(public_key)
}
fn should_encrypt_key(
key: &str,
specific_keys: Option<&[String]>,
exclude_keys: Option<&[String]>,
) -> bool {
if key == "DOTENV_PUBLIC_KEY" {
return false;
}
if let Some(keys) = specific_keys {
return keys.contains(&key.to_string());
}
if let Some(keys) = exclude_keys {
return !keys.contains(&key.to_string());
}
true
}
fn parse_value(value: &str) -> String {
let value = value.trim();
if ((value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\'')))
&& value.len() >= 2
{
return value[1..value.len() - 1].to_string();
}
value.to_string()
}
fn find_private_key(env_file: &Path, keys_file: Option<&Path>, key_name: &str) -> Result<String> {
if let Some(keys_path) = keys_file {
if keys_path.exists() {
let content = read_file(keys_path)?;
if let Some(key) = extract_key_from_content(&content, key_name) {
return Ok(key);
}
}
}
if let Some(parent) = env_file.parent() {
let default_keys = parent.join(".env.keys");
if default_keys.exists() {
let content = read_file(&default_keys)?;
if let Some(key) = extract_key_from_content(&content, key_name) {
return Ok(key);
}
}
}
if let Ok(key) = std::env::var(key_name) {
return Ok(key);
}
Err(DotenvxError::MissingPrivateKey {
key_name: key_name.to_string(),
})
}
fn extract_key_from_content(content: &str, key_name: &str) -> Option<String> {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with(&format!("{}=", key_name)) {
let value = &trimmed[key_name.len() + 1..];
return Some(parse_value(value));
}
}
None
}
fn write_keys_file(
env_file: &Path,
keys_file: Option<&Path>,
key_name: &str,
private_key: &str,
) -> Result<()> {
let keys_path = if let Some(path) = keys_file {
path.to_path_buf()
} else {
env_file
.parent()
.ok_or_else(|| DotenvxError::Other("Invalid env file path".to_string()))?
.join(".env.keys")
};
let mut content = if keys_path.exists() {
read_file(&keys_path)?
} else {
String::new()
};
let key_line = format!("{}={}", key_name, private_key);
if !content.contains(&format!("{}=", key_name)) {
if content.is_empty() {
content.push_str("#/------------------!DOTENV_PRIVATE_KEYS!-------------------/\n");
content.push_str("#/ private decryption keys. DO NOT commit to source control /\n");
content.push_str("#/ [how it works](https://dotenvx.com/encryption) /\n");
content.push_str("#/----------------------------------------------------------/\n\n");
content.push_str(&format!(
"# {}\n",
env_file.file_name().unwrap().to_string_lossy()
));
}
content.push_str(&format!("{}\n", key_line));
write_file(&keys_path, &content)?;
info!("✔ key saved to {}", keys_path.display());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_encrypt_file() {
let temp = TempDir::new().unwrap();
let env_file = temp.path().join(".env");
write_file(&env_file, "SECRET=my_secret_value").unwrap();
let public_key = encrypt_file(&env_file, None, None, None).unwrap();
assert_eq!(public_key.len(), 66);
let content = read_file(&env_file).unwrap();
assert!(content.contains("DOTENV_PUBLIC_KEY"));
assert!(content.contains("encrypted:"));
assert!(!content.contains("my_secret_value"));
}
#[test]
fn test_encrypt_specific_keys() {
let temp = TempDir::new().unwrap();
let env_file = temp.path().join(".env");
write_file(&env_file, "KEY1=value1\nKEY2=value2").unwrap();
let keys = vec!["KEY1".to_string()];
encrypt_file(&env_file, None, Some(&keys), None).unwrap();
let content = read_file(&env_file).unwrap();
assert!(content.contains("KEY1=\"encrypted:"));
assert!(content.contains("KEY2=value2"));
}
}