use crate::error::{CargoCryptError, CryptoResult};
use crate::crypto::{CryptoEngine, PerformanceProfile, EncryptedSecret};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Debug)]
pub struct CargoCrypt {
engine: Arc<CryptoEngine>,
config: Arc<RwLock<CryptoConfig>>,
project_root: PathBuf,
secret_store: Arc<dyn SecretStore>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CryptoConfig {
pub performance_profile: PerformanceProfile,
pub key_params: KeyDerivationConfig,
pub file_ops: FileOperationConfig,
pub security: SecurityConfig,
pub performance: PerformanceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyDerivationConfig {
pub memory_cost: u32,
pub time_cost: u32,
pub parallelism: u32,
pub output_length: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileOperationConfig {
pub backup_originals: bool,
pub encrypted_extension: String,
pub buffer_size: usize,
pub preserve_permissions: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
pub require_confirmation: bool,
pub auto_zeroize: bool,
pub fail_secure: bool,
pub max_password_attempts: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceConfig {
pub async_operations: bool,
pub max_concurrent_ops: usize,
pub progress_reporting: bool,
pub key_caching: bool,
}
pub trait SecretStore: Send + Sync + std::fmt::Debug {
fn store_secret(&self, key: &str, secret: SecretBytes) -> CryptoResult<()>;
fn get_secret(&self, key: &str) -> CryptoResult<Option<SecretBytes>>;
fn remove_secret(&self, key: &str) -> CryptoResult<bool>;
fn clear_all(&self) -> CryptoResult<()>;
fn contains_secret(&self, key: &str) -> bool;
fn secret_count(&self) -> usize;
}
#[derive(Debug, Clone, Zeroize, ZeroizeOnDrop)]
pub struct SecretBytes {
inner: Vec<u8>,
}
impl SecretBytes {
pub fn new(data: Vec<u8>) -> Self {
Self { inner: data }
}
pub fn from_str(s: &str) -> Self {
Self::new(s.as_bytes().to_vec())
}
pub fn expose_secret(&self) -> &[u8] {
&self.inner
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn to_string_lossy(&self) -> String {
String::from_utf8_lossy(&self.inner).to_string()
}
}
#[derive(Debug, Default)]
pub struct InMemorySecretStore {
secrets: Arc<RwLock<std::collections::HashMap<String, SecretBytes>>>,
}
impl InMemorySecretStore {
pub fn new() -> Self {
Self {
secrets: Arc::new(RwLock::new(std::collections::HashMap::new())),
}
}
}
impl SecretStore for InMemorySecretStore {
fn store_secret(&self, key: &str, secret: SecretBytes) -> CryptoResult<()> {
let mut secrets = self.secrets.blocking_write();
secrets.insert(key.to_string(), secret);
Ok(())
}
fn get_secret(&self, key: &str) -> CryptoResult<Option<SecretBytes>> {
let secrets = self.secrets.blocking_read();
Ok(secrets.get(key).cloned())
}
fn remove_secret(&self, key: &str) -> CryptoResult<bool> {
let mut secrets = self.secrets.blocking_write();
Ok(secrets.remove(key).is_some())
}
fn clear_all(&self) -> CryptoResult<()> {
let mut secrets = self.secrets.blocking_write();
secrets.clear();
Ok(())
}
fn contains_secret(&self, key: &str) -> bool {
let secrets = self.secrets.blocking_read();
secrets.contains_key(key)
}
fn secret_count(&self) -> usize {
let secrets = self.secrets.blocking_read();
secrets.len()
}
}
impl Default for CryptoConfig {
fn default() -> Self {
Self {
performance_profile: PerformanceProfile::Balanced,
key_params: KeyDerivationConfig::default(),
file_ops: FileOperationConfig::default(),
security: SecurityConfig::default(),
performance: PerformanceConfig::default(),
}
}
}
impl Default for KeyDerivationConfig {
fn default() -> Self {
Self {
memory_cost: 65536, time_cost: 3, parallelism: 4, output_length: 32, }
}
}
impl Default for FileOperationConfig {
fn default() -> Self {
Self {
backup_originals: true,
encrypted_extension: "enc".to_string(),
buffer_size: 64 * 1024, preserve_permissions: true,
}
}
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
require_confirmation: true,
auto_zeroize: true,
fail_secure: true,
max_password_attempts: 3,
}
}
}
impl Default for PerformanceConfig {
fn default() -> Self {
Self {
async_operations: true,
max_concurrent_ops: 4,
progress_reporting: true,
key_caching: true,
}
}
}
impl CargoCrypt {
pub async fn new() -> CryptoResult<Self> {
let config = CryptoConfig::default();
Self::with_config(config).await
}
pub async fn with_config(config: CryptoConfig) -> CryptoResult<Self> {
let project_root = crate::utils::find_project_root()?;
let engine = Arc::new(CryptoEngine::with_performance_profile(config.performance_profile));
let secret_store = Arc::new(InMemorySecretStore::new());
Ok(Self {
engine,
config: Arc::new(RwLock::new(config)),
project_root,
secret_store,
})
}
pub async fn init_project() -> CryptoResult<()> {
let project_root = crate::utils::find_project_root()?;
let config_dir = project_root.join(".cargocrypt");
if !config_dir.exists() {
tokio::fs::create_dir_all(&config_dir).await?;
}
let config_path = config_dir.join("config.toml");
if !config_path.exists() {
let default_config = CryptoConfig::default();
let config_toml = toml::to_string_pretty(&default_config)
.map_err(|e| CargoCryptError::Serialization {
message: "Failed to serialize default configuration".to_string(),
source: Box::new(e),
})?;
tokio::fs::write(&config_path, config_toml).await?;
}
let gitignore_path = project_root.join(".gitignore");
let gitignore_entry = "\n# CargoCrypt secrets\n.cargocrypt/secrets/\n*.enc\n";
if gitignore_path.exists() {
let existing_content = tokio::fs::read_to_string(&gitignore_path).await?;
if !existing_content.contains(".cargocrypt/secrets/") {
tokio::fs::write(&gitignore_path, existing_content + gitignore_entry).await?;
}
} else {
tokio::fs::write(&gitignore_path, gitignore_entry).await?;
}
Ok(())
}
pub async fn encrypt_file<P: AsRef<Path>>(&self, path: P, password: &str) -> CryptoResult<PathBuf> {
let path = path.as_ref();
let config = self.config.read().await;
let output_path = path.with_extension(
format!("{}.{}",
path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or(""),
config.file_ops.encrypted_extension
)
);
let input_data = tokio::fs::read(path).await?;
let encrypted = self.engine.encrypt_data(&input_data, password)?;
let encrypted_bytes = bincode::serialize(&encrypted)
.map_err(|e| CargoCryptError::Serialization {
message: format!("Failed to serialize encrypted data: {}", e),
source: Box::new(e),
})?;
tokio::fs::write(&output_path, encrypted_bytes).await?;
if config.file_ops.backup_originals {
let backup_path = path.with_extension(
format!("{}.backup",
path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
)
);
tokio::fs::copy(path, backup_path).await?;
}
Ok(output_path)
}
pub async fn decrypt_file<P: AsRef<Path>>(&self, path: P, password: &str) -> CryptoResult<PathBuf> {
let path = path.as_ref();
let config = self.config.read().await;
let output_path = if path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext == config.file_ops.encrypted_extension)
.unwrap_or(false)
{
path.with_extension("")
} else {
return Err(CargoCryptError::Config {
message: format!("File '{}' doesn't appear to be encrypted", path.display()),
suggestion: Some("Encrypted files should have the .enc extension".to_string()),
});
};
let encrypted_data = tokio::fs::read(path).await?;
let encrypted: EncryptedSecret = bincode::deserialize(&encrypted_data)
.map_err(|e| CargoCryptError::Serialization {
message: format!("Failed to deserialize encrypted data: {}", e),
source: Box::new(e),
})?;
let decrypted_data = self.engine.decrypt_data(&encrypted, password)?;
tokio::fs::write(&output_path, decrypted_data).await?;
Ok(output_path)
}
pub async fn config(&self) -> CryptoConfig {
self.config.read().await.clone()
}
pub async fn update_config<F>(&self, updater: F) -> CryptoResult<()>
where
F: FnOnce(&mut CryptoConfig),
{
let mut config = self.config.write().await;
updater(&mut *config);
Ok(())
}
pub fn project_root(&self) -> &Path {
&self.project_root
}
pub fn secret_store(&self) -> &dyn SecretStore {
self.secret_store.as_ref()
}
pub fn engine(&self) -> &CryptoEngine {
&self.engine
}
pub fn crypto_engine(&self) -> &CryptoEngine {
&self.engine
}
}
pub struct CargoCryptBuilder {
config: CryptoConfig,
project_root: Option<PathBuf>,
secret_store: Option<Arc<dyn SecretStore>>,
}
impl CargoCryptBuilder {
pub fn new() -> Self {
Self {
config: CryptoConfig::default(),
project_root: None,
secret_store: None,
}
}
pub fn performance_profile(mut self, profile: PerformanceProfile) -> Self {
self.config.performance_profile = profile;
self
}
pub fn project_root<P: Into<PathBuf>>(mut self, root: P) -> Self {
self.project_root = Some(root.into());
self
}
pub fn secret_store(mut self, store: Arc<dyn SecretStore>) -> Self {
self.secret_store = Some(store);
self
}
pub fn key_params(mut self, params: KeyDerivationConfig) -> Self {
self.config.key_params = params;
self
}
pub fn file_ops(mut self, ops: FileOperationConfig) -> Self {
self.config.file_ops = ops;
self
}
pub fn security(mut self, security: SecurityConfig) -> Self {
self.config.security = security;
self
}
pub fn performance(mut self, performance: PerformanceConfig) -> Self {
self.config.performance = performance;
self
}
pub async fn build(self) -> CryptoResult<CargoCrypt> {
let project_root = self.project_root
.map(Ok)
.unwrap_or_else(crate::utils::find_project_root)?;
let engine = Arc::new(CryptoEngine::with_performance_profile(self.config.performance_profile));
let secret_store = self.secret_store
.unwrap_or_else(|| Arc::new(InMemorySecretStore::new()));
Ok(CargoCrypt {
engine,
config: Arc::new(RwLock::new(self.config)),
project_root,
secret_store,
})
}
}
impl Default for CargoCryptBuilder {
fn default() -> Self {
Self::new()
}
}
impl CryptoConfig {
pub fn performance_profiles(&self) -> Vec<PerformanceProfile> {
vec![
PerformanceProfile::Fast,
PerformanceProfile::Balanced,
PerformanceProfile::Secure,
PerformanceProfile::Paranoid,
]
}
pub fn validate(&self) -> CryptoResult<()> {
if self.key_params.memory_cost < 1024 {
return Err(CargoCryptError::Config {
message: "Memory cost too low (minimum 1024 KiB)".to_string(),
suggestion: Some("Increase memory_cost to at least 1024 for security".to_string()),
});
}
if self.key_params.time_cost < 1 {
return Err(CargoCryptError::Config {
message: "Time cost too low (minimum 1)".to_string(),
suggestion: Some("Increase time_cost to at least 1".to_string()),
});
}
if self.key_params.parallelism < 1 {
return Err(CargoCryptError::Config {
message: "Parallelism too low (minimum 1)".to_string(),
suggestion: Some("Increase parallelism to at least 1".to_string()),
});
}
if self.file_ops.buffer_size < 1024 {
return Err(CargoCryptError::Config {
message: "Buffer size too small (minimum 1024 bytes)".to_string(),
suggestion: Some("Increase buffer_size to at least 1024".to_string()),
});
}
if self.security.max_password_attempts < 1 {
return Err(CargoCryptError::Config {
message: "Max password attempts too low (minimum 1)".to_string(),
suggestion: Some("Increase max_password_attempts to at least 1".to_string()),
});
}
if self.performance.max_concurrent_ops < 1 {
return Err(CargoCryptError::Config {
message: "Max concurrent operations too low (minimum 1)".to_string(),
suggestion: Some("Increase max_concurrent_ops to at least 1".to_string()),
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = CryptoConfig::default();
assert_eq!(config.performance_profile, PerformanceProfile::Balanced);
assert_eq!(config.key_params.memory_cost, 65536);
assert_eq!(config.key_params.time_cost, 3);
assert_eq!(config.key_params.parallelism, 4);
assert_eq!(config.file_ops.encrypted_extension, "enc");
assert!(config.security.fail_secure);
assert!(config.performance.async_operations);
}
#[test]
fn test_config_validation() {
let mut config = CryptoConfig::default();
assert!(config.validate().is_ok());
config.key_params.memory_cost = 512; assert!(config.validate().is_err());
config.key_params.memory_cost = 65536; config.security.max_password_attempts = 0; assert!(config.validate().is_err());
}
#[tokio::test]
async fn test_secret_store() {
let store = InMemorySecretStore::new();
let secret = SecretBytes::from_str("test-secret");
store.store_secret("test-key", secret.clone()).unwrap();
let retrieved = store.get_secret("test-key").unwrap().unwrap();
assert_eq!(retrieved.expose_secret(), secret.expose_secret());
assert!(store.contains_secret("test-key"));
assert!(!store.contains_secret("non-existent"));
assert!(store.remove_secret("test-key").unwrap());
assert!(!store.contains_secret("test-key"));
}
#[test]
fn test_secret_bytes_zeroization() {
let mut secret = SecretBytes::from_str("sensitive-data");
assert!(!secret.is_empty());
assert_eq!(secret.len(), 14);
drop(secret);
}
#[test]
fn test_builder_pattern() {
let builder = CargoCryptBuilder::new()
.performance_profile(PerformanceProfile::Secure)
.project_root("/tmp/test");
assert_eq!(builder.config.performance_profile, PerformanceProfile::Secure);
}
}