use anyhow::{anyhow, Context, Result};
use aes::{
cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit},
Aes256,
};
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey};
use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey};
use rand::RngCore;
use cbc::{Decryptor, Encryptor};
use rsa::{Oaep, RsaPrivateKey, RsaPublicKey};
use rsa::pkcs8::LineEnding;
use sled::IVec;
use std::{
fs::{self, File}, io::{self, Read, Write}, path::Path
};
use std::io::{BufReader, BufWriter};
use tar::{Archive, Builder};
use flate2::{write::GzEncoder, Compression};
use flate2::read::GzDecoder;
use tempfile::tempdir;
use walkdir::WalkDir;
use rand::rngs::OsRng;
type Aes256CbcEnc = Encryptor<Aes256>;
type Aes256CbcDec = Decryptor<Aes256>;
const EXTENSION: &str = "esz";
const RSA_KEY_SIZE: usize = 4096;
pub struct SecureCrypto {
private_key: RsaPrivateKey,
public_key: RsaPublicKey,
}
impl SecureCrypto {
pub fn new() -> Result<Self> {
let keys = generate_rsa_keypair().unwrap();
let private_key = RsaPrivateKey::from_pkcs8_pem(&keys.0).unwrap();
let public_key = RsaPublicKey::from_public_key_pem(&keys.1).unwrap();
Ok(Self {
private_key,
public_key,
})
}
pub fn from_pem_keys(public_key_pem: &str, private_key_pem: &str) -> Result<Self> {
let private_key = RsaPrivateKey::from_pkcs8_pem(private_key_pem)
.context("Failed to parse private key PEM")?;
let public_key = RsaPublicKey::from_public_key_pem(public_key_pem)
.context("Failed to parse public key PEM")?;
Ok(Self {
private_key,
public_key,
})
}
pub fn encrypt_string(&self, text: &str) -> Result<Vec<u8>> {
let (aes_key, iv) = self.generate_aes_components();
let ciphertext = self.aes_encrypt(text.as_bytes(), &aes_key, &iv)?;
let encrypted_key = self
.public_key
.encrypt(
&mut rand::thread_rng(),
Oaep::new::<sha2::Sha256>(),
&aes_key,
)
.context("RSA encryption failed")?;
let mut result = Vec::with_capacity(4 + encrypted_key.len() + 16 + ciphertext.len());
result.extend_from_slice(&(encrypted_key.len() as u32).to_le_bytes());
result.extend_from_slice(&encrypted_key);
result.extend_from_slice(&iv);
result.extend_from_slice(&ciphertext);
Ok(result)
}
pub fn decrypt_string(&self, data: &IVec) -> Result<String> {
let data = data.as_ref();
if data.len() < 4 + 16 {
return Err(anyhow!("Invalid encrypted data format: too short ({} bytes)", data.len()));
}
let key_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let key_start = 4;
let key_end = key_start + key_len;
let iv_start = key_end;
let iv_end = iv_start + 16;
let ciphertext_start = iv_end;
if data.len() < ciphertext_start {
return Err(anyhow!("Invalid encrypted data format: key_len {} exceeds data length {} (needs at least {})", key_len, data.len(), ciphertext_start));
}
let encrypted_key = &data[key_start..key_end];
let iv = &data[iv_start..iv_end];
let ciphertext = &data[ciphertext_start..];
let aes_key = self
.private_key
.decrypt(Oaep::new::<sha2::Sha256>(), encrypted_key)
.context("RSA decryption failed")?;
if aes_key.len() != 32 {
return Err(anyhow!("Invalid AES key length: expected 32 bytes, got {}", aes_key.len()));
}
let plaintext = self.aes_decrypt(ciphertext, &aes_key, iv)?;
String::from_utf8(plaintext).context("Decrypted text is not valid UTF-8")
}
pub fn encrypt_path(&self, source_path: impl AsRef<Path>, target_path: impl AsRef<Path>) -> Result<()> {
let source_path = source_path.as_ref();
let target_path = target_path.as_ref();
if source_path.is_dir() {
self.encrypt_dir(source_path, target_path)
} else {
self.encrypt_file(source_path, target_path)
}
}
pub fn decrypt_path(&self, source_path: impl AsRef<Path>, target_path: impl AsRef<Path>) -> Result<()> {
let source_path = source_path.as_ref();
let target_path = target_path.as_ref();
if source_path.is_file() && source_path.extension().map_or(false, |e| e == EXTENSION) {
let name = source_path.file_stem().unwrap().to_str().unwrap();
if name.contains(".tgz") {
self.decrypt_dir(source_path, target_path)
} else {
self.decrypt_file(source_path, target_path)
}
} else {
Err(anyhow!("Invalid source path for decryption: must be a .esz file"))
}
}
pub fn create_tar_archive(&self, source_dir: &Path, tar_path: &Path) -> Result<()> {
let out_file = File::create(tar_path)?;
let extension = tar_path.extension().and_then(|e| e.to_str());
let use_gzip = matches!(extension, Some("gz") | Some("tgz") | Some("esz"));
let mut tar_builder = if use_gzip {
let enc = GzEncoder::new(out_file, Compression::default());
Builder::new(Box::new(enc) as Box<dyn Write>)
} else {
Builder::new(Box::new(out_file) as Box<dyn Write>)
};
for entry in WalkDir::new(source_dir) {
let entry = entry?;
let file_path = entry.path();
if file_path == source_dir {
continue;
}
let relative_path = file_path.strip_prefix(source_dir)
.unwrap_or_else(|_| file_path.file_name().unwrap().as_ref());
if file_path.is_file() {
let mut file = File::open(file_path)?;
tar_builder.append_file(relative_path, &mut file)?;
} else if file_path.is_dir() {
let mut entries = fs::read_dir(file_path)?;
if entries.next().is_none() {
tar_builder.append_dir(relative_path, file_path)?;
}
}
}
tar_builder.finish()?;
Ok(())
}
pub fn extract_tar_archive(&self, tar_path: &Path, output_dir: &Path) -> Result<()> {
if !output_dir.exists() {
fs::create_dir_all(output_dir)?;
}
let extension = tar_path.extension().and_then(|e| e.to_str());
let use_gzip = matches!(extension, Some("gz") | Some("tgz") | Some("esz"));
let file = File::open(tar_path)?;
let mut archive = if use_gzip {
let dec = GzDecoder::new(file);
Archive::new(Box::new(dec) as Box<dyn Read>)
} else {
Archive::new(Box::new(file) as Box<dyn Read>)
};
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?;
let full_path = output_dir.join(path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent)?;
}
entry.unpack(&full_path)?;
}
Ok(())
}
fn generate_aes_components(&self) -> (Vec<u8>, [u8; 16]) {
let mut key = [0u8; 32];
let mut iv = [0u8; 16];
let mut rng = OsRng;
let _ = rng.try_fill_bytes(&mut key);
let _ = rng.try_fill_bytes(&mut iv);
(key.to_vec(), iv)
}
fn aes_encrypt(&self, data: &[u8], key: &[u8], iv: &[u8]) -> Result<Vec<u8>> {
if key.len() != 32 {
return Err(anyhow!("Invalid AES key length"));
}
if iv.len() != 16 {
return Err(anyhow!("Invalid IV length"));
}
let cipher = Aes256CbcEnc::new(key.into(), iv.into());
Ok(cipher.encrypt_padded_vec_mut::<Pkcs7>(data))
}
fn aes_decrypt(&self, data: &[u8], key: &[u8], iv: &[u8]) -> Result<Vec<u8>> {
if key.len() != 32 {
return Err(anyhow!("Invalid AES key length"));
}
if iv.len() != 16 {
return Err(anyhow!("Invalid IV length"));
}
let cipher = Aes256CbcDec::new(key.into(), iv.into());
cipher
.decrypt_padded_vec_mut::<Pkcs7>(data)
.map_err(|e| anyhow!("AES decryption failed: {:?}", e))
}
fn encrypt_file(&self, input_path: &Path, output_path: &Path) -> Result<()> {
let mut input_file = fs::File::open(input_path)
.with_context(|| format!("Failed to open input file: {:?}", input_path))?;
let filename = input_path.file_name() .and_then(|s| s.to_str()) .unwrap_or("");
let mut data = Vec::new();
let mut reader = BufReader::new(&mut input_file);
reader.read_to_end(&mut data)?;
let (aes_key, iv) = self.generate_aes_components();
let encrypted_data = self.aes_encrypt(data.as_ref(), &aes_key, &iv).unwrap();
let encrypted_key = self
.public_key
.encrypt(
&mut rand::thread_rng(),
Oaep::new::<sha2::Sha256>(),
&aes_key,
)
.context("RSA encryption failed")?;
let mut result = Vec::with_capacity(4 + encrypted_key.len() + 16 + encrypted_data.len());
result.extend_from_slice(&(encrypted_key.len() as u32).to_le_bytes());
result.extend_from_slice(&encrypted_key);
result.extend_from_slice(&iv);
result.extend_from_slice(&encrypted_data);
if !output_path.exists() {
fs::create_dir_all(output_path)?;
}
let output_file_path = output_path.join(format!("{}.{}", filename, EXTENSION));
let mut output_file = File::create(&output_file_path).unwrap();
let mut writer = BufWriter::new(&mut output_file);
writer
.write_all(&result).unwrap();
Ok(())
}
fn decrypt_file(&self, input_path: &Path, output_path: &Path) -> Result<()> {
if !input_path.exists() {
return Err(anyhow!("Input file does not exist"));
}
if !input_path.extension().map_or(false, |e| e == EXTENSION) {
return Err(anyhow!("Invalid input file extension"));
}
let mut input_file = File::open(input_path)
.with_context(|| format!("Failed to open input file: {:?}", input_path)).unwrap();
let filename = input_path.file_stem() .and_then(|s| s.to_str()) .unwrap_or("");
let mut encrypted_data = Vec::new();
let mut reader = BufReader::new(&mut input_file);
reader
.read_to_end(&mut encrypted_data)
.context("Failed to read encrypted data")?;
if encrypted_data.len() < 4 + 16 {
return Err(anyhow!("Invalid encrypted data format: too short ({} bytes)", encrypted_data.len()));
}
let key_len = u32::from_le_bytes([encrypted_data[0], encrypted_data[1], encrypted_data[2], encrypted_data[3]]) as usize;
let key_start = 4;
let key_end = key_start + key_len;
let iv_start = key_end;
let iv_end = iv_start + 16;
let ciphertext_start = iv_end;
if encrypted_data.len() < ciphertext_start {
return Err(anyhow!("Invalid encrypted data format: key_len {} exceeds data length {} (needs at least {})",
key_len, encrypted_data.len(), ciphertext_start));
}
let encrypted_key = &encrypted_data[key_start..key_end];
let iv = &encrypted_data[iv_start..iv_end];
let ciphertext = &encrypted_data[ciphertext_start..];
let aes_key = self
.private_key
.decrypt(Oaep::new::<sha2::Sha256>(), encrypted_key)
.context("RSA decryption failed").unwrap();
if aes_key.len() != 32 {
return Err(anyhow!("Invalid AES key length: expected 32 bytes, got {}", aes_key.len()));
}
let decrypted_data = self.aes_decrypt(&ciphertext, &aes_key, &iv).unwrap();
if !output_path.exists() {
fs::create_dir_all(output_path)?;
}
let output_file_path = output_path.join(filename);
let mut output_file = File::create(&output_file_path).unwrap();
let mut writer = BufWriter::new(&mut output_file);
writer
.write_all(&decrypted_data)
.context("Failed to write decrypted data").unwrap();
Ok(())
}
fn encrypt_dir(&self, dir_path: &Path, target_path: &Path) -> Result<()> {
let filename = dir_path.file_name() .and_then(|s| s.to_str()) .unwrap_or("");
let temp_dir = tempdir().expect("Failed to create temp directory");
let temp_tar_path = temp_dir.path().join(format!("{}.tgz", filename));
self.create_tar_archive(dir_path, &temp_tar_path)?;
self.encrypt_file(&temp_tar_path, target_path)?;
temp_dir.close()?;
Ok(())
}
fn decrypt_dir(&self, input_path: &Path, output_dir: &Path) -> Result<()> {
let temp_dir = tempdir().expect("Failed to create temp directory");
let filename = input_path.file_stem() .and_then(|s| s.to_str()) .unwrap_or("");
let decrypted_tar_path = temp_dir.path();
self.decrypt_file(input_path, &decrypted_tar_path)?;
if !output_dir.exists() {
fs::create_dir_all(output_dir)?;
}
let tar_path = decrypted_tar_path.join(filename);
self.extract_tar_archive(&tar_path, output_dir)?;
temp_dir.close()?;
Ok(())
}
}
pub fn generate_rsa_keypair() -> Result<(String, String), String> {
let mut rng = OsRng;
let private_key = RsaPrivateKey::new(&mut rng, RSA_KEY_SIZE)
.map_err(|e| format!("Failed to generate RSA key pair: {}", e))?;
let public_key = private_key.to_public_key();
let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF)
.map_err(|e| format!("Failed to serialize private key: {}", e))?;
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF)
.map_err(|e| format!("Failed to serialize public key: {}", e))?;
Ok((private_key_pem.to_string(), public_key_pem.to_string()))
}
pub fn save_keypair(encrypted_private_key: String, public_key: String, path: &Path) -> Result<(), String> {
let private_key_path = path.join("prikey.esz");
let public_key_path = path.join("pubkey.esz");
let mut private_key_file = File::create(&private_key_path)
.map_err(|e| format!("Failed to create file: {}", e))?;
let mut public_key_file = File::create(&public_key_path)
.map_err(|e| format!("Failed to create file: {}", e))?;
private_key_file.write_all(encrypted_private_key.as_bytes())
.map_err(|e| format!("Failed to write private key: {}", e))?;
public_key_file.write_all(public_key.as_bytes())
.map_err(|e| format!("Failed to write public key: {}", e))?;
Ok(())
}
pub fn read_keypair(path: &Path) -> Result<(String, String), String> {
let private_key_path = path.join("prikey.esz");
let public_key_path = path.join("pubkey.esz");
let private_key_file = File::open(&private_key_path)
.map_err(|e| format!("Failed to open file: {}", e))?;
let public_key_file = File::open(&public_key_path)
.map_err(|e| format!("Failed to open file: {}", e))?;
let private_key = io::read_to_string(private_key_file)
.map_err(|e| format!("Failed to read private key: {}", e))?;
let public_key = io::read_to_string(public_key_file)
.map_err(|e| format!("Failed to read public key: {}", e))?;
Ok((private_key, public_key))
}
use hmac::Hmac;
use hex::encode;
use aes_gcm::{Aes256Gcm, AeadInPlace, KeyInit, Nonce};
use pbkdf2::pbkdf2;
use sha2::Sha256;
pub fn encrypt_private_key(private_key: &str, core_password: &str) -> Result<String, String> {
let mut salt = [0u8; 16];
let mut nonce_bytes = [0u8; 12];
let mut rng = OsRng;
let _ = rng.try_fill_bytes(&mut salt);
let _ = rng.try_fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let mut key = [0u8; 32];
pbkdf2::<Hmac<Sha256>>(
core_password.as_bytes(),
&salt,
100000,
&mut key
);
let cipher = Aes256Gcm::new(&key.into());
let mut data = private_key.as_bytes().to_vec();
cipher.encrypt_in_place(nonce, b"", &mut data)
.map_err(|e| format!("Encryption failed: {}", e))?;
let ciphertext = data;
let mut result = Vec::new();
result.extend_from_slice(&salt);
result.extend_from_slice(&nonce_bytes);
result.extend_from_slice(&ciphertext);
Ok(encode(&result))
}
pub fn decrypt_private_key(encrypted_private_key: &str, core_password: &str) -> Result<String, String> {
let data = hex::decode(encrypted_private_key)
.map_err(|e| format!("Failed to decode encrypted private key: {}", e))?;
if data.len() < 16 + 12 {
return Err("Encrypted private key is too short - must contain at least salt (16 bytes) and nonce (12 bytes)".to_string());
}
let (salt, rest) = data.split_at(16);
let (nonce_bytes, ciphertext) = rest.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
let mut key = [0u8; 32];
pbkdf2::<Hmac<Sha256>>(
core_password.as_bytes(),
salt,
100000,
&mut key
);
let cipher = Aes256Gcm::new(&key.into());
let mut decrypted_data = ciphertext.to_vec();
cipher.decrypt_in_place(nonce, b"", &mut decrypted_data)
.map_err(|e| format!("Decryption failed (invalid password?): {}", e))?;
String::from_utf8(decrypted_data)
.map_err(|e| format!("Invalid UTF-8 in decrypted private key: {}", e))
}