use super::{GitRepo, GitError, GitResult};
use crate::crypto::{CryptoEngine, EncryptedSecret, PlaintextSecret, SecretMetadata, CryptoResult, EncryptionOptions};
use git2::{Repository, Oid, ObjectType, Blob, Tree, TreeBuilder, Signature};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tokio::fs;
use serde::{Deserialize, Serialize};
use base64ct::{Base64, Encoding};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub storage_ref: String,
pub compress: bool,
pub max_blob_size: usize,
pub version: u32,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
storage_ref: "refs/cargocrypt/storage".to_string(),
compress: true,
max_blob_size: 1024 * 1024, version: 1,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageRef {
pub oid: String,
pub path: String,
pub metadata: StorageMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageMetadata {
pub original_path: String,
pub size: u64,
pub timestamp: u64,
pub checksum: String,
pub algorithm: String,
pub compressed: bool,
}
pub struct EncryptedStorage {
repo: GitRepo,
crypto: CryptoEngine,
config: StorageConfig,
}
impl EncryptedStorage {
pub fn new(repo: &GitRepo, crypto: &CryptoEngine) -> GitResult<Self> {
let config = StorageConfig::default();
Ok(Self {
repo: repo.clone(),
crypto: crypto.clone(),
config,
})
}
pub fn with_config(repo: &GitRepo, crypto: &CryptoEngine, config: StorageConfig) -> GitResult<Self> {
Ok(Self {
repo: repo.clone(),
crypto: crypto.clone(),
config,
})
}
pub async fn initialize(&self) -> GitResult<()> {
let git_repo = self.repo.inner();
let signature = self.get_signature()?;
let mut tree_builder = git_repo.treebuilder(None)?;
let tree_oid = tree_builder.write()?;
let tree = git_repo.find_tree(tree_oid)?;
let commit_oid = git_repo.commit(
Some(&self.config.storage_ref),
&signature,
&signature,
"Initialize CargoCrypt encrypted storage",
&tree,
&[],
)?;
let storage_config_path = self.repo.workdir().join(".cargocrypt").join("storage.toml");
let config_content = toml::to_string(&self.config)
.map_err(|e| GitError::StorageFailed(format!("Failed to serialize config: {}", e)))?;
fs::write(&storage_config_path, config_content).await
.map_err(|e| GitError::StorageFailed(format!("Failed to write storage config: {}", e)))?;
Ok(())
}
pub async fn store(&self, file_path: &Path, encrypted_secret: &EncryptedSecret) -> GitResult<StorageRef> {
let git_repo = self.repo.inner();
let encrypted_data = self.serialize_encrypted_secret(encrypted_secret)?;
let final_data = if self.config.compress {
self.compress_data(&encrypted_data)?
} else {
encrypted_data
};
let blob_oid = git_repo.blob(&final_data)?;
let metadata = StorageMetadata {
original_path: file_path.to_string_lossy().to_string(),
size: final_data.len() as u64,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
checksum: self.calculate_checksum(&final_data),
algorithm: "ChaCha20-Poly1305".to_string(), compressed: self.config.compress,
};
let storage_path = self.get_storage_path(file_path);
self.update_storage_tree(&storage_path, blob_oid, &metadata).await?;
Ok(StorageRef {
oid: blob_oid.to_string(),
path: storage_path,
metadata,
})
}
pub async fn retrieve(&self, storage_ref: &StorageRef) -> GitResult<EncryptedSecret> {
let git_repo = self.repo.inner();
let oid = Oid::from_str(&storage_ref.oid)
.map_err(|e| GitError::StorageFailed(format!("Invalid OID: {}", e)))?;
let blob = git_repo.find_blob(oid)?;
let data = if storage_ref.metadata.compressed {
self.decompress_data(blob.content())?
} else {
blob.content().to_vec()
};
self.deserialize_encrypted_secret(&data)
}
pub async fn list_stored_files(&self) -> GitResult<Vec<StorageRef>> {
let git_repo = self.repo.inner();
let mut stored_files = Vec::new();
if let Ok(storage_ref) = git_repo.find_reference(&self.config.storage_ref) {
let commit = git_repo.find_commit(storage_ref.target().unwrap())?;
let tree = commit.tree()?;
tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| {
if entry.kind() == Some(ObjectType::Blob) {
let entry_name = entry.name().unwrap_or("");
let full_path = if root.is_empty() {
entry_name.to_string()
} else {
format!("{}{}", root, entry_name)
};
if let Ok(metadata) = self.load_metadata_for_path(&full_path) {
stored_files.push(StorageRef {
oid: entry.id().to_string(),
path: full_path,
metadata,
});
}
}
git2::TreeWalkResult::Ok
})?;
}
Ok(stored_files)
}
pub async fn delete(&self, storage_ref: &StorageRef) -> GitResult<()> {
self.remove_from_storage_tree(&storage_ref.path).await?;
Ok(())
}
async fn update_storage_tree(&self, path: &str, blob_oid: Oid, metadata: &StorageMetadata) -> GitResult<()> {
let git_repo = self.repo.inner();
let signature = self.get_signature()?;
let current_tree = if let Ok(storage_ref) = git_repo.find_reference(&self.config.storage_ref) {
let commit = git_repo.find_commit(storage_ref.target().unwrap())?;
Some(commit.tree()?)
} else {
None
};
let mut tree_builder = git_repo.treebuilder(current_tree.as_ref())?;
tree_builder.insert(path, blob_oid, git2::FileMode::Blob.into())?;
let metadata_path = format!("{}.metadata", path);
let metadata_json = serde_json::to_string(metadata)
.map_err(|e| GitError::StorageFailed(format!("Failed to serialize metadata: {}", e)))?;
let metadata_oid = git_repo.blob(metadata_json.as_bytes())?;
tree_builder.insert(&metadata_path, metadata_oid, git2::FileMode::Blob.into())?;
let tree_oid = tree_builder.write()?;
let tree = git_repo.find_tree(tree_oid)?;
let parent_commits = if let Ok(storage_ref) = git_repo.find_reference(&self.config.storage_ref) {
vec![git_repo.find_commit(storage_ref.target().unwrap())?]
} else {
vec![]
};
let parent_refs: Vec<&git2::Commit> = parent_commits.iter().collect();
git_repo.commit(
Some(&self.config.storage_ref),
&signature,
&signature,
&format!("Store encrypted file: {}", path),
&tree,
&parent_refs,
)?;
Ok(())
}
async fn remove_from_storage_tree(&self, path: &str) -> GitResult<()> {
let git_repo = self.repo.inner();
let signature = self.get_signature()?;
let storage_ref = git_repo.find_reference(&self.config.storage_ref)?;
let commit = git_repo.find_commit(storage_ref.target().unwrap())?;
let current_tree = commit.tree()?;
let mut tree_builder = git_repo.treebuilder(Some(¤t_tree))?;
tree_builder.remove(path)?;
tree_builder.remove(&format!("{}.metadata", path))?;
let tree_oid = tree_builder.write()?;
let tree = git_repo.find_tree(tree_oid)?;
git_repo.commit(
Some(&self.config.storage_ref),
&signature,
&signature,
&format!("Remove encrypted file: {}", path),
&tree,
&[&commit],
)?;
Ok(())
}
fn get_storage_path(&self, file_path: &Path) -> String {
let path_str = file_path.to_string_lossy();
path_str.replace('/', "_").replace('\\', "_")
}
fn load_metadata_for_path(&self, path: &str) -> GitResult<StorageMetadata> {
let git_repo = self.repo.inner();
let metadata_path = format!("{}.metadata", path);
let storage_ref = git_repo.find_reference(&self.config.storage_ref)?;
let commit = git_repo.find_commit(storage_ref.target().unwrap())?;
let tree = commit.tree()?;
let metadata_entry = tree.get_path(Path::new(&metadata_path))?;
let metadata_blob = git_repo.find_blob(metadata_entry.id())?;
let metadata: StorageMetadata = serde_json::from_slice(metadata_blob.content())
.map_err(|e| GitError::StorageFailed(format!("Failed to deserialize metadata: {}", e)))?;
Ok(metadata)
}
fn serialize_encrypted_secret(&self, encrypted_secret: &EncryptedSecret) -> GitResult<Vec<u8>> {
bincode::serialize(encrypted_secret)
.map_err(|e| GitError::StorageFailed(format!("Failed to serialize encrypted secret: {}", e)))
}
fn deserialize_encrypted_secret(&self, data: &[u8]) -> GitResult<EncryptedSecret> {
bincode::deserialize(data)
.map_err(|e| GitError::StorageFailed(format!("Failed to deserialize encrypted secret: {}", e)))
}
fn compress_data(&self, data: &[u8]) -> GitResult<Vec<u8>> {
use std::io::Write;
use std::io::prelude::*;
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
encoder.write_all(data)
.map_err(|e| GitError::StorageFailed(format!("Compression failed: {}", e)))?;
encoder.finish()
.map_err(|e| GitError::StorageFailed(format!("Compression finish failed: {}", e)))
}
fn decompress_data(&self, data: &[u8]) -> GitResult<Vec<u8>> {
use std::io::prelude::*;
let mut decoder = flate2::read::GzDecoder::new(data);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)
.map_err(|e| GitError::StorageFailed(format!("Decompression failed: {}", e)))?;
Ok(decompressed)
}
fn calculate_checksum(&self, data: &[u8]) -> String {
use ring::digest;
let digest = digest::digest(&digest::SHA256, data);
hex::encode(digest.as_ref())
}
fn get_signature(&self) -> GitResult<Signature> {
self.repo.inner().signature()
.or_else(|_| Signature::now("CargoCrypt Storage", "storage@cargocrypt.local"))
.map_err(|e| GitError::StorageFailed(format!("Failed to create signature: {}", e)))
}
pub async fn get_storage_stats(&self) -> GitResult<StorageStats> {
let stored_files = self.list_stored_files().await?;
let total_files = stored_files.len();
let total_size: u64 = stored_files.iter().map(|f| f.metadata.size).sum();
let compressed_files = stored_files.iter().filter(|f| f.metadata.compressed).count();
let algorithms: HashMap<String, usize> = stored_files.iter().fold(HashMap::new(), |mut acc, f| {
*acc.entry(f.metadata.algorithm.clone()).or_insert(0) += 1;
acc
});
Ok(StorageStats {
total_files,
total_size,
compressed_files,
algorithms,
storage_ref: self.config.storage_ref.clone(),
})
}
pub async fn optimize(&self) -> GitResult<OptimizationResult> {
let mut result = OptimizationResult::default();
let stored_files = self.list_stored_files().await?;
result.files_before = stored_files.len();
result.size_before = stored_files.iter().map(|f| f.metadata.size).sum();
result.files_after = result.files_before;
result.size_after = result.size_before;
Ok(result)
}
pub async fn export(&self, export_path: &Path) -> GitResult<()> {
let stored_files = self.list_stored_files().await?;
fs::create_dir_all(export_path).await
.map_err(|e| GitError::StorageFailed(format!("Failed to create export directory: {}", e)))?;
for storage_ref in stored_files {
let encrypted_secret = self.retrieve(&storage_ref).await?;
let export_file_path = export_path.join(&storage_ref.path);
let serialized = self.serialize_encrypted_secret(&encrypted_secret)?;
fs::write(&export_file_path, serialized).await
.map_err(|e| GitError::StorageFailed(format!("Failed to export file: {}", e)))?;
let metadata_path = export_file_path.with_extension("metadata.json");
let metadata_json = serde_json::to_string_pretty(&storage_ref.metadata)
.map_err(|e| GitError::StorageFailed(format!("Failed to serialize metadata: {}", e)))?;
fs::write(&metadata_path, metadata_json).await
.map_err(|e| GitError::StorageFailed(format!("Failed to export metadata: {}", e)))?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct StorageStats {
pub total_files: usize,
pub total_size: u64,
pub compressed_files: usize,
pub algorithms: HashMap<String, usize>,
pub storage_ref: String,
}
#[derive(Debug, Clone, Default)]
pub struct OptimizationResult {
pub files_before: usize,
pub files_after: usize,
pub size_before: u64,
pub size_after: u64,
pub operations_performed: Vec<String>,
}
pub struct GitObjectStorage {
repo: GitRepo,
config: StorageConfig,
}
impl GitObjectStorage {
pub fn new(repo: &GitRepo) -> GitResult<Self> {
let config = StorageConfig::default();
Ok(Self {
repo: repo.clone(),
config,
})
}
pub async fn store_large_file(&self, file_path: &Path) -> GitResult<Vec<Oid>> {
let git_repo = self.repo.inner();
let mut oids = Vec::new();
let file_content = fs::read(file_path).await
.map_err(|e| GitError::StorageFailed(format!("Failed to read file: {}", e)))?;
let chunks = if file_content.len() > self.config.max_blob_size {
file_content.chunks(self.config.max_blob_size).collect::<Vec<_>>()
} else {
vec![&file_content[..]]
};
for chunk in chunks {
let oid = git_repo.blob(chunk)?;
oids.push(oid);
}
Ok(oids)
}
pub async fn retrieve_large_file(&self, oids: &[Oid]) -> GitResult<Vec<u8>> {
let git_repo = self.repo.inner();
let mut content = Vec::new();
for oid in oids {
let blob = git_repo.find_blob(*oid)?;
content.extend_from_slice(blob.content());
}
Ok(content)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use crate::crypto::{PlaintextSecret, SecretType};
#[tokio::test]
async fn test_encrypted_storage_creation() {
let temp_dir = TempDir::new().unwrap();
let repo = GitRepo::init(temp_dir.path()).unwrap();
let crypto = CryptoEngine::new();
let storage = EncryptedStorage::new(&repo, &crypto).unwrap();
storage.initialize().await.unwrap();
let config_path = temp_dir.path().join(".cargocrypt/storage.toml");
assert!(config_path.exists());
}
#[tokio::test]
async fn test_store_and_retrieve() {
let temp_dir = TempDir::new().unwrap();
let repo = GitRepo::init(temp_dir.path()).unwrap();
let crypto = CryptoEngine::new();
let storage = EncryptedStorage::new(&repo, &crypto).unwrap();
storage.initialize().await.unwrap();
let plaintext = PlaintextSecret::new("test-secret".as_bytes().to_vec());
let encrypted = crypto.encrypt(plaintext, "test_password", EncryptionOptions::default()).unwrap();
let file_path = Path::new("test.secret");
let storage_ref = storage.store(file_path, &encrypted).await.unwrap();
let retrieved = storage.retrieve(&storage_ref).await.unwrap();
let decrypted = crypto.decrypt(&retrieved, "test_password").unwrap();
assert_eq!(decrypted.as_bytes(), b"test-secret");
}
#[tokio::test]
async fn test_list_stored_files() {
let temp_dir = TempDir::new().unwrap();
let repo = GitRepo::init(temp_dir.path()).unwrap();
let crypto = CryptoEngine::new();
let storage = EncryptedStorage::new(&repo, &crypto).unwrap();
storage.initialize().await.unwrap();
for i in 0..3 {
let plaintext = PlaintextSecret::new(format!("secret-{}", i).as_bytes().to_vec());
let encrypted = crypto.encrypt(plaintext, "test_password", EncryptionOptions::default()).unwrap();
let file_name = format!("test{}.secret", i);
let file_path = Path::new(&file_name);
storage.store(file_path, &encrypted).await.unwrap();
}
let stored_files = storage.list_stored_files().await.unwrap();
assert_eq!(stored_files.len(), 3);
}
#[tokio::test]
async fn test_storage_stats() {
let temp_dir = TempDir::new().unwrap();
let repo = GitRepo::init(temp_dir.path()).unwrap();
let crypto = CryptoEngine::new();
let storage = EncryptedStorage::new(&repo, &crypto).unwrap();
storage.initialize().await.unwrap();
let plaintext = PlaintextSecret::new("test-secret".as_bytes().to_vec());
let encrypted = crypto.encrypt(plaintext, "test_password", EncryptionOptions::default()).unwrap();
let file_path = Path::new("test.secret");
storage.store(file_path, &encrypted).await.unwrap();
let stats = storage.get_storage_stats().await.unwrap();
assert_eq!(stats.total_files, 1);
assert!(stats.total_size > 0);
}
}