use crate::error::ConfigError;
use crate::key::{now_timestamp, KeyManager};
use crate::secret::{SecretBytes, XChaCha20Crypto};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::path::PathBuf;
use std::sync::LazyLock;
static HEX_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[0-9a-fA-F]{8,64}").unwrap());
static KEY_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)key\s*[:=]\s*[0-9a-fA-F]+").unwrap());
static MASTER_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)master\s*[:=]\s*[0-9a-fA-F]+").unwrap());
static SECRET_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)secret\s*[:=]\s*[0-9a-fA-F]+").unwrap());
static LONG_HEX_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"[0-9a-fA-F]{64}").unwrap());
static BASE64_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"[A-Za-z0-9+/]{32,}={0,2}").unwrap());
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SanitizationLevel {
Minimal,
Standard,
Aggressive,
}
pub struct ErrorSanitizer {
master_key_hex: String,
level: SanitizationLevel,
replacement: String,
}
impl ErrorSanitizer {
pub fn new(master_key: &[u8; 32], level: SanitizationLevel) -> Self {
Self {
master_key_hex: hex::encode(master_key),
level,
replacement: "***".to_string(),
}
}
pub fn with_replacement(mut self, replacement: String) -> Self {
self.replacement = replacement;
self
}
pub fn sanitize(&self, error_message: &str) -> String {
let mut sanitized = error_message.to_string();
match self.level {
SanitizationLevel::Minimal => {
sanitized = self.sanitize_full_key(&sanitized);
}
SanitizationLevel::Standard => {
sanitized = self.sanitize_full_key(&sanitized);
sanitized = self.sanitize_key_fragments(&sanitized);
sanitized = self.sanitize_key_patterns(&sanitized);
}
SanitizationLevel::Aggressive => {
sanitized = self.sanitize_full_key(&sanitized);
sanitized = self.sanitize_key_fragments(&sanitized);
sanitized = self.sanitize_key_patterns(&sanitized);
sanitized = self.sanitize_suspicious_patterns(&sanitized);
}
}
sanitized
}
fn sanitize_full_key(&self, message: &str) -> String {
message.replace(&self.master_key_hex, &self.replacement)
}
fn sanitize_key_fragments(&self, message: &str) -> String {
HEX_PATTERN
.replace_all(message, &self.replacement)
.to_string()
}
fn sanitize_key_patterns(&self, message: &str) -> String {
let mut result = message.to_string();
result = KEY_PATTERN.replace_all(&result, "key: ***").to_string();
result = MASTER_PATTERN
.replace_all(&result, "master: ***")
.to_string();
result = SECRET_PATTERN
.replace_all(&result, "secret: ***")
.to_string();
result
}
fn sanitize_suspicious_patterns(&self, message: &str) -> String {
let mut result = message.to_string();
result = LONG_HEX_PATTERN
.replace_all(&result, &self.replacement)
.to_string();
result = BASE64_PATTERN
.replace_all(&result, &self.replacement)
.to_string();
result
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedKeyStore {
pub version: u32,
pub encrypted_data: String,
pub checksum: String,
pub created_at: u64,
pub metadata: KeyStoreMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyStoreMetadata {
pub key_id: String,
pub key_count: u32,
pub last_modified: u64,
pub schema_version: u32,
}
pub struct KeyStorage {
storage_path: PathBuf,
master_key: Option<SecretBytes>,
key_manager: KeyManager,
error_sanitizer: Option<ErrorSanitizer>,
}
impl KeyStorage {
pub fn new(storage_path: PathBuf) -> Result<Self, ConfigError> {
fs::create_dir_all(&storage_path).map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to create key storage directory: {}", e),
location: None,
source: None,
})?;
let key_manager = KeyManager::new(storage_path.join("keys.json"))?;
Ok(Self {
storage_path,
master_key: None,
key_manager,
error_sanitizer: None,
})
}
pub fn set_master_key(&mut self, master_key: &[u8; 32]) {
self.master_key = Some(SecretBytes::new(master_key.to_vec()));
self.error_sanitizer = Some(ErrorSanitizer::new(master_key, SanitizationLevel::Standard));
}
pub fn clear_master_key(&mut self) {
self.master_key = None;
self.error_sanitizer = None;
}
fn get_master_key_bytes(&self) -> Result<[u8; 32], ConfigError> {
let secret = self
.master_key
.as_ref()
.ok_or_else(|| ConfigError::ParseError {
format: "key".to_string(),
message: "Master key not set".to_string(),
location: None,
source: None,
})?;
let slice = secret.as_slice();
if slice.len() != 32 {
return Err(ConfigError::ParseError {
format: "key".to_string(),
message: "Invalid master key length".to_string(),
location: None,
source: None,
});
}
let mut arr = [0u8; 32];
arr.copy_from_slice(slice);
Ok(arr)
}
pub fn initialize_with_master_key(
&mut self,
master_key: &[u8; 32],
key_id: String,
created_by: String,
) -> Result<(), ConfigError> {
self.master_key = Some(SecretBytes::new(master_key.to_vec()));
let key_id_for_error = key_id.clone();
self.key_manager
.initialize(master_key, key_id, created_by)
.map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!(
"Failed to initialize key ring for '{}': {}",
key_id_for_error,
self.sanitize_error(&e.to_string())
),
location: None,
source: None,
})?;
self.save()?;
Ok(())
}
pub fn save(&self) -> Result<(), ConfigError> {
let master_key = self.get_master_key_bytes()?;
let key_data = self.serialize_key_manager()?;
let encrypted_data =
self.encrypt_data(&key_data, &master_key)
.map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!(
"Failed to encrypt key data: {}",
self.sanitize_error(&e.to_string())
),
location: None,
source: None,
})?;
let checksum = KeyStorage::calculate_checksum(&encrypted_data);
let store = EncryptedKeyStore {
version: 1,
encrypted_data,
checksum,
created_at: now_timestamp(),
metadata: KeyStoreMetadata {
key_id: self.key_manager.get_default_key_id().to_string(),
key_count: self.key_manager.list_keys().len() as u32,
last_modified: now_timestamp(),
schema_version: 1,
},
};
self.write_store(&store)?;
Ok(())
}
pub fn load(&mut self) -> Result<(), ConfigError> {
let master_key = self.get_master_key_bytes()?;
if !self.storage_path.join("keys.json").exists() {
return Ok(());
}
let store = self.read_store()?;
self.validate_checksum(&store)?;
let key_data = self
.decrypt_data(&store.encrypted_data, &master_key)
.map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!(
"Failed to decrypt key data: {}",
self.sanitize_error(&e.to_string())
),
location: None,
source: None,
})?;
self.deserialize_key_manager(&key_data)?;
Ok(())
}
fn serialize_key_manager(&self) -> Result<String, ConfigError> {
let data = serde_json::to_vec(&self.key_manager).map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to serialize key manager: {}", e),
location: None,
source: None,
})?;
Ok(BASE64.encode(data))
}
fn deserialize_key_manager(&mut self, data: &str) -> Result<(), ConfigError> {
let bytes = BASE64.decode(data).map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Invalid key data: {}", e),
location: None,
source: None,
})?;
let key_manager: KeyManager =
serde_json::from_slice(&bytes).map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to deserialize key manager: {}", e),
location: None,
source: None,
})?;
self.key_manager = key_manager;
Ok(())
}
fn encrypt_data(&self, data: &str, master_key: &[u8; 32]) -> Result<String, ConfigError> {
let encryptor = XChaCha20Crypto::new();
let (nonce, ciphertext) = encryptor
.encrypt(data.as_bytes(), master_key)
.map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Encryption failed: {}", self.sanitize_error(&e.to_string())),
location: None,
source: None,
})?;
let result = format!("{}:{}", BASE64.encode(&nonce), BASE64.encode(&ciphertext));
Ok(result)
}
fn decrypt_data(&self, encrypted: &str, master_key: &[u8; 32]) -> Result<String, ConfigError> {
let parts: Vec<&str> = encrypted.split(':').collect();
if parts.len() != 2 {
return Err(ConfigError::ParseError {
format: "key".to_string(),
message: "Invalid encrypted data format".to_string(),
location: None,
source: None,
});
}
let nonce = BASE64
.decode(parts[0])
.map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to decode nonce: {}", e),
location: None,
source: None,
})?;
let ciphertext = BASE64
.decode(parts[1])
.map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to decode ciphertext: {}", e),
location: None,
source: None,
})?;
let encryptor = XChaCha20Crypto::new();
let plaintext = encryptor
.decrypt(&nonce, &ciphertext, master_key)
.map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Decryption failed: {}", self.sanitize_error(&e.to_string())),
location: None,
source: None,
})?;
String::from_utf8(plaintext).map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to convert decrypted data to string: {}", e),
location: None,
source: None,
})
}
fn calculate_checksum(data: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(data.as_bytes());
let hash = hasher.finalize();
BASE64.encode(hash)
}
fn validate_checksum(&self, store: &EncryptedKeyStore) -> Result<(), ConfigError> {
let calculated = Self::calculate_checksum(&store.encrypted_data);
if store.checksum != calculated {
return Err(ConfigError::ParseError {
format: "key".to_string(),
message: "Key store checksum mismatch".to_string(),
location: None,
source: None,
});
}
Ok(())
}
fn write_store(&self, store: &EncryptedKeyStore) -> Result<(), ConfigError> {
let store_path = self.storage_path.join("keys.json");
let tmp_path = self.storage_path.join("keys.json.tmp");
let json = serde_json::to_string_pretty(store).map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to serialize key store: {}", e),
location: None,
source: None,
})?;
{
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&tmp_path)
.map_err(|e| {
std::io::Error::new(e.kind(), format!("Failed to open temp key store: {}", e))
})?;
file.write_all(json.as_bytes()).map_err(|e| {
std::io::Error::new(e.kind(), format!("Failed to write key store: {}", e))
})?;
file.sync_all().map_err(|e| {
std::io::Error::new(e.kind(), format!("Failed to fsync key store: {}", e))
})?;
}
std::fs::rename(&tmp_path, &store_path).map_err(|e| {
std::io::Error::new(e.kind(), format!("Failed to rename key store: {}", e))
})?;
Ok(())
}
fn read_store(&self) -> Result<EncryptedKeyStore, ConfigError> {
let store_path = self.storage_path.join("keys.json");
let mut file = File::open(&store_path).map_err(|e| {
std::io::Error::new(e.kind(), format!("Failed to open key store: {}", e))
})?;
let mut contents = String::new();
file.read_to_string(&mut contents).map_err(|e| {
std::io::Error::new(e.kind(), format!("Failed to read key store: {}", e))
})?;
serde_json::from_str(&contents).map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to parse key store: {}", e),
location: None,
source: None,
})
}
pub fn export_keys(&self, output_path: &PathBuf) -> Result<(), ConfigError> {
let master_key = self.get_master_key_bytes()?;
let key_data = self.serialize_key_manager()?;
let encrypted_data = self.encrypt_data(&key_data, &master_key)?;
let export = KeyExport {
version: 1,
exported_at: now_timestamp(),
encrypted_data,
};
let json = serde_json::to_string_pretty(&export).map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to serialize export: {}", e),
location: None,
source: None,
})?;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(output_path)
.map_err(|e| std::io::Error::other(format!("Failed to create export file: {}", e)))?;
file.write_all(json.as_bytes())
.map_err(|e| std::io::Error::other(format!("Failed to write export file: {}", e)))?;
Ok(())
}
pub fn import_keys(
&mut self,
input_path: &PathBuf,
master_key: &[u8; 32],
) -> Result<(), ConfigError> {
self.master_key = Some(SecretBytes::new(master_key.to_vec()));
let mut file = File::open(input_path)
.map_err(|e| std::io::Error::other(format!("Failed to open import file: {}", e)))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| std::io::Error::other(format!("Failed to read import file: {}", e)))?;
let export: KeyExport =
serde_json::from_str(&contents).map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to parse import file: {}", e),
location: None,
source: None,
})?;
self.validate_checksum_by_data(&export.encrypted_data)?;
let key_data = self.decrypt_data(&export.encrypted_data, master_key)?;
self.deserialize_key_manager(&key_data)?;
self.save()?;
Ok(())
}
fn validate_checksum_by_data(&self, encrypted_data: &str) -> Result<(), ConfigError> {
let checksum = Self::calculate_checksum(encrypted_data);
let store = self.read_store()?;
if store.checksum != checksum {
return Err(ConfigError::ParseError {
format: "key".to_string(),
message: "Import checksum mismatch".to_string(),
location: None,
source: None,
});
}
Ok(())
}
pub fn get_key_manager(&self) -> &KeyManager {
&self.key_manager
}
pub fn get_key_manager_mut(&mut self) -> &mut KeyManager {
&mut self.key_manager
}
pub fn sanitize_error(&self, error_message: &str) -> String {
match &self.error_sanitizer {
Some(sanitizer) => sanitizer.sanitize(error_message),
None => {
error_message.to_string()
}
}
}
pub fn backup(&self, backup_path: &PathBuf) -> Result<(), ConfigError> {
let timestamp = now_timestamp();
let backup_file = backup_path.join(format!("keys_backup_{}.json", timestamp));
let master_key = self.get_master_key_bytes()?;
let key_data = self.serialize_key_manager()?;
let encrypted_data = self.encrypt_data(&key_data, &master_key)?;
let backup = KeyExport {
version: 1,
exported_at: now_timestamp(),
encrypted_data,
};
let json = serde_json::to_string_pretty(&backup).map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to serialize backup: {}", e),
location: None,
source: None,
})?;
fs::create_dir_all(backup_path).map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!("Failed to create backup directory: {}", e),
location: None,
source: None,
})?;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&backup_file)
.map_err(|e| std::io::Error::other(format!("Failed to create backup file: {}", e)))?;
file.write_all(json.as_bytes())
.map_err(|e| std::io::Error::other(format!("Failed to write backup file: {}", e)))?;
Ok(())
}
pub fn list_backups(&self, backup_path: &PathBuf) -> Result<Vec<BackupInfo>, ConfigError> {
let mut backups = Vec::new();
if let Ok(entries) = fs::read_dir(backup_path) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.starts_with("keys_backup_") && file_name.ends_with(".json") {
let timestamp_str = file_name
.strip_prefix("keys_backup_")
.and_then(|s| s.strip_suffix(".json"))
.and_then(|s| s.parse::<u64>().ok());
if let Some(timestamp) = timestamp_str {
let backup_path = path.clone();
backups.push(BackupInfo {
path: backup_path,
timestamp,
file_name: file_name.to_string(),
});
}
}
}
}
}
backups.sort_by_key(|b| std::cmp::Reverse(b.timestamp));
Ok(backups)
}
pub fn rotate_master_key(
&mut self,
_old_master_key: &[u8; 32],
new_master_key: &[u8; 32],
) -> Result<(), ConfigError> {
let plaintext = self.serialize_key_manager()?;
let reencrypted_data =
self.encrypt_data(&plaintext, new_master_key)
.map_err(|e| ConfigError::ParseError {
format: "key".to_string(),
message: format!(
"Failed to encrypt with new master key: {}",
self.sanitize_error(&e.to_string())
),
location: None,
source: None,
})?;
self.master_key = Some(SecretBytes::new(new_master_key.to_vec()));
let checksum = Self::calculate_checksum(&reencrypted_data);
let store = EncryptedKeyStore {
version: 1,
encrypted_data: reencrypted_data,
checksum,
created_at: now_timestamp(),
metadata: KeyStoreMetadata {
key_id: self.key_manager.get_default_key_id().to_string(),
key_count: self.key_manager.list_keys().len() as u32,
last_modified: now_timestamp(),
schema_version: 1,
},
};
self.write_store(&store)?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyExport {
pub version: u32,
pub exported_at: u64,
pub encrypted_data: String,
}
#[derive(Debug, Clone)]
pub struct BackupInfo {
pub path: PathBuf,
pub timestamp: u64,
pub file_name: String,
}
impl Drop for KeyStorage {
fn drop(&mut self) {
self.clear_master_key();
}
}
const _: () = ();
#[cfg(test)]
mod tests {
use super::*;
use tempfile;
#[test]
fn test_error_sanitizer_minimal_level() {
let master_key = [0x42; 32];
let sanitizer = ErrorSanitizer::new(&master_key, SanitizationLevel::Minimal);
let key_hex = hex::encode(master_key);
let error_msg = format!("Failed with key: {key_hex}");
let sanitized = sanitizer.sanitize(&error_msg);
assert_eq!(sanitized, "Failed with key: ***");
}
#[test]
fn test_error_sanitizer_standard_level() {
let master_key = [0x42; 32];
let sanitizer = ErrorSanitizer::new(&master_key, SanitizationLevel::Standard);
let test_key_hex = hex::encode(master_key);
let error_msg = format!("Failed with key: {}", test_key_hex);
let sanitized = sanitizer.sanitize(&error_msg);
assert!(sanitized.contains("***"));
assert!(!sanitized.contains(&test_key_hex));
let fragment_msg = "Error with key fragment: deadbeefcafebabe";
let sanitized = sanitizer.sanitize(fragment_msg);
assert!(sanitized.contains("***"));
assert!(!sanitized.contains("deadbeefcafebabe"));
let pattern_msg = "key: 12345678, master: abcdefgh, secret: 87654321";
let sanitized = sanitizer.sanitize(pattern_msg);
assert!(sanitized.contains("key: ***"));
assert!(sanitized.contains("master: ***"));
assert!(sanitized.contains("secret: ***"));
}
#[test]
fn test_error_sanitizer_aggressive_level() {
let master_key = [0x42; 32];
let sanitizer = ErrorSanitizer::new(&master_key, SanitizationLevel::Aggressive);
let hex_64 = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; let sanitized = sanitizer.sanitize(&format!("Error: {}", hex_64));
assert!(sanitized.contains("***"));
assert!(!sanitized.contains(hex_64));
let base64_long = "SGVsbG8gV29ybGQhVGhpcyBpcyBhIHZlcnkgbG9uZyBiYXNlNjQgc3RyaW5nIHRlc3Q="; let sanitized = sanitizer.sanitize(&format!("Data: {}", base64_long));
assert!(sanitized.contains("***"));
assert!(!sanitized.contains(base64_long));
}
#[test]
fn test_key_storage_sanitization_integration() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let error_msg = "Test error without master key";
let sanitized = storage.sanitize_error(error_msg);
assert_eq!(sanitized, error_msg);
let master_key = [0x42; 32];
storage.set_master_key(&master_key);
let test_key_hex = hex::encode(master_key);
let error_msg = format!("Test error with key: {}", test_key_hex);
let sanitized = storage.sanitize_error(&error_msg);
assert!(sanitized.contains("***"));
assert!(!sanitized.contains(&test_key_hex));
storage.clear_master_key();
let error_msg = "Test error after clearing master key";
let sanitized = storage.sanitize_error(error_msg);
assert_eq!(sanitized, error_msg);
}
#[test]
fn test_sanitization_level_variants() {
let levels = [
SanitizationLevel::Minimal,
SanitizationLevel::Standard,
SanitizationLevel::Aggressive,
];
assert_ne!(levels[0], levels[1]);
assert_ne!(levels[1], levels[2]);
assert_ne!(levels[0], levels[2]);
let copied = levels[0];
assert_eq!(copied, levels[0]);
let _debug = format!("{:?}", levels[0]);
}
#[test]
fn test_error_sanitizer_with_replacement_overrides_default() {
let master_key = [0x55; 32];
let sanitizer = ErrorSanitizer::new(&master_key, SanitizationLevel::Minimal)
.with_replacement("[REDACTED]".to_string());
let key_hex = hex::encode(master_key);
let msg = format!("error: {}", key_hex);
let sanitized = sanitizer.sanitize(&msg);
assert_eq!(sanitized, "error: [REDACTED]");
}
#[test]
fn test_error_sanitizer_minimal_leaves_other_patterns() {
let master_key = [0x77; 32];
let sanitizer = ErrorSanitizer::new(&master_key, SanitizationLevel::Minimal);
let msg = "fragment: deadbeefcafebabe";
let sanitized = sanitizer.sanitize(msg);
assert_eq!(
sanitized, msg,
"Minimal level must not touch other fragments"
);
}
#[test]
fn test_key_storage_new_creates_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let storage_dir = temp_dir.path().join("nested").join("keys");
let _storage = KeyStorage::new(storage_dir.clone()).expect("KeyStorage::new");
assert!(storage_dir.exists(), "storage directory should be created");
assert!(storage_dir.is_dir());
}
#[test]
fn test_key_storage_new_returns_empty_manager() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let km = storage.get_key_manager();
assert!(km.list_keys().is_empty());
}
#[test]
fn test_key_storage_set_master_key_enables_sanitizer() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let plain = "no secret here";
assert_eq!(storage.sanitize_error(plain), plain);
let master_key = [0x42; 32];
storage.set_master_key(&master_key);
let key_hex = hex::encode(master_key);
let msg = format!("error with {}", key_hex);
let sanitized = storage.sanitize_error(&msg);
assert!(sanitized.contains("***"));
assert!(!sanitized.contains(&key_hex));
}
#[test]
fn test_key_storage_clear_master_key_disables_sanitizer() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0x42; 32];
storage.set_master_key(&master_key);
storage.clear_master_key();
let plain = "no secret here";
assert_eq!(storage.sanitize_error(plain), plain);
}
#[test]
fn test_key_storage_save_without_master_key_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let err = storage.save().unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Master key not set"), "got: {}", msg);
}
#[test]
fn test_key_storage_load_without_master_key_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let err = storage.load().unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Master key not set"), "got: {}", msg);
}
#[test]
fn test_key_storage_load_no_file_returns_ok() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0x01; 32];
storage.set_master_key(&master_key);
storage
.load()
.expect("load should succeed when no file exists");
}
#[test]
fn test_key_storage_initialize_with_master_key_persists_file() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0x10; 32];
storage
.initialize_with_master_key(&master_key, "prod".to_string(), "team".to_string())
.expect("initialize_with_master_key");
let keys_file = temp_dir.path().join("keys.json");
assert!(
keys_file.exists(),
"keys.json should be created after initialize"
);
let contents = std::fs::read_to_string(&keys_file).unwrap();
assert!(contents.contains("\"version\""), "got: {}", contents);
assert!(contents.contains("\"encrypted_data\""));
assert!(contents.contains("\"checksum\""));
}
#[test]
fn test_key_storage_save_load_round_trip() {
let temp_dir = tempfile::tempdir().unwrap();
let master_key = [0x20; 32];
let mut storage_a = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage_a.set_master_key(&master_key);
storage_a
.initialize_with_master_key(&master_key, "prod".to_string(), "team".to_string())
.expect("initialize");
let km_a = storage_a.get_key_manager();
assert_eq!(km_a.get_default_key_id(), "prod");
assert_eq!(km_a.list_keys().len(), 1);
let mut storage_b = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage_b.set_master_key(&master_key);
storage_b.load().expect("load");
let km_b = storage_b.get_key_manager();
assert_eq!(km_b.get_default_key_id(), "prod");
assert_eq!(km_b.list_keys().len(), 1);
}
#[test]
fn test_key_storage_load_wrong_master_key_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let master_key = [0x30; 32];
let wrong_key = [0x99; 32];
let mut storage_a = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage_a.set_master_key(&master_key);
storage_a
.initialize_with_master_key(&master_key, "prod".to_string(), "team".to_string())
.expect("initialize");
let mut storage_b = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage_b.set_master_key(&wrong_key);
let err = storage_b.load().unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Failed to decrypt") || msg.contains("Decryption"),
"got: {}",
msg
);
}
#[test]
fn test_key_storage_get_key_manager_mut_allows_mutation() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0x40; 32];
storage.set_master_key(&master_key);
{
let km = storage.get_key_manager_mut();
km.initialize(&master_key, "k1".to_string(), "u".to_string())
.expect("initialize");
}
let km = storage.get_key_manager();
assert_eq!(km.get_default_key_id(), "k1");
}
#[test]
fn test_key_storage_export_keys_creates_file() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0x50; 32];
storage.set_master_key(&master_key);
storage
.initialize_with_master_key(&master_key, "prod".to_string(), "team".to_string())
.unwrap();
let export_path = temp_dir.path().join("export.json");
storage.export_keys(&export_path).expect("export_keys");
assert!(export_path.exists(), "export file should be created");
let contents = std::fs::read_to_string(&export_path).unwrap();
assert!(contents.contains("\"version\""), "got: {}", contents);
assert!(contents.contains("\"encrypted_data\""));
assert!(contents.contains("\"exported_at\""));
}
#[test]
fn test_key_storage_export_keys_without_master_key_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let export_path = temp_dir.path().join("export.json");
let err = storage.export_keys(&export_path).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Master key not set"), "got: {}", msg);
}
#[test]
fn test_key_storage_import_keys_nonexistent_file_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0x60; 32];
let import_path = temp_dir.path().join("nonexistent.json");
let err = storage.import_keys(&import_path, &master_key).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Failed to open import file"), "got: {}", msg);
}
#[test]
fn test_key_storage_backup_creates_backup_file() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0x70; 32];
storage.set_master_key(&master_key);
storage
.initialize_with_master_key(&master_key, "prod".to_string(), "team".to_string())
.unwrap();
let backup_dir = temp_dir.path().join("backups");
storage.backup(&backup_dir).expect("backup");
assert!(backup_dir.exists(), "backup directory should be created");
let backups = storage.list_backups(&backup_dir).expect("list_backups");
assert_eq!(
backups.len(),
1,
"expected exactly one backup, got {:?}",
backups
);
let b = &backups[0];
assert!(b.file_name.starts_with("keys_backup_"));
assert!(b.file_name.ends_with(".json"));
assert!(b.timestamp > 0);
assert!(b.path.exists());
}
#[test]
fn test_key_storage_list_backups_returns_empty_for_no_backups() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let backup_dir = temp_dir.path().join("no_backups_here");
std::fs::create_dir_all(&backup_dir).unwrap();
let backups = storage.list_backups(&backup_dir).expect("list_backups");
assert!(
backups.is_empty(),
"expected zero backups, got {:?}",
backups
);
}
#[test]
fn test_key_storage_list_backups_filters_non_backup_files() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let backup_dir = temp_dir.path().join("mixed");
std::fs::create_dir_all(&backup_dir).unwrap();
std::fs::write(backup_dir.join("keys_backup_1000.json"), "{}").unwrap();
std::fs::write(backup_dir.join("random.json"), "{}").unwrap();
std::fs::write(backup_dir.join("keys_backup_notanumber.json"), "{}").unwrap();
std::fs::write(backup_dir.join("keys_backup_.json"), "{}").unwrap();
let backups = storage.list_backups(&backup_dir).expect("list_backups");
assert_eq!(backups.len(), 1, "got {:?}", backups);
assert_eq!(backups[0].timestamp, 1000);
}
#[test]
fn test_key_storage_list_backups_sorted_newest_first() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let backup_dir = temp_dir.path().join("sorted");
std::fs::create_dir_all(&backup_dir).unwrap();
std::fs::write(backup_dir.join("keys_backup_100.json"), "{}").unwrap();
std::fs::write(backup_dir.join("keys_backup_300.json"), "{}").unwrap();
std::fs::write(backup_dir.join("keys_backup_200.json"), "{}").unwrap();
let backups = storage.list_backups(&backup_dir).expect("list_backups");
assert_eq!(backups.len(), 3);
assert_eq!(backups[0].timestamp, 300, "newest first");
assert_eq!(backups[1].timestamp, 200);
assert_eq!(backups[2].timestamp, 100, "oldest last");
}
#[test]
fn test_key_storage_backup_without_master_key_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let backup_dir = temp_dir.path().join("backups");
let err = storage.backup(&backup_dir).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Master key not set"), "got: {}", msg);
}
#[test]
fn test_key_storage_rotate_master_key_round_trip() {
let temp_dir = tempfile::tempdir().unwrap();
let old_key = [0x80; 32];
let new_key = [0x81; 32];
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage.set_master_key(&old_key);
storage
.initialize_with_master_key(&old_key, "prod".to_string(), "team".to_string())
.unwrap();
storage
.rotate_master_key(&old_key, &new_key)
.expect("rotate_master_key should succeed after bug fix");
let store = storage
.read_store()
.expect("read_store should succeed after rotation");
let plaintext = storage
.decrypt_data(&store.encrypted_data, &new_key)
.expect("decryption with new master key should succeed");
assert!(
!plaintext.is_empty(),
"decrypted plaintext should be non-empty"
);
let old_key_result = storage.decrypt_data(&store.encrypted_data, &old_key);
assert!(
old_key_result.is_err(),
"decryption with old key should fail after rotation, but succeeded"
);
}
#[test]
fn test_encrypted_key_store_serialize_deserialize() {
let store = EncryptedKeyStore {
version: 1,
encrypted_data: "enc_data".to_string(),
checksum: "checksum_val".to_string(),
created_at: 12345,
metadata: KeyStoreMetadata {
key_id: "k1".to_string(),
key_count: 3,
last_modified: 12345,
schema_version: 1,
},
};
let json = serde_json::to_string(&store).expect("serialize");
let de: EncryptedKeyStore = serde_json::from_str(&json).expect("deserialize");
assert_eq!(de.version, 1);
assert_eq!(de.encrypted_data, "enc_data");
assert_eq!(de.checksum, "checksum_val");
assert_eq!(de.created_at, 12345);
assert_eq!(de.metadata.key_id, "k1");
assert_eq!(de.metadata.key_count, 3);
assert_eq!(de.metadata.schema_version, 1);
}
#[test]
fn test_key_store_metadata_construction() {
let meta = KeyStoreMetadata {
key_id: "k1".to_string(),
key_count: 5,
last_modified: 999,
schema_version: 2,
};
assert_eq!(meta.key_id, "k1");
assert_eq!(meta.key_count, 5);
assert_eq!(meta.last_modified, 999);
assert_eq!(meta.schema_version, 2);
}
#[test]
fn test_key_export_serialize_deserialize() {
let export = KeyExport {
version: 1,
exported_at: 42,
encrypted_data: "data".to_string(),
};
let json = serde_json::to_string(&export).expect("serialize");
let de: KeyExport = serde_json::from_str(&json).expect("deserialize");
assert_eq!(de.version, 1);
assert_eq!(de.exported_at, 42);
assert_eq!(de.encrypted_data, "data");
}
#[test]
fn test_backup_info_construction() {
let path = std::path::PathBuf::from("/tmp/keys_backup_123.json");
let info = BackupInfo {
path: path.clone(),
timestamp: 123,
file_name: "keys_backup_123.json".to_string(),
};
assert_eq!(info.path, path);
assert_eq!(info.timestamp, 123);
assert_eq!(info.file_name, "keys_backup_123.json");
}
#[test]
fn test_key_storage_drop_clears_master_key() {
let temp_dir = tempfile::tempdir().unwrap();
let master_key = [0x90; 32];
{
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage.set_master_key(&master_key);
storage
.initialize_with_master_key(&master_key, "k".to_string(), "u".to_string())
.unwrap();
assert!(temp_dir.path().join("keys.json").exists());
}
assert!(temp_dir.path().join("keys.json").exists());
}
#[test]
fn test_decrypt_data_invalid_format_no_colon() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let err = storage
.decrypt_data("no-colon-here-just-text", &[0u8; 32])
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Invalid encrypted data format"),
"got: {}",
msg
);
}
#[test]
fn test_decrypt_data_invalid_format_too_many_colons() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let err = storage.decrypt_data("aaa:bbb:ccc", &[0u8; 32]).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Invalid encrypted data format"),
"got: {}",
msg
);
}
#[test]
fn test_decrypt_data_invalid_base64_nonce() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let err = storage
.decrypt_data("!!!invalid!!!:SGVsbG8=", &[0u8; 32])
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Failed to decode nonce"), "got: {}", msg);
}
#[test]
fn test_decrypt_data_invalid_base64_ciphertext() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let err = storage
.decrypt_data("SGVsbG8=:!!!not-base64!!!", &[0u8; 32])
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Failed to decode ciphertext"), "got: {}", msg);
}
#[test]
fn test_decrypt_data_decryption_failure() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let err = storage
.decrypt_data(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:AAAAAAAAAAAAAAAA",
&[0u8; 32],
)
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Decryption failed"), "got: {}", msg);
}
#[test]
fn test_calculate_checksum_deterministic() {
let s = "test-data-for-checksum";
let h1 = KeyStorage::calculate_checksum(s);
let h2 = KeyStorage::calculate_checksum(s);
assert_eq!(h1, h2, "checksum must be deterministic for same input");
}
#[test]
fn test_calculate_checksum_differs_for_different_input() {
let h1 = KeyStorage::calculate_checksum("input-one");
let h2 = KeyStorage::calculate_checksum("input-two");
assert_ne!(h1, h2, "different inputs must yield different checksums");
}
#[test]
fn test_calculate_checksum_is_base64_sha256() {
let h = KeyStorage::calculate_checksum("hello");
assert_eq!(h.len(), 44, "expected 44-char base64 SHA-256, got: {}", h);
assert!(h.ends_with('='), "base64 of 32 bytes ends with padding");
}
#[test]
fn test_validate_checksum_mismatch_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xAB; 32];
storage.set_master_key(&master_key);
storage
.initialize_with_master_key(&master_key, "prod".to_string(), "team".to_string())
.unwrap();
let mut store = storage.read_store().expect("read_store");
let original_checksum = store.checksum.clone();
store.checksum = "tampered-checksum-value".to_string();
let err = storage.validate_checksum(&store).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("checksum mismatch"), "got: {}", msg);
assert_ne!(original_checksum, store.checksum);
}
#[test]
fn test_validate_checksum_passes_for_valid_store() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xAC; 32];
storage.set_master_key(&master_key);
storage
.initialize_with_master_key(&master_key, "prod".to_string(), "team".to_string())
.unwrap();
let store = storage.read_store().expect("read_store");
storage
.validate_checksum(&store)
.expect("checksum must match for a freshly-saved store");
}
#[test]
fn test_load_with_corrupted_checksum_file_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xAD; 32];
storage.set_master_key(&master_key);
storage
.initialize_with_master_key(&master_key, "prod".to_string(), "team".to_string())
.unwrap();
let keys_path = temp_dir.path().join("keys.json");
let raw = std::fs::read_to_string(&keys_path).unwrap();
let mut value: serde_json::Value = serde_json::from_str(&raw).unwrap();
value["checksum"] = serde_json::json!("corrupted-checksum");
std::fs::write(&keys_path, serde_json::to_string_pretty(&value).unwrap()).unwrap();
let err = storage.load().unwrap_err();
let msg = err.to_string();
assert!(msg.contains("checksum mismatch"), "got: {}", msg);
}
#[test]
fn test_read_store_invalid_json_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xAE; 32];
storage.set_master_key(&master_key);
let keys_path = temp_dir.path().join("keys.json");
std::fs::write(&keys_path, "{ this is not valid json }").unwrap();
let err = storage.load().unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Failed to parse key store"), "got: {}", msg);
}
#[test]
fn test_export_import_round_trip() {
let temp_dir = tempfile::tempdir().unwrap();
let master_key = [0xB0; 32];
let mut storage_a = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage_a.set_master_key(&master_key);
storage_a
.initialize_with_master_key(&master_key, "prod".to_string(), "team".to_string())
.unwrap();
let store = storage_a.read_store().expect("read_store");
let export = KeyExport {
version: 1,
exported_at: now_timestamp(),
encrypted_data: store.encrypted_data.clone(),
};
let export_path = temp_dir.path().join("export.json");
std::fs::write(&export_path, serde_json::to_string_pretty(&export).unwrap()).unwrap();
let mut storage_b = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage_b
.import_keys(&export_path, &master_key)
.expect("import should succeed with matching checksum");
let km_b = storage_b.get_key_manager();
assert_eq!(km_b.get_default_key_id(), "prod");
assert_eq!(km_b.list_keys().len(), 1);
}
#[test]
fn test_import_keys_invalid_json_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xB1; 32];
let import_path = temp_dir.path().join("bad.json");
std::fs::write(&import_path, "not valid json").unwrap();
let err = storage.import_keys(&import_path, &master_key).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Failed to parse import file"), "got: {}", msg);
}
#[test]
fn test_import_keys_checksum_mismatch_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let master_key = [0xB2; 32];
let mut storage_a = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage_a.set_master_key(&master_key);
storage_a
.initialize_with_master_key(&master_key, "prod".to_string(), "team".to_string())
.unwrap();
let sub_dir = temp_dir.path().join("other");
let mut storage_b = KeyStorage::new(sub_dir.clone()).unwrap();
storage_b.set_master_key(&master_key);
storage_b
.initialize_with_master_key(&master_key, "other-id".to_string(), "u".to_string())
.unwrap();
let export_path = sub_dir.join("export_b.json");
storage_b.export_keys(&export_path).expect("export_b");
let err = storage_a
.import_keys(&export_path, &master_key)
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("checksum mismatch"), "got: {}", msg);
}
#[test]
fn test_serialize_deserialize_key_manager_round_trip() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xB3; 32];
storage.set_master_key(&master_key);
storage
.initialize_with_master_key(&master_key, "round-trip".to_string(), "u".to_string())
.unwrap();
let serialized = storage.serialize_key_manager().expect("serialize");
assert!(!serialized.is_empty());
BASE64.decode(&serialized).expect("base64 decode");
}
#[test]
fn test_deserialize_key_manager_invalid_base64_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let err = storage
.deserialize_key_manager("!!!not-base64!!!")
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Invalid key data"), "got: {}", msg);
}
#[test]
fn test_deserialize_key_manager_invalid_json_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let bad_json = BASE64.encode(b"{ not valid json }");
let err = storage.deserialize_key_manager(&bad_json).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Failed to deserialize"), "got: {}", msg);
}
#[test]
fn test_encrypt_decrypt_data_round_trip() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xB4; 32];
let plaintext = "sensitive-key-data";
let encrypted = storage
.encrypt_data(plaintext, &master_key)
.expect("encrypt_data");
assert_eq!(encrypted.matches(':').count(), 1);
let decrypted = storage
.decrypt_data(&encrypted, &master_key)
.expect("decrypt_data");
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_encrypt_data_differs_for_each_call() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xB5; 32];
let plaintext = "same-plaintext";
let e1 = storage.encrypt_data(plaintext, &master_key).unwrap();
let e2 = storage.encrypt_data(plaintext, &master_key).unwrap();
assert_ne!(e1, e2, "encrypted outputs must differ due to random nonce");
}
#[test]
fn test_save_without_master_key_then_set_and_save() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let err = storage.save().unwrap_err();
assert!(err.to_string().contains("Master key not set"));
let master_key = [0xB6; 32];
storage.set_master_key(&master_key);
{
let km = storage.get_key_manager_mut();
km.initialize(&master_key, "k1".to_string(), "u".to_string())
.expect("initialize");
}
storage.save().expect("save after set_master_key");
assert!(temp_dir.path().join("keys.json").exists());
}
#[test]
fn test_load_then_save_preserves_state() {
let temp_dir = tempfile::tempdir().unwrap();
let master_key = [0xB7; 32];
let mut storage_a = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage_a.set_master_key(&master_key);
storage_a
.initialize_with_master_key(&master_key, "persist".to_string(), "u".to_string())
.unwrap();
let mut storage_b = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage_b.set_master_key(&master_key);
storage_b.load().expect("load");
assert_eq!(storage_b.get_key_manager().get_default_key_id(), "persist");
storage_b.save().expect("save");
let mut storage_c = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage_c.set_master_key(&master_key);
storage_c.load().expect("load");
assert_eq!(storage_c.get_key_manager().get_default_key_id(), "persist");
}
#[test]
fn test_initialize_with_master_key_sets_master_key_for_encryption() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xB8; 32];
storage
.initialize_with_master_key(&master_key, "k".to_string(), "u".to_string())
.unwrap();
storage
.save()
.expect("save must succeed after initialize_with_master_key");
assert!(temp_dir.path().join("keys.json").exists());
let key_hex = hex::encode(master_key);
let msg = format!("error with key: {}", key_hex);
let sanitized = storage.sanitize_error(&msg);
assert_eq!(
sanitized, msg,
"initialize_with_master_key must NOT enable the sanitizer"
);
}
#[test]
fn test_get_key_manager_returns_reference() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let km = storage.get_key_manager();
let _ = km.get_default_key_id();
assert!(km.list_keys().is_empty());
}
#[test]
fn test_rotate_master_key_updates_internal_key() {
let temp_dir = tempfile::tempdir().unwrap();
let old_key = [0xC0; 32];
let new_key = [0xC1; 32];
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage.set_master_key(&old_key);
storage
.initialize_with_master_key(&old_key, "prod".to_string(), "team".to_string())
.unwrap();
storage
.rotate_master_key(&old_key, &new_key)
.expect("rotate");
storage.save().expect("save after rotation");
let mut storage_b = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage_b.set_master_key(&new_key);
storage_b.load().expect("load with new key");
assert_eq!(storage_b.get_key_manager().get_default_key_id(), "prod");
}
#[test]
fn test_list_backups_nonexistent_directory_returns_empty() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let backups = storage
.list_backups(&temp_dir.path().join("never-created"))
.expect("list_backups");
assert!(backups.is_empty());
}
#[test]
fn test_export_keys_overwrites_existing_file() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xD0; 32];
storage.set_master_key(&master_key);
storage
.initialize_with_master_key(&master_key, "prod".to_string(), "team".to_string())
.unwrap();
let export_path = temp_dir.path().join("export.json");
std::fs::write(&export_path, "garbage").unwrap();
storage.export_keys(&export_path).expect("export");
let contents = std::fs::read_to_string(&export_path).unwrap();
assert!(
contents.contains("\"encrypted_data\""),
"export should overwrite, got: {}",
contents
);
}
#[test]
fn test_encrypted_key_store_default_fields() {
let store = EncryptedKeyStore {
version: u32::MAX,
encrypted_data: String::new(),
checksum: String::new(),
created_at: 0,
metadata: KeyStoreMetadata {
key_id: String::new(),
key_count: 0,
last_modified: 0,
schema_version: 0,
},
};
let json = serde_json::to_string(&store).expect("serialize");
let de: EncryptedKeyStore = serde_json::from_str(&json).expect("deserialize");
assert_eq!(de.version, u32::MAX);
assert!(de.encrypted_data.is_empty());
assert_eq!(de.metadata.key_count, 0);
}
#[test]
fn test_key_storage_new_create_dir_all_fails() {
let temp_dir = tempfile::tempdir().unwrap();
let blocking_file = temp_dir.path().join("blocking_file");
std::fs::write(&blocking_file, "content").unwrap();
let bad_path = blocking_file.join("subdir");
let err = KeyStorage::new(bad_path)
.err()
.expect("expected KeyStorage::new to fail");
let msg = err.to_string();
assert!(
msg.contains("Failed to create key storage directory"),
"got: {}",
msg
);
}
#[test]
fn test_get_master_key_bytes_invalid_length_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
storage.master_key = Some(SecretBytes::new(vec![0u8; 16]));
let err = storage.save().unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Invalid master key length"), "got: {}", msg);
}
#[test]
fn test_decrypt_data_invalid_utf8_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xE0; 32];
let encryptor = XChaCha20Crypto::new();
let non_utf8: [u8; 4] = [0xFF, 0xFE, 0xFD, 0xFC];
let (nonce, ciphertext) = encryptor.encrypt(&non_utf8, &master_key).expect("encrypt");
let encrypted = format!("{}:{}", BASE64.encode(&nonce), BASE64.encode(&ciphertext));
let err = storage.decrypt_data(&encrypted, &master_key).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Failed to convert decrypted data to string"),
"got: {}",
msg
);
}
#[test]
fn test_write_store_open_fails() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xE1; 32];
storage.set_master_key(&master_key);
storage
.initialize_with_master_key(&master_key, "k".to_string(), "u".to_string())
.unwrap();
let tmp_path = temp_dir.path().join("keys.json.tmp");
std::fs::create_dir(&tmp_path).unwrap();
let err = storage.save().unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Failed to open temp key store"),
"got: {}",
msg
);
}
#[test]
fn test_read_store_open_fails() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let err = storage.read_store().unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Failed to open key store"), "got: {}", msg);
}
#[test]
fn test_read_store_read_fails() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xE3; 32];
storage.set_master_key(&master_key);
let keys_path = temp_dir.path().join("keys.json");
std::fs::write(&keys_path, [0xFF, 0xFE, 0xFD]).unwrap();
let err = storage.load().unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Failed to read key store"), "got: {}", msg);
}
#[test]
fn test_backup_create_dir_all_fails() {
let temp_dir = tempfile::tempdir().unwrap();
let mut storage = KeyStorage::new(temp_dir.path().to_path_buf()).unwrap();
let master_key = [0xE4; 32];
storage.set_master_key(&master_key);
storage
.initialize_with_master_key(&master_key, "k".to_string(), "u".to_string())
.unwrap();
let blocking_file = temp_dir.path().join("blocking_file");
std::fs::write(&blocking_file, "content").unwrap();
let bad_backup_path = blocking_file.join("subdir");
let err = storage.backup(&bad_backup_path).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Failed to create backup directory"),
"got: {}",
msg
);
}
}