use crate::traits::{StorageBackend, StorageError, StorageItem};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use aes_gcm::aead::{Aead, KeyInit};
use async_trait::async_trait;
use rand::RngCore;
use std::path::Path;
use tokio::fs;
pub struct EncryptedBackend<B: StorageBackend> {
inner: B,
key: Key<Aes256Gcm>,
name: String,
}
impl<B: StorageBackend> EncryptedBackend<B> {
pub fn new(inner: B, password: &str) -> Self {
let salt = b"cloud_sync_lib_salt_aes256gcm";
let mut key_bytes = [0u8; 32];
ring::pbkdf2::derive(
ring::pbkdf2::PBKDF2_HMAC_SHA256,
std::num::NonZeroU32::new(10_000).unwrap(),
salt,
password.as_bytes(),
&mut key_bytes,
);
let key = *Key::<Aes256Gcm>::from_slice(&key_bytes);
use zeroize::Zeroize;
key_bytes.zeroize();
let name = format!("Encrypted({})", inner.name());
Self {
inner,
key,
name,
}
}
}
#[async_trait]
impl<B: StorageBackend> StorageBackend for EncryptedBackend<B> {
fn name(&self) -> &str {
&self.name
}
async fn upload(&self, local_path: &Path, remote_path: &str) -> Result<(), StorageError> {
use zeroize::Zeroize;
let mut plaintext = fs::read(local_path).await?;
let mut nonce_bytes = [0u8; 12];
rand::thread_rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let cipher = Aes256Gcm::new(&self.key);
let ciphertext = cipher.encrypt(nonce, plaintext.as_slice())
.map_err(|e| StorageError::Provider { message: format!("Encryption failed: {}", e), status: None })?;
plaintext.zeroize();
let mut payload = Vec::with_capacity(12 + ciphertext.len());
payload.extend_from_slice(&nonce_bytes);
payload.extend_from_slice(&ciphertext);
nonce_bytes.zeroize();
let temp_dir = std::env::temp_dir();
let temp_filename = format!("enc_sync_tmp_{}.enc", rand::random::<u64>());
let temp_path = temp_dir.join(temp_filename);
let mut options = tokio::fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
options.mode(0o600);
}
{
use tokio::io::AsyncWriteExt;
let mut file = options.open(&temp_path).await?;
file.write_all(&payload).await?;
file.flush().await?;
}
let upload_res = self.inner.upload(&temp_path, remote_path).await;
let _ = fs::remove_file(&temp_path).await;
upload_res
}
async fn download(&self, remote_path: &str, local_path: &Path) -> Result<(), StorageError> {
use zeroize::Zeroize;
let temp_dir = std::env::temp_dir();
let temp_filename = format!("dec_sync_tmp_{}.enc", rand::random::<u64>());
let temp_path = temp_dir.join(temp_filename);
let mut options = tokio::fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
options.mode(0o600);
}
{
let _file = options.open(&temp_path).await?;
}
self.inner.download(remote_path, &temp_path).await?;
let mut payload = fs::read(&temp_path).await?;
let _ = fs::remove_file(&temp_path).await;
if payload.len() < 12 {
payload.zeroize();
return Err(StorageError::Provider { message: "Invalid encrypted file: too short".to_string(), status: None });
}
let (nonce_bytes, ciphertext) = payload.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
let cipher = Aes256Gcm::new(&self.key);
let mut plaintext = cipher.decrypt(nonce, ciphertext)
.map_err(|e| StorageError::Provider { message: format!("Decryption failed: {}", e), status: None })?;
payload.zeroize();
if let Some(parent) = local_path.parent() {
fs::create_dir_all(parent).await?;
}
fs::write(local_path, &plaintext).await?;
plaintext.zeroize();
Ok(())
}
async fn delete(&self, remote_path: &str) -> Result<(), StorageError> {
self.inner.delete(remote_path).await
}
async fn list(&self, remote_path: &str) -> Result<Vec<StorageItem>, StorageError> {
let items = self.inner.list(remote_path).await?;
let mut mapped = Vec::with_capacity(items.len());
for mut item in items {
if !item.is_dir {
item.size = item.size.saturating_sub(28);
}
mapped.push(item);
}
Ok(mapped)
}
async fn create_folder(&self, remote_path: &str) -> Result<(), StorageError> {
self.inner.create_folder(remote_path).await
}
}
impl<B: StorageBackend> Drop for EncryptedBackend<B> {
fn drop(&mut self) {
use zeroize::Zeroize;
self.key.as_mut_slice().zeroize();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::providers::local_sim::LocalSimulation;
use tempfile::tempdir;
#[tokio::test]
async fn test_encrypted_backend_flow() {
let temp_dir = tempdir().unwrap();
let local_root = temp_dir.path().join("local");
let remote_root = temp_dir.path().join("remote");
fs::create_dir_all(&local_root).await.unwrap();
fs::create_dir_all(&remote_root).await.unwrap();
let local_sim = LocalSimulation::new(remote_root.clone(), "MockRemote".to_string());
let enc_backend = EncryptedBackend::new(local_sim, "supersecretpassword");
assert_eq!(enc_backend.name(), "Encrypted(MockRemote)");
let local_file = local_root.join("test.txt");
let content = b"Hello, Zero-Knowledge Sync World!";
fs::write(&local_file, content).await.unwrap();
enc_backend.upload(&local_file, "remote_test.txt").await.unwrap();
let remote_stored_file = remote_root.join("remote_test.txt");
let encrypted_content = fs::read(&remote_stored_file).await.unwrap();
assert_ne!(encrypted_content, content);
assert!(encrypted_content.len() > content.len());
let list = enc_backend.list("").await.unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].size, content.len() as u64);
let downloaded_file = local_root.join("downloaded.txt");
enc_backend.download("remote_test.txt", &downloaded_file).await.unwrap();
let decrypted_content = fs::read(&downloaded_file).await.unwrap();
assert_eq!(decrypted_content, content);
}
#[tokio::test]
async fn test_encrypted_backend_edge_cases() {
let temp_dir = tempdir().unwrap();
let remote_root = temp_dir.path().join("remote");
fs::create_dir_all(&remote_root).await.unwrap();
let local_sim = LocalSimulation::new(remote_root.clone(), "MockRemote".to_string());
let enc_backend = EncryptedBackend::new(local_sim, "supersecretpassword");
enc_backend.create_folder("new_dir").await.unwrap();
assert!(remote_root.join("new_dir").exists());
let short_file = remote_root.join("short.txt");
fs::write(&short_file, b"too_short").await.unwrap();
let local_dest = temp_dir.path().join("dest.txt");
let res = enc_backend.download("short.txt", &local_dest).await;
assert!(res.is_err());
assert!(res.unwrap_err().to_string().contains("too short"));
enc_backend.delete("short.txt").await.unwrap();
assert!(!short_file.exists());
}
}