Skip to main content

beam/sea/session/
memory.rs

1//! In-memory session storage — ephemeral, test-safe, fast.
2//!
3//! [`InMemorySessionStorage`] stores keypairs in a `HashMap` in process memory.
4//! Data is lost when the process exits. Suitable for testing, short-lived
5//! CLI invocations, and scenarios where persistence is unnecessary.
6//!
7//! # Security
8//!
9//! Private keys live in the process heap for the duration of the session.
10//! This is NOT suitable for production use — use `super::EncryptedFileSessionStorage`
11//! for persistent, encrypted session storage.
12
13use super::super::{KeyPair, SeaError, SessionStorage};
14use async_trait::async_trait;
15use std::collections::HashMap;
16use std::sync::Mutex;
17
18/// Ephemeral session storage backed by an in-memory `HashMap`.
19///
20/// NOT for production — private keys live in process heap.
21pub struct InMemorySessionStorage {
22    data: Mutex<HashMap<String, KeyPair>>,
23}
24
25impl Default for InMemorySessionStorage {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl InMemorySessionStorage {
32    /// Creates a new empty in-memory session store.
33    pub fn new() -> Self {
34        Self {
35            data: Mutex::new(HashMap::new()),
36        }
37    }
38}
39
40#[async_trait]
41impl SessionStorage for InMemorySessionStorage {
42    async fn save(&self, alias: &str, pair: &KeyPair) -> Result<(), SeaError> {
43        self.data
44            .lock()
45            .unwrap()
46            .insert(alias.to_string(), pair.clone());
47        Ok(())
48    }
49
50    async fn load(&self, alias: &str) -> Result<Option<KeyPair>, SeaError> {
51        Ok(self.data.lock().unwrap().get(alias).cloned())
52    }
53
54    async fn clear(&self, alias: &str) -> Result<(), SeaError> {
55        self.data.lock().unwrap().remove(alias);
56        Ok(())
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    fn test_pair() -> KeyPair {
65        KeyPair {
66            pub_key: "test.pub".to_string(),
67            priv_key: "test.priv".to_string(),
68            epub_key: Some("test.epub".to_string()),
69            epriv_key: Some("test.epriv".to_string()),
70        }
71    }
72
73    #[tokio::test]
74    async fn test_save_load_roundtrip() {
75        let storage = InMemorySessionStorage::new();
76        let pair = test_pair();
77        storage.save("alice", &pair).await.unwrap();
78        let loaded = storage.load("alice").await.unwrap();
79        assert!(loaded.is_some());
80        let loaded = loaded.unwrap();
81        assert_eq!(loaded.pub_key, pair.pub_key);
82        assert_eq!(loaded.priv_key, pair.priv_key);
83    }
84
85    #[tokio::test]
86    async fn test_load_missing_returns_none() {
87        let storage = InMemorySessionStorage::new();
88        let loaded = storage.load("nobody").await.unwrap();
89        assert!(loaded.is_none());
90    }
91
92    #[tokio::test]
93    async fn test_clear_removes_entry() {
94        let storage = InMemorySessionStorage::new();
95        let pair = test_pair();
96        storage.save("bob", &pair).await.unwrap();
97        assert!(storage.load("bob").await.unwrap().is_some());
98        storage.clear("bob").await.unwrap();
99        assert!(storage.load("bob").await.unwrap().is_none());
100    }
101
102    #[tokio::test]
103    async fn test_default_is_empty() {
104        let storage = InMemorySessionStorage::default();
105        let loaded = storage.load("anyone").await.unwrap();
106        assert!(loaded.is_none());
107    }
108
109    #[tokio::test]
110    async fn test_overwrite_existing() {
111        let storage = InMemorySessionStorage::new();
112        let pair1 = test_pair();
113        let mut pair2 = test_pair();
114        pair2.pub_key = "other.pub".to_string();
115        storage.save("alice", &pair1).await.unwrap();
116        storage.save("alice", &pair2).await.unwrap();
117        let loaded = storage.load("alice").await.unwrap().unwrap();
118        assert_eq!(loaded.pub_key, "other.pub");
119    }
120}