use std::path::PathBuf;
#[derive(Debug, thiserror::Error)]
pub enum StorageError {
#[error("Key not found: {0}")]
NotFound(String),
#[error("Storage access denied")]
AccessDenied,
#[error("Encryption error: {0}")]
EncryptionError(String),
#[error("Deserialization error: {0}")]
DeserializationError(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Keyring error: {0}")]
KeyringError(String),
}
pub const KEY_DID_PRIVATE: &str = "signedby_did_private_key";
pub const KEY_LEAF_SECRET: &str = "signedby_leaf_secret";
const KEYRING_SERVICE: &str = "com.signedby.sdk";
pub trait SecureStorage: Send + Sync {
fn store(&self, key: &str, data: &[u8]) -> Result<(), StorageError>;
fn retrieve(&self, key: &str) -> Result<Vec<u8>, StorageError>;
fn exists(&self, key: &str) -> bool;
fn delete(&self, key: &str) -> Result<(), StorageError>;
}
pub struct EncryptedFileStorage {
storage_dir: PathBuf,
}
impl EncryptedFileStorage {
pub fn new(storage_dir: PathBuf) -> Result<Self, StorageError> {
std::fs::create_dir_all(&storage_dir)?;
Ok(Self { storage_dir })
}
fn key_path(&self, key: &str) -> PathBuf {
let safe_key = key.replace(|c: char| !c.is_alphanumeric() && c != '_', "_");
self.storage_dir.join(format!("{}.enc", safe_key))
}
fn get_or_create_master_key() -> Result<[u8; 32], StorageError> {
use keyring::Entry;
use chacha20poly1305::aead::rand_core::{OsRng, RngCore};
let entry = Entry::new(KEYRING_SERVICE, "master_encryption_key")
.map_err(|e| StorageError::KeyringError(format!("Failed to access keyring: {}", e)))?;
match entry.get_password() {
Ok(key_hex) => {
let key_bytes = hex::decode(&key_hex)
.map_err(|e| StorageError::KeyringError(format!("Invalid key in keyring: {}", e)))?;
if key_bytes.len() != 32 {
return Err(StorageError::KeyringError(
format!("Invalid key length in keyring: expected 32, got {}", key_bytes.len())
));
}
let mut key = [0u8; 32];
key.copy_from_slice(&key_bytes);
Ok(key)
}
Err(keyring::Error::NoEntry) => {
let mut key = [0u8; 32];
OsRng.fill_bytes(&mut key);
let key_hex = hex::encode(&key);
entry.set_password(&key_hex)
.map_err(|e| StorageError::KeyringError(format!("Failed to store key in keyring: {}", e)))?;
Ok(key)
}
Err(e) => {
Err(StorageError::KeyringError(format!("Keyring access error: {}", e)))
}
}
}
fn derive_key_specific_key(master_key: &[u8; 32], storage_key: &str) -> [u8; 32] {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(master_key);
hasher.update(b"signedby_v1:");
hasher.update(storage_key.as_bytes());
let result = hasher.finalize();
let mut derived = [0u8; 32];
derived.copy_from_slice(&result);
derived
}
}
impl SecureStorage for EncryptedFileStorage {
fn store(&self, key: &str, data: &[u8]) -> Result<(), StorageError> {
use chacha20poly1305::{
aead::{Aead, KeyInit, OsRng},
ChaCha20Poly1305, Nonce,
};
use chacha20poly1305::aead::rand_core::RngCore;
let master_key = Self::get_or_create_master_key()?;
let enc_key = Self::derive_key_specific_key(&master_key, key);
let mut nonce_bytes = [0u8; 12];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let cipher = ChaCha20Poly1305::new_from_slice(&enc_key)
.map_err(|e| StorageError::EncryptionError(e.to_string()))?;
let ciphertext = cipher.encrypt(nonce, data)
.map_err(|e| StorageError::EncryptionError(e.to_string()))?;
let mut output = Vec::with_capacity(12 + ciphertext.len());
output.extend_from_slice(&nonce_bytes);
output.extend_from_slice(&ciphertext);
std::fs::write(self.key_path(key), output)?;
Ok(())
}
fn retrieve(&self, key: &str) -> Result<Vec<u8>, StorageError> {
use chacha20poly1305::{
aead::{Aead, KeyInit},
ChaCha20Poly1305, Nonce,
};
let path = self.key_path(key);
if !path.exists() {
return Err(StorageError::NotFound(key.to_string()));
}
let data = std::fs::read(&path)?;
if data.len() < 12 {
return Err(StorageError::DeserializationError("Data too short".to_string()));
}
let nonce = Nonce::from_slice(&data[..12]);
let ciphertext = &data[12..];
let master_key = Self::get_or_create_master_key()?;
let enc_key = Self::derive_key_specific_key(&master_key, key);
let cipher = ChaCha20Poly1305::new_from_slice(&enc_key)
.map_err(|e| StorageError::EncryptionError(e.to_string()))?;
let plaintext = cipher.decrypt(nonce, ciphertext)
.map_err(|e| StorageError::EncryptionError(e.to_string()))?;
Ok(plaintext)
}
fn exists(&self, key: &str) -> bool {
self.key_path(key).exists()
}
fn delete(&self, key: &str) -> Result<(), StorageError> {
let path = self.key_path(key);
if path.exists() {
std::fs::remove_file(path)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_encrypted_storage_roundtrip() {
let dir = tempdir().unwrap();
let storage = EncryptedFileStorage::new(dir.path().to_path_buf()).unwrap();
let key = "test_key";
let data = b"secret data here";
match storage.store(key, data) {
Ok(()) => {
assert!(storage.exists(key));
let retrieved = storage.retrieve(key).unwrap();
assert_eq!(retrieved, data);
storage.delete(key).unwrap();
assert!(!storage.exists(key));
}
Err(StorageError::KeyringError(_)) => {
eprintln!("Skipping test: OS keyring not available");
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn test_not_found() {
let dir = tempdir().unwrap();
let storage = EncryptedFileStorage::new(dir.path().to_path_buf()).unwrap();
let result = storage.retrieve("nonexistent");
assert!(matches!(result, Err(StorageError::NotFound(_))));
}
#[test]
fn test_key_derivation_deterministic() {
let master = [0xABu8; 32];
let key1 = EncryptedFileStorage::derive_key_specific_key(&master, "test_key");
let key2 = EncryptedFileStorage::derive_key_specific_key(&master, "test_key");
assert_eq!(key1, key2);
}
#[test]
fn test_key_derivation_different_keys() {
let master = [0xABu8; 32];
let key1 = EncryptedFileStorage::derive_key_specific_key(&master, "key_a");
let key2 = EncryptedFileStorage::derive_key_specific_key(&master, "key_b");
assert_ne!(key1, key2);
}
}