use crate::crypto::keypair::Keypair;
use crate::utils::error::{DotenvxError, Result};
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use hkdf::Hkdf;
use rand::RngCore;
use secp256k1::{ecdh::SharedSecret, PublicKey};
use sha2::Sha256;
const ENCRYPTED_PREFIX: &str = "encrypted:";
const AES_KEY_SIZE: usize = 32;
const NONCE_SIZE: usize = 12;
pub fn encrypt(plaintext: &str, public_key_hex: &str) -> Result<String> {
let keypair = Keypair::from_public_key(public_key_hex)?;
let recipient_public_key = keypair.public_key_raw();
let ephemeral_keypair = Keypair::generate();
let ephemeral_secret = ephemeral_keypair.secret_key();
let ephemeral_public = ephemeral_keypair.public_key_raw();
let shared_secret = SharedSecret::new(recipient_public_key, ephemeral_secret);
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_ref());
let mut aes_key = [0u8; AES_KEY_SIZE];
hkdf.expand(b"dotenvx-ecies-aes", &mut aes_key)
.map_err(|e| DotenvxError::EncryptionFailed(format!("HKDF expand failed: {}", e)))?;
let mut nonce_bytes = [0u8; NONCE_SIZE];
rand::thread_rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let cipher = Aes256Gcm::new(&aes_key.into());
let ciphertext = cipher
.encrypt(nonce, plaintext.as_bytes())
.map_err(|e| DotenvxError::EncryptionFailed(format!("AES encryption failed: {}", e)))?;
let ephemeral_public_bytes = ephemeral_public.serialize();
let mut combined = Vec::new();
combined.extend_from_slice(&ephemeral_public_bytes);
combined.extend_from_slice(&nonce_bytes);
combined.extend_from_slice(&ciphertext);
use base64::{engine::general_purpose, Engine as _};
let encoded = general_purpose::STANDARD.encode(&combined);
Ok(format!("{}{}", ENCRYPTED_PREFIX, encoded))
}
pub fn decrypt(encrypted: &str, private_key_hex: &str) -> Result<String> {
if !encrypted.starts_with(ENCRYPTED_PREFIX) {
return Ok(encrypted.to_string());
}
let encoded = &encrypted[ENCRYPTED_PREFIX.len()..];
use base64::{engine::general_purpose, Engine as _};
let combined = general_purpose::STANDARD.decode(encoded).map_err(|_| {
DotenvxError::MalformedEncryptedData {
key: "unknown".to_string(),
}
})?;
if combined.len() < 33 + NONCE_SIZE {
return Err(DotenvxError::MalformedEncryptedData {
key: "unknown".to_string(),
});
}
let ephemeral_public_bytes = &combined[..33];
let nonce_bytes = &combined[33..33 + NONCE_SIZE];
let ciphertext = &combined[33 + NONCE_SIZE..];
let ephemeral_public = PublicKey::from_slice(ephemeral_public_bytes).map_err(|e| {
DotenvxError::MalformedEncryptedData {
key: format!("invalid ephemeral public key: {}", e),
}
})?;
let keypair = Keypair::from_private_key(private_key_hex)?;
let recipient_secret = keypair.secret_key();
let shared_secret = SharedSecret::new(&ephemeral_public, recipient_secret);
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_ref());
let mut aes_key = [0u8; AES_KEY_SIZE];
hkdf.expand(b"dotenvx-ecies-aes", &mut aes_key)
.map_err(|_| DotenvxError::DecryptionFailed {
key: "unknown".to_string(),
private_key_name: "provided".to_string(),
})?;
let cipher = Aes256Gcm::new(&aes_key.into());
let nonce = Nonce::from_slice(nonce_bytes);
let plaintext_bytes =
cipher
.decrypt(nonce, ciphertext)
.map_err(|_| DotenvxError::DecryptionFailed {
key: "unknown".to_string(),
private_key_name: "provided".to_string(),
})?;
let plaintext =
String::from_utf8(plaintext_bytes).map_err(|e| DotenvxError::DecryptionFailed {
key: "unknown".to_string(),
private_key_name: format!("invalid UTF-8: {}", e),
})?;
Ok(plaintext)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encrypt_decrypt_roundtrip() {
let keypair = Keypair::generate();
let plaintext = "Hello, World!";
let encrypted = encrypt(plaintext, &keypair.public_key()).unwrap();
assert!(encrypted.starts_with(ENCRYPTED_PREFIX));
let decrypted = decrypt(&encrypted, &keypair.private_key()).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_encrypt_multiple_times_different_output() {
let keypair = Keypair::generate();
let plaintext = "test";
let encrypted1 = encrypt(plaintext, &keypair.public_key()).unwrap();
let encrypted2 = encrypt(plaintext, &keypair.public_key()).unwrap();
assert_ne!(encrypted1, encrypted2);
assert_eq!(
decrypt(&encrypted1, &keypair.private_key()).unwrap(),
plaintext
);
assert_eq!(
decrypt(&encrypted2, &keypair.private_key()).unwrap(),
plaintext
);
}
#[test]
fn test_decrypt_without_prefix() {
let plaintext = "not_encrypted";
let keypair = Keypair::generate();
let result = decrypt(plaintext, &keypair.private_key()).unwrap();
assert_eq!(result, plaintext);
}
#[test]
fn test_decrypt_invalid_base64() {
let keypair = Keypair::generate();
let result = decrypt("encrypted:!!!invalid!!!", &keypair.private_key());
assert!(result.is_err());
}
#[test]
fn test_decrypt_wrong_key() {
let keypair1 = Keypair::generate();
let keypair2 = Keypair::generate();
let plaintext = "secret";
let encrypted = encrypt(plaintext, &keypair1.public_key()).unwrap();
let result = decrypt(&encrypted, &keypair2.private_key());
assert!(result.is_err());
}
#[test]
fn test_encrypt_empty_string() {
let keypair = Keypair::generate();
let plaintext = "";
let encrypted = encrypt(plaintext, &keypair.public_key()).unwrap();
let decrypted = decrypt(&encrypted, &keypair.private_key()).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_encrypt_long_string() {
let keypair = Keypair::generate();
let plaintext = "a".repeat(10000);
let encrypted = encrypt(&plaintext, &keypair.public_key()).unwrap();
let decrypted = decrypt(&encrypted, &keypair.private_key()).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_encrypt_unicode() {
let keypair = Keypair::generate();
let plaintext = "Hello, δΈη! π";
let encrypted = encrypt(plaintext, &keypair.public_key()).unwrap();
let decrypted = decrypt(&encrypted, &keypair.private_key()).unwrap();
assert_eq!(decrypted, plaintext);
}
}