use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Mutex;
use crate::error::{CryptoError, Result};
use crate::types::{KeyData, TofuRecord};
#[async_trait]
pub trait KeyStore: Send + Sync {
async fn save_group_key(&self, session_id: &str, data: KeyData) -> Result<()>;
async fn load_group_key(&self, session_id: &str) -> Result<Option<KeyData>>;
async fn delete_group_key(&self, session_id: &str) -> Result<()>;
async fn save_tofu_record(&self, id: &str, record: TofuRecord) -> Result<()>;
async fn load_tofu_record(&self, id: &str) -> Result<Option<TofuRecord>>;
}
pub struct MemoryKeyStore {
group_keys: Mutex<HashMap<String, KeyData>>,
tofu_records: Mutex<HashMap<String, TofuRecord>>,
}
impl MemoryKeyStore {
pub fn new() -> Self {
Self {
group_keys: Mutex::new(HashMap::new()),
tofu_records: Mutex::new(HashMap::new()),
}
}
}
impl Default for MemoryKeyStore {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl KeyStore for MemoryKeyStore {
async fn save_group_key(&self, session_id: &str, data: KeyData) -> Result<()> {
self.group_keys
.lock()
.map_err(|_| CryptoError::Storage("key store lock poisoned".into()))?
.insert(session_id.to_string(), data);
Ok(())
}
async fn load_group_key(&self, session_id: &str) -> Result<Option<KeyData>> {
Ok(self
.group_keys
.lock()
.map_err(|_| CryptoError::Storage("key store lock poisoned".into()))?
.get(session_id)
.cloned())
}
async fn delete_group_key(&self, session_id: &str) -> Result<()> {
self.group_keys
.lock()
.map_err(|_| CryptoError::Storage("key store lock poisoned".into()))?
.remove(session_id);
Ok(())
}
async fn save_tofu_record(&self, id: &str, record: TofuRecord) -> Result<()> {
self.tofu_records
.lock()
.map_err(|_| CryptoError::Storage("key store lock poisoned".into()))?
.insert(id.to_string(), record);
Ok(())
}
async fn load_tofu_record(&self, id: &str) -> Result<Option<TofuRecord>> {
Ok(self
.tofu_records
.lock()
.map_err(|_| CryptoError::Storage("key store lock poisoned".into()))?
.get(id)
.cloned())
}
}
#[cfg(feature = "fs-store")]
pub struct FileSystemKeyStore {
base_dir: std::path::PathBuf,
}
#[cfg(feature = "fs-store")]
impl FileSystemKeyStore {
pub fn new(base_dir: impl Into<std::path::PathBuf>) -> Self {
Self {
base_dir: base_dir.into(),
}
}
fn hash_id(id: &str) -> String {
use sha2::{Digest, Sha256};
let hash = Sha256::digest(id.as_bytes());
hash.iter().map(|b| format!("{b:02x}")).collect()
}
fn group_key_path(&self, session_id: &str) -> std::path::PathBuf {
self.base_dir
.join("group-keys")
.join(format!("{}.json", Self::hash_id(session_id)))
}
fn tofu_path(&self, id: &str) -> std::path::PathBuf {
self.base_dir
.join("tofu")
.join(format!("{}.json", Self::hash_id(id)))
}
async fn atomic_write(
path: &std::path::Path,
data: &[u8],
) -> std::result::Result<(), CryptoError> {
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| CryptoError::Storage(format!("create dir: {e}")))?;
}
let tmp = path.with_extension("tmp");
tokio::fs::write(&tmp, data)
.await
.map_err(|e| CryptoError::Storage(format!("write tmp: {e}")))?;
tokio::fs::rename(&tmp, path)
.await
.map_err(|e| CryptoError::Storage(format!("rename: {e}")))?;
Ok(())
}
}
#[cfg(feature = "fs-store")]
#[async_trait]
impl KeyStore for FileSystemKeyStore {
async fn save_group_key(&self, session_id: &str, data: KeyData) -> Result<()> {
let path = self.group_key_path(session_id);
let json = serde_json::to_vec_pretty(&data)
.map_err(|e| CryptoError::Serialization(e.to_string()))?;
Self::atomic_write(&path, &json).await
}
async fn load_group_key(&self, session_id: &str) -> Result<Option<KeyData>> {
let path = self.group_key_path(session_id);
match tokio::fs::read(&path).await {
Ok(bytes) => {
let data: KeyData = serde_json::from_slice(&bytes)
.map_err(|e| CryptoError::Storage(format!("parse group key: {e}")))?;
Ok(Some(data))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(CryptoError::Storage(format!("read group key: {e}"))),
}
}
async fn delete_group_key(&self, session_id: &str) -> Result<()> {
let path = self.group_key_path(session_id);
match tokio::fs::remove_file(&path).await {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(CryptoError::Storage(format!("delete group key: {e}"))),
}
}
async fn save_tofu_record(&self, id: &str, record: TofuRecord) -> Result<()> {
let path = self.tofu_path(id);
let json = serde_json::to_vec_pretty(&record)
.map_err(|e| CryptoError::Serialization(e.to_string()))?;
Self::atomic_write(&path, &json).await
}
async fn load_tofu_record(&self, id: &str) -> Result<Option<TofuRecord>> {
let path = self.tofu_path(id);
match tokio::fs::read(&path).await {
Ok(bytes) => {
let record: TofuRecord = serde_json::from_slice(&bytes)
.map_err(|e| CryptoError::Storage(format!("parse tofu: {e}")))?;
Ok(Some(record))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(CryptoError::Storage(format!("read tofu: {e}"))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn memory_store_group_key_round_trip() {
let store = MemoryKeyStore::new();
let data = KeyData {
key: serde_json::json!({"kty": "oct", "k": "dGVzdA=="}),
stored_at: 1000,
};
store
.save_group_key("session-1", data.clone())
.await
.unwrap();
let loaded = store.load_group_key("session-1").await.unwrap();
assert!(loaded.is_some());
assert_eq!(loaded.unwrap().key["kty"], "oct");
}
#[tokio::test]
async fn memory_store_group_key_missing() {
let store = MemoryKeyStore::new();
let loaded = store.load_group_key("nonexistent").await.unwrap();
assert!(loaded.is_none());
}
#[tokio::test]
async fn memory_store_delete_group_key() {
let store = MemoryKeyStore::new();
let data = KeyData {
key: serde_json::json!({"kty": "oct", "k": "dGVzdA=="}),
stored_at: 1000,
};
store.save_group_key("s1", data).await.unwrap();
store.delete_group_key("s1").await.unwrap();
let loaded = store.load_group_key("s1").await.unwrap();
assert!(loaded.is_none());
}
#[tokio::test]
async fn memory_store_tofu_round_trip() {
let store = MemoryKeyStore::new();
let record = TofuRecord {
fingerprint: "abcd 1234".to_string(),
first_seen: 5000,
};
store.save_tofu_record("peer-1", record).await.unwrap();
let loaded = store.load_tofu_record("peer-1").await.unwrap();
assert!(loaded.is_some());
assert_eq!(loaded.unwrap().fingerprint, "abcd 1234");
}
}
#[cfg(all(test, feature = "fs-store"))]
mod fs_tests {
use super::*;
#[tokio::test]
async fn fs_store_group_key_round_trip() {
let dir = tempfile::tempdir().unwrap();
let store = FileSystemKeyStore::new(dir.path());
let data = KeyData {
key: serde_json::json!({"kty": "oct", "k": "dGVzdA=="}),
stored_at: 1000,
};
store
.save_group_key("session-1", data.clone())
.await
.unwrap();
let loaded = store.load_group_key("session-1").await.unwrap();
assert!(loaded.is_some());
let loaded = loaded.unwrap();
assert_eq!(loaded.key["kty"], "oct");
assert_eq!(loaded.stored_at, 1000);
}
#[tokio::test]
async fn fs_store_group_key_missing() {
let dir = tempfile::tempdir().unwrap();
let store = FileSystemKeyStore::new(dir.path());
let loaded = store.load_group_key("nonexistent").await.unwrap();
assert!(loaded.is_none());
}
#[tokio::test]
async fn fs_store_delete_group_key() {
let dir = tempfile::tempdir().unwrap();
let store = FileSystemKeyStore::new(dir.path());
let data = KeyData {
key: serde_json::json!({"kty": "oct", "k": "dGVzdA=="}),
stored_at: 1000,
};
store.save_group_key("s1", data).await.unwrap();
store.delete_group_key("s1").await.unwrap();
let loaded = store.load_group_key("s1").await.unwrap();
assert!(loaded.is_none());
}
#[tokio::test]
async fn fs_store_tofu_round_trip() {
let dir = tempfile::tempdir().unwrap();
let store = FileSystemKeyStore::new(dir.path());
let record = TofuRecord {
fingerprint: "abcd 1234".to_string(),
first_seen: 5000,
};
store.save_tofu_record("peer-1", record).await.unwrap();
let loaded = store.load_tofu_record("peer-1").await.unwrap();
assert!(loaded.is_some());
assert_eq!(loaded.unwrap().fingerprint, "abcd 1234");
}
#[tokio::test]
async fn fs_store_persist_and_reload() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().to_path_buf();
{
let store = FileSystemKeyStore::new(&path);
let data = KeyData {
key: serde_json::json!({"kty": "oct", "k": "dGVzdGtleQ=="}),
stored_at: 42000,
};
store.save_group_key("persist-test", data).await.unwrap();
}
{
let store = FileSystemKeyStore::new(&path);
let loaded = store.load_group_key("persist-test").await.unwrap();
assert!(loaded.is_some());
let loaded = loaded.unwrap();
assert_eq!(loaded.key["k"], "dGVzdGtleQ==");
assert_eq!(loaded.stored_at, 42000);
}
}
}