mpc_wallet_core/
storage.rs

1//! Key Share Storage Interface
2//!
3//! This module provides abstractions for secure storage of MPC key shares.
4//! Key shares are always stored encrypted, with multiple backend options:
5//!
6//! - **FileSystemStore**: Local encrypted files (development/testing)
7//! - **EncryptedMemoryStore**: In-memory with encryption (testing)
8//!
9//! ## Security Considerations
10//!
11//! - Key shares are encrypted using ChaCha20-Poly1305 before storage
12//! - Encryption keys should be derived from user passwords or hardware security modules
13//! - The storage interface is async to support remote backends (cloud, TEE)
14//!
15//! ## Example
16//!
17//! ```rust,ignore
18//! use mpc_wallet_core::storage::{FileSystemStore, KeyShareStore};
19//!
20//! // Create a file system store
21//! let store = FileSystemStore::new("/path/to/shares", encryption_key)?;
22//!
23//! // Store a key share
24//! store.store("my-wallet", &encrypted_share).await?;
25//!
26//! // Load a key share
27//! let share = store.load("my-wallet").await?;
28//! ```
29
30use crate::{AgentKeyShare, Error, PartyRole, Result};
31use async_trait::async_trait;
32use chacha20poly1305::{
33    ChaCha20Poly1305, Nonce,
34    aead::{Aead, KeyInit},
35};
36use serde::{Deserialize, Serialize};
37use std::collections::HashMap;
38use std::path::PathBuf;
39use std::sync::Arc;
40use tokio::sync::RwLock;
41
42/// Encrypted key share with metadata
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct EncryptedKeyShare {
45    /// Encrypted share data
46    pub ciphertext: Vec<u8>,
47    /// Nonce used for encryption (12 bytes)
48    pub nonce: [u8; 12],
49    /// Key derivation salt (32 bytes)
50    pub salt: [u8; 32],
51    /// Role of the party
52    pub role: PartyRole,
53    /// Public key (not encrypted)
54    pub public_key: Vec<u8>,
55    /// Ethereum address
56    pub eth_address: String,
57    /// Creation timestamp
58    pub created_at: i64,
59    /// Version for future compatibility
60    pub version: u32,
61}
62
63impl EncryptedKeyShare {
64    /// Current version of the encrypted share format
65    pub const CURRENT_VERSION: u32 = 1;
66
67    /// Encrypt a key share using the provided key
68    pub fn encrypt(share: &AgentKeyShare, encryption_key: &[u8; 32]) -> Result<Self> {
69        let cipher = ChaCha20Poly1305::new(encryption_key.into());
70
71        // Generate random nonce and salt
72        let nonce_bytes: [u8; 12] = rand::random();
73        let salt: [u8; 32] = rand::random();
74        let nonce = Nonce::from_slice(&nonce_bytes);
75
76        // Serialize and encrypt the share
77        let plaintext =
78            serde_json::to_vec(share).map_err(|e| Error::Serialization(e.to_string()))?;
79
80        let ciphertext = cipher
81            .encrypt(nonce, plaintext.as_ref())
82            .map_err(|e| Error::Encryption(e.to_string()))?;
83
84        let eth_address = share.eth_address().unwrap_or_default();
85
86        Ok(Self {
87            ciphertext,
88            nonce: nonce_bytes,
89            salt,
90            role: share.role,
91            public_key: share.public_key.clone(),
92            eth_address,
93            created_at: chrono::Utc::now().timestamp(),
94            version: Self::CURRENT_VERSION,
95        })
96    }
97
98    /// Decrypt the key share using the provided key
99    pub fn decrypt(&self, encryption_key: &[u8; 32]) -> Result<AgentKeyShare> {
100        let cipher = ChaCha20Poly1305::new(encryption_key.into());
101        let nonce = Nonce::from_slice(&self.nonce);
102
103        let plaintext = cipher
104            .decrypt(nonce, self.ciphertext.as_ref())
105            .map_err(|_| {
106                Error::Encryption("Decryption failed - invalid key or corrupted data".into())
107            })?;
108
109        let share: AgentKeyShare = serde_json::from_slice(&plaintext)
110            .map_err(|e| Error::Deserialization(e.to_string()))?;
111
112        Ok(share)
113    }
114}
115
116/// Trait for key share storage backends
117#[async_trait]
118pub trait KeyShareStore: Send + Sync {
119    /// Store an encrypted key share
120    async fn store(&self, id: &str, share: &EncryptedKeyShare) -> Result<()>;
121
122    /// Load an encrypted key share
123    async fn load(&self, id: &str) -> Result<EncryptedKeyShare>;
124
125    /// Delete a key share
126    async fn delete(&self, id: &str) -> Result<()>;
127
128    /// Check if a key share exists
129    async fn exists(&self, id: &str) -> Result<bool>;
130
131    /// List all stored share IDs
132    async fn list(&self) -> Result<Vec<String>>;
133
134    /// Get metadata for a share without decrypting
135    async fn get_metadata(&self, id: &str) -> Result<ShareMetadata>;
136}
137
138/// Metadata about a stored share (without sensitive data)
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ShareMetadata {
141    /// Share identifier
142    pub id: String,
143    /// Role of the party
144    pub role: PartyRole,
145    /// Public key
146    pub public_key: Vec<u8>,
147    /// Ethereum address
148    pub eth_address: String,
149    /// Creation timestamp
150    pub created_at: i64,
151    /// Storage version
152    pub version: u32,
153}
154
155/// In-memory store for testing
156#[derive(Debug)]
157pub struct EncryptedMemoryStore {
158    shares: Arc<RwLock<HashMap<String, EncryptedKeyShare>>>,
159}
160
161impl EncryptedMemoryStore {
162    /// Create a new in-memory store
163    pub fn new() -> Self {
164        Self {
165            shares: Arc::new(RwLock::new(HashMap::new())),
166        }
167    }
168}
169
170impl Default for EncryptedMemoryStore {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176#[async_trait]
177impl KeyShareStore for EncryptedMemoryStore {
178    async fn store(&self, id: &str, share: &EncryptedKeyShare) -> Result<()> {
179        let mut shares = self.shares.write().await;
180        shares.insert(id.to_string(), share.clone());
181        Ok(())
182    }
183
184    async fn load(&self, id: &str) -> Result<EncryptedKeyShare> {
185        let shares = self.shares.read().await;
186        shares
187            .get(id)
188            .cloned()
189            .ok_or_else(|| Error::KeyShareNotFound(id.to_string()))
190    }
191
192    async fn delete(&self, id: &str) -> Result<()> {
193        let mut shares = self.shares.write().await;
194        shares.remove(id);
195        Ok(())
196    }
197
198    async fn exists(&self, id: &str) -> Result<bool> {
199        let shares = self.shares.read().await;
200        Ok(shares.contains_key(id))
201    }
202
203    async fn list(&self) -> Result<Vec<String>> {
204        let shares = self.shares.read().await;
205        Ok(shares.keys().cloned().collect())
206    }
207
208    async fn get_metadata(&self, id: &str) -> Result<ShareMetadata> {
209        let shares = self.shares.read().await;
210        let share = shares
211            .get(id)
212            .ok_or_else(|| Error::KeyShareNotFound(id.to_string()))?;
213
214        Ok(ShareMetadata {
215            id: id.to_string(),
216            role: share.role,
217            public_key: share.public_key.clone(),
218            eth_address: share.eth_address.clone(),
219            created_at: share.created_at,
220            version: share.version,
221        })
222    }
223}
224
225/// File system store for local storage
226#[derive(Debug)]
227pub struct FileSystemStore {
228    /// Base directory for storing shares
229    base_path: PathBuf,
230}
231
232impl FileSystemStore {
233    /// Create a new file system store
234    pub fn new(base_path: impl Into<PathBuf>) -> Result<Self> {
235        let base_path = base_path.into();
236
237        // Create directory if it doesn't exist
238        if !base_path.exists() {
239            std::fs::create_dir_all(&base_path)?;
240        }
241
242        Ok(Self { base_path })
243    }
244
245    /// Get the file path for a share ID
246    fn share_path(&self, id: &str) -> PathBuf {
247        // Sanitize ID to prevent path traversal
248        let safe_id = id.replace(['/', '\\', '.', '~'], "_");
249        self.base_path.join(format!("{}.share", safe_id))
250    }
251}
252
253#[async_trait]
254impl KeyShareStore for FileSystemStore {
255    async fn store(&self, id: &str, share: &EncryptedKeyShare) -> Result<()> {
256        let path = self.share_path(id);
257        let data = serde_json::to_vec_pretty(share)?;
258
259        tokio::fs::write(&path, data).await?;
260
261        // Set restrictive permissions on Unix
262        #[cfg(unix)]
263        {
264            use std::os::unix::fs::PermissionsExt;
265            let perms = std::fs::Permissions::from_mode(0o600);
266            std::fs::set_permissions(&path, perms)?;
267        }
268
269        Ok(())
270    }
271
272    async fn load(&self, id: &str) -> Result<EncryptedKeyShare> {
273        let path = self.share_path(id);
274
275        if !path.exists() {
276            return Err(Error::KeyShareNotFound(id.to_string()));
277        }
278
279        let data = tokio::fs::read(&path).await?;
280        let share: EncryptedKeyShare =
281            serde_json::from_slice(&data).map_err(|e| Error::Deserialization(e.to_string()))?;
282
283        Ok(share)
284    }
285
286    async fn delete(&self, id: &str) -> Result<()> {
287        let path = self.share_path(id);
288
289        if path.exists() {
290            // Overwrite with zeros before deleting for security
291            let size = tokio::fs::metadata(&path).await?.len() as usize;
292            let zeros = vec![0u8; size];
293            tokio::fs::write(&path, zeros).await?;
294            tokio::fs::remove_file(&path).await?;
295        }
296
297        Ok(())
298    }
299
300    async fn exists(&self, id: &str) -> Result<bool> {
301        let path = self.share_path(id);
302        Ok(path.exists())
303    }
304
305    async fn list(&self) -> Result<Vec<String>> {
306        let mut ids = Vec::new();
307        let mut entries = tokio::fs::read_dir(&self.base_path).await?;
308
309        while let Some(entry) = entries.next_entry().await? {
310            let path = entry.path();
311            if path.extension().and_then(|s| s.to_str()) == Some("share") {
312                if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
313                    ids.push(stem.to_string());
314                }
315            }
316        }
317
318        Ok(ids)
319    }
320
321    async fn get_metadata(&self, id: &str) -> Result<ShareMetadata> {
322        let share = self.load(id).await?;
323
324        Ok(ShareMetadata {
325            id: id.to_string(),
326            role: share.role,
327            public_key: share.public_key,
328            eth_address: share.eth_address,
329            created_at: share.created_at,
330            version: share.version,
331        })
332    }
333}
334
335/// Derive an encryption key from a password using Argon2
336pub fn derive_key_from_password(password: &str, salt: &[u8; 32]) -> Result<[u8; 32]> {
337    use sha2::{Digest, Sha256};
338
339    // Simple key derivation (in production, use Argon2 or similar)
340    let mut hasher = Sha256::new();
341    hasher.update(password.as_bytes());
342    hasher.update(salt);
343
344    // Multiple rounds for basic stretching
345    let mut result = hasher.finalize();
346    for _ in 0..10000 {
347        let mut hasher = Sha256::new();
348        hasher.update(&result);
349        hasher.update(salt);
350        result = hasher.finalize();
351    }
352
353    let mut key = [0u8; 32];
354    key.copy_from_slice(&result);
355    Ok(key)
356}
357
358/// Generate a random encryption key
359pub fn generate_encryption_key() -> [u8; 32] {
360    rand::random()
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use crate::types::KeyShareMetadata;
367    use k256::Scalar;
368    use std::collections::HashMap;
369
370    fn create_test_share() -> AgentKeyShare {
371        AgentKeyShare {
372            party_id: 0,
373            role: PartyRole::Agent,
374            secret_share: Scalar::ONE,
375            public_key: vec![0x02; 33],
376            public_shares: vec![vec![0x02; 33]; 3],
377            chain_code: [0u8; 32],
378            metadata: KeyShareMetadata {
379                share_id: "test".to_string(),
380                role: PartyRole::Agent,
381                created_at: 0,
382                last_refreshed_at: None,
383                addresses: HashMap::new(),
384                label: None,
385            },
386        }
387    }
388
389    #[test]
390    fn test_encrypt_decrypt() {
391        let share = create_test_share();
392        let key = generate_encryption_key();
393
394        let encrypted = EncryptedKeyShare::encrypt(&share, &key).unwrap();
395        assert!(!encrypted.ciphertext.is_empty());
396        assert_eq!(encrypted.role, PartyRole::Agent);
397
398        let decrypted = encrypted.decrypt(&key).unwrap();
399        assert_eq!(decrypted.party_id, share.party_id);
400        assert_eq!(decrypted.role, share.role);
401    }
402
403    #[test]
404    fn test_decrypt_wrong_key() {
405        let share = create_test_share();
406        let key1 = generate_encryption_key();
407        let key2 = generate_encryption_key();
408
409        let encrypted = EncryptedKeyShare::encrypt(&share, &key1).unwrap();
410        let result = encrypted.decrypt(&key2);
411        assert!(result.is_err());
412    }
413
414    #[tokio::test]
415    async fn test_memory_store() {
416        let store = EncryptedMemoryStore::new();
417        let share = create_test_share();
418        let key = generate_encryption_key();
419
420        let encrypted = EncryptedKeyShare::encrypt(&share, &key).unwrap();
421
422        // Store
423        store.store("test-id", &encrypted).await.unwrap();
424
425        // Exists
426        assert!(store.exists("test-id").await.unwrap());
427        assert!(!store.exists("nonexistent").await.unwrap());
428
429        // Load
430        let loaded = store.load("test-id").await.unwrap();
431        assert_eq!(loaded.role, encrypted.role);
432
433        // List
434        let list = store.list().await.unwrap();
435        assert_eq!(list, vec!["test-id"]);
436
437        // Metadata
438        let metadata = store.get_metadata("test-id").await.unwrap();
439        assert_eq!(metadata.role, PartyRole::Agent);
440
441        // Delete
442        store.delete("test-id").await.unwrap();
443        assert!(!store.exists("test-id").await.unwrap());
444    }
445
446    #[test]
447    fn test_derive_key_from_password() {
448        let password = "test-password";
449        let salt: [u8; 32] = rand::random();
450
451        let key1 = derive_key_from_password(password, &salt).unwrap();
452        let key2 = derive_key_from_password(password, &salt).unwrap();
453
454        // Same password + salt should produce same key
455        assert_eq!(key1, key2);
456
457        // Different password should produce different key
458        let key3 = derive_key_from_password("different", &salt).unwrap();
459        assert_ne!(key1, key3);
460    }
461
462    #[tokio::test]
463    async fn test_file_system_store() {
464        let temp_dir = std::env::temp_dir().join(format!("mpc-test-{}", rand::random::<u64>()));
465        let store = FileSystemStore::new(&temp_dir).unwrap();
466        let share = create_test_share();
467        let key = generate_encryption_key();
468
469        let encrypted = EncryptedKeyShare::encrypt(&share, &key).unwrap();
470
471        // Store
472        store.store("test-file", &encrypted).await.unwrap();
473
474        // Load
475        let loaded = store.load("test-file").await.unwrap();
476        assert_eq!(loaded.role, encrypted.role);
477
478        // List
479        let list = store.list().await.unwrap();
480        assert!(list.contains(&"test-file".to_string()));
481
482        // Cleanup
483        store.delete("test-file").await.unwrap();
484        std::fs::remove_dir_all(&temp_dir).ok();
485    }
486}