use crate::core::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct CredentialStore {
credentials: HashMap<String, EncryptedCredential>,
encryption_key: Option<Vec<u8>>,
config: CredentialStoreConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredentialStoreConfig {
pub encrypt_at_rest: bool,
pub encryption_algorithm: String,
pub key_derivation: String,
pub salt: Vec<u8>,
pub iterations: u32,
pub file_path: Option<String>,
pub auto_save: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedCredential {
pub data: Vec<u8>,
pub iv: Vec<u8>,
pub tag: Vec<u8>,
pub metadata: CredentialMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredentialMetadata {
pub credential_type: String,
pub created_at: String,
pub last_accessed: Option<String>,
pub expires_at: Option<String>,
pub tags: Vec<String>,
pub active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CredentialType {
Database {
username: String,
password: String,
host: String,
port: u16,
database: String,
},
Cloud {
provider: String,
access_key: String,
secret_key: String,
session_token: Option<String>,
region: Option<String>,
},
ApiKey {
key: String,
secret: Option<String>,
endpoint: Option<String>,
},
SshKey {
private_key: String,
public_key: String,
passphrase: Option<String>,
},
Generic { fields: HashMap<String, String> },
}
impl Default for CredentialStoreConfig {
fn default() -> Self {
Self {
encrypt_at_rest: true,
encryption_algorithm: "AES-256-GCM".to_string(),
key_derivation: "PBKDF2".to_string(),
salt: generate_random_bytes(32),
iterations: 100_000,
file_path: None,
auto_save: false,
}
}
}
impl CredentialStore {
pub fn new(config: CredentialStoreConfig) -> Self {
Self {
credentials: HashMap::new(),
encryption_key: None,
config,
}
}
pub fn with_defaults() -> Self {
Self::new(CredentialStoreConfig::default())
}
pub fn init_encryption(&mut self, password: &str) -> Result<()> {
let key = derive_key(
password.as_bytes(),
&self.config.salt,
self.config.iterations,
)?;
self.encryption_key = Some(key);
Ok(())
}
pub fn init_encryption_from_env(&mut self, env_var: &str) -> Result<()> {
let password = env::var(env_var).map_err(|_| {
Error::ConfigurationError(format!("Environment variable {} not found", env_var))
})?;
self.init_encryption(&password)
}
pub fn store_credential(&mut self, name: &str, credential: CredentialType) -> Result<()> {
let encrypted = self.encrypt_credential(&credential)?;
self.credentials.insert(name.to_string(), encrypted);
if self.config.auto_save {
self.save_to_file()?;
}
Ok(())
}
pub fn get_credential(&mut self, name: &str) -> Result<CredentialType> {
let encrypted = self
.credentials
.get(name)
.ok_or_else(|| Error::ConfigurationError(format!("Credential '{}' not found", name)))?;
let credential = self.decrypt_credential(encrypted)?;
if let Some(encrypted_mut) = self.credentials.get_mut(name) {
encrypted_mut.metadata.last_accessed = Some(current_timestamp());
}
Ok(credential)
}
pub fn list_credentials(&self) -> Vec<String> {
self.credentials.keys().cloned().collect()
}
pub fn has_credential(&self, name: &str) -> bool {
self.credentials.contains_key(name)
}
pub fn remove_credential(&mut self, name: &str) -> Result<()> {
self.credentials
.remove(name)
.ok_or_else(|| Error::ConfigurationError(format!("Credential '{}' not found", name)))?;
if self.config.auto_save {
self.save_to_file()?;
}
Ok(())
}
pub fn get_credential_metadata(&self, name: &str) -> Result<&CredentialMetadata> {
let encrypted = self
.credentials
.get(name)
.ok_or_else(|| Error::ConfigurationError(format!("Credential '{}' not found", name)))?;
Ok(&encrypted.metadata)
}
pub fn update_credential_metadata(
&mut self,
name: &str,
metadata: CredentialMetadata,
) -> Result<()> {
let encrypted = self
.credentials
.get_mut(name)
.ok_or_else(|| Error::ConfigurationError(format!("Credential '{}' not found", name)))?;
encrypted.metadata = metadata;
if self.config.auto_save {
self.save_to_file()?;
}
Ok(())
}
pub fn rotate_encryption_key(&mut self, new_password: &str) -> Result<()> {
let mut decrypted_credentials = HashMap::new();
for (name, encrypted) in &self.credentials {
let credential = self.decrypt_credential(encrypted)?;
decrypted_credentials.insert(name.clone(), credential);
}
self.config.salt = generate_random_bytes(32);
self.init_encryption(new_password)?;
self.credentials.clear();
for (name, credential) in decrypted_credentials {
self.store_credential(&name, credential)?;
}
if self.config.auto_save {
self.save_to_file()?;
}
Ok(())
}
pub fn save_to_file(&self) -> Result<()> {
if let Some(file_path) = &self.config.file_path {
let data = CredentialFileData {
config: self.config.clone(),
credentials: self.credentials.clone(),
};
let json = serde_json::to_string_pretty(&data).map_err(|e| {
Error::ConfigurationError(format!("Failed to serialize credentials: {}", e))
})?;
if let Some(parent) = Path::new(file_path).parent() {
if !parent.exists() {
fs::create_dir_all(parent).map_err(|e| {
Error::ConfigurationError(format!(
"Failed to create credential directory: {}",
e
))
})?;
}
}
fs::write(file_path, json).map_err(|e| {
Error::ConfigurationError(format!("Failed to write credential file: {}", e))
})?;
}
Ok(())
}
pub fn load_from_file(file_path: &str) -> Result<Self> {
if !Path::new(file_path).exists() {
return Err(Error::ConfigurationError(format!(
"Credential file not found: {}",
file_path
)));
}
let json = fs::read_to_string(file_path).map_err(|e| {
Error::ConfigurationError(format!("Failed to read credential file: {}", e))
})?;
let data: CredentialFileData = serde_json::from_str(&json).map_err(|e| {
Error::ConfigurationError(format!("Failed to parse credential file: {}", e))
})?;
Ok(Self {
credentials: data.credentials,
encryption_key: None,
config: data.config,
})
}
pub fn export_credentials(&self, password: &str) -> Result<String> {
let mut export_config = self.config.clone();
export_config.salt = generate_random_bytes(32);
let mut export_store = Self::new(export_config);
export_store.init_encryption(password)?;
for (name, encrypted) in &self.credentials {
let credential = self.decrypt_credential(encrypted)?;
export_store.store_credential(name, credential)?;
}
let export_data = CredentialFileData {
config: export_store.config,
credentials: export_store.credentials,
};
serde_json::to_string_pretty(&export_data)
.map_err(|e| Error::ConfigurationError(format!("Failed to export credentials: {}", e)))
}
pub fn import_credentials(&mut self, export_data: &str, password: &str) -> Result<()> {
let data: CredentialFileData = serde_json::from_str(export_data).map_err(|e| {
Error::ConfigurationError(format!("Failed to parse import data: {}", e))
})?;
let mut import_store = Self {
credentials: data.credentials,
encryption_key: None,
config: data.config,
};
import_store.init_encryption(password)?;
for name in import_store.list_credentials() {
let credential = import_store.get_credential(&name)?;
self.store_credential(&name, credential)?;
}
Ok(())
}
fn encrypt_credential(&self, credential: &CredentialType) -> Result<EncryptedCredential> {
if !self.config.encrypt_at_rest {
let data = serde_json::to_vec(credential).map_err(|e| {
Error::ConfigurationError(format!("Failed to serialize credential: {}", e))
})?;
return Ok(EncryptedCredential {
data,
iv: Vec::new(),
tag: Vec::new(),
metadata: CredentialMetadata {
credential_type: get_credential_type_name(credential),
created_at: current_timestamp(),
last_accessed: None,
expires_at: None,
tags: Vec::new(),
active: true,
},
});
}
let key = self.encryption_key.as_ref().ok_or_else(|| {
Error::ConfigurationError("Encryption key not initialized".to_string())
})?;
let plaintext = serde_json::to_vec(credential).map_err(|e| {
Error::ConfigurationError(format!("Failed to serialize credential: {}", e))
})?;
let (ciphertext, iv, tag) = encrypt_data(&plaintext, key)?;
Ok(EncryptedCredential {
data: ciphertext,
iv,
tag,
metadata: CredentialMetadata {
credential_type: get_credential_type_name(credential),
created_at: current_timestamp(),
last_accessed: None,
expires_at: None,
tags: Vec::new(),
active: true,
},
})
}
fn decrypt_credential(&self, encrypted: &EncryptedCredential) -> Result<CredentialType> {
if !self.config.encrypt_at_rest {
let credential: CredentialType =
serde_json::from_slice(&encrypted.data).map_err(|e| {
Error::ConfigurationError(format!("Failed to deserialize credential: {}", e))
})?;
return Ok(credential);
}
let key = self.encryption_key.as_ref().ok_or_else(|| {
Error::ConfigurationError("Encryption key not initialized".to_string())
})?;
let plaintext = decrypt_data(&encrypted.data, &encrypted.iv, &encrypted.tag, key)?;
let credential: CredentialType = serde_json::from_slice(&plaintext).map_err(|e| {
Error::ConfigurationError(format!("Failed to deserialize credential: {}", e))
})?;
Ok(credential)
}
}
#[derive(Debug, Serialize, Deserialize)]
struct CredentialFileData {
config: CredentialStoreConfig,
credentials: HashMap<String, EncryptedCredential>,
}
fn generate_random_bytes(len: usize) -> Vec<u8> {
use rand::Rng;
let mut bytes = vec![0u8; len];
rand::rng().fill_bytes(&mut bytes);
bytes
}
fn derive_key(password: &[u8], salt: &[u8], iterations: u32) -> Result<Vec<u8>> {
use pbkdf2::pbkdf2_hmac;
use sha2::Sha256;
let mut key = [0u8; 32]; pbkdf2_hmac::<Sha256>(password, salt, iterations, &mut key);
Ok(key.to_vec())
}
fn encrypt_data(plaintext: &[u8], key: &[u8]) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
use aes_gcm::{AeadInPlace, Aes256Gcm, Key, KeyInit, Nonce};
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
let nonce_bytes = generate_random_bytes(12); let nonce = Nonce::from_slice(&nonce_bytes);
let mut buffer = plaintext.to_vec();
let tag = cipher
.encrypt_in_place_detached(nonce, b"", &mut buffer)
.map_err(|e| Error::ConfigurationError(format!("Encryption failed: {}", e)))?;
Ok((buffer, nonce_bytes, tag.to_vec()))
}
fn decrypt_data(ciphertext: &[u8], iv: &[u8], tag: &[u8], key: &[u8]) -> Result<Vec<u8>> {
use aes_gcm::{AeadInPlace, Aes256Gcm, Key, KeyInit, Nonce, Tag};
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
let nonce = Nonce::from_slice(iv);
let tag = Tag::from_slice(tag);
let mut buffer = ciphertext.to_vec();
cipher
.decrypt_in_place_detached(nonce, b"", &mut buffer, tag)
.map_err(|e| Error::ConfigurationError(format!("Decryption failed: {}", e)))?;
Ok(buffer)
}
fn get_credential_type_name(credential: &CredentialType) -> String {
match credential {
CredentialType::Database { .. } => "database".to_string(),
CredentialType::Cloud { .. } => "cloud".to_string(),
CredentialType::ApiKey { .. } => "api_key".to_string(),
CredentialType::SshKey { .. } => "ssh_key".to_string(),
CredentialType::Generic { .. } => "generic".to_string(),
}
}
fn current_timestamp() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("operation should succeed")
.as_secs();
format!("{}", timestamp)
}
pub struct CredentialBuilder {
credential_type: Option<CredentialType>,
metadata: CredentialMetadata,
}
impl CredentialBuilder {
pub fn new() -> Self {
Self {
credential_type: None,
metadata: CredentialMetadata {
credential_type: String::new(),
created_at: current_timestamp(),
last_accessed: None,
expires_at: None,
tags: Vec::new(),
active: true,
},
}
}
pub fn database(
mut self,
username: &str,
password: &str,
host: &str,
port: u16,
database: &str,
) -> Self {
self.credential_type = Some(CredentialType::Database {
username: username.to_string(),
password: password.to_string(),
host: host.to_string(),
port,
database: database.to_string(),
});
self.metadata.credential_type = "database".to_string();
self
}
pub fn cloud_aws(mut self, access_key: &str, secret_key: &str, region: Option<&str>) -> Self {
self.credential_type = Some(CredentialType::Cloud {
provider: "aws".to_string(),
access_key: access_key.to_string(),
secret_key: secret_key.to_string(),
session_token: None,
region: region.map(|s| s.to_string()),
});
self.metadata.credential_type = "cloud".to_string();
self
}
pub fn api_key(mut self, key: &str, secret: Option<&str>, endpoint: Option<&str>) -> Self {
self.credential_type = Some(CredentialType::ApiKey {
key: key.to_string(),
secret: secret.map(|s| s.to_string()),
endpoint: endpoint.map(|s| s.to_string()),
});
self.metadata.credential_type = "api_key".to_string();
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.metadata.tags = tags;
self
}
pub fn with_expiry(mut self, expires_at: &str) -> Self {
self.metadata.expires_at = Some(expires_at.to_string());
self
}
pub fn build(self) -> Result<CredentialType> {
self.credential_type
.ok_or_else(|| Error::ConfigurationError("Credential type not specified".to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_credential_store_basic_operations() {
let mut store = CredentialStore::with_defaults();
store
.init_encryption("test_password")
.expect("operation should succeed");
let credential = CredentialBuilder::new()
.database("user", "pass", "localhost", 5432, "mydb")
.build()
.expect("operation should succeed");
store
.store_credential("db1", credential)
.expect("operation should succeed");
assert!(store.has_credential("db1"));
assert!(!store.has_credential("db2"));
let retrieved = store
.get_credential("db1")
.expect("operation should succeed");
match retrieved {
CredentialType::Database {
username,
password,
host,
port,
database,
} => {
assert_eq!(username, "user");
assert_eq!(password, "pass");
assert_eq!(host, "localhost");
assert_eq!(port, 5432);
assert_eq!(database, "mydb");
}
_ => panic!("Wrong credential type retrieved"),
}
store
.remove_credential("db1")
.expect("operation should succeed");
assert!(!store.has_credential("db1"));
}
#[test]
fn test_credential_builder() {
let db_cred = CredentialBuilder::new()
.database("admin", "secret123", "db.example.com", 5432, "production")
.with_tags(vec!["production".to_string(), "primary".to_string()])
.build()
.expect("operation should succeed");
match db_cred {
CredentialType::Database {
username,
password,
host,
port,
database,
} => {
assert_eq!(username, "admin");
assert_eq!(password, "secret123");
assert_eq!(host, "db.example.com");
assert_eq!(port, 5432);
assert_eq!(database, "production");
}
_ => panic!("Wrong credential type"),
}
let aws_cred = CredentialBuilder::new()
.cloud_aws("AKIATEST", "secret", Some("us-west-2"))
.build()
.expect("operation should succeed");
match aws_cred {
CredentialType::Cloud {
provider,
access_key,
secret_key,
region,
..
} => {
assert_eq!(provider, "aws");
assert_eq!(access_key, "AKIATEST");
assert_eq!(secret_key, "secret");
assert_eq!(region, Some("us-west-2".to_string()));
}
_ => panic!("Wrong credential type"),
}
}
}