Skip to main content

vyn_core/
storage.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use secrecy::ExposeSecret;
5use tokio::sync::RwLock;
6
7use crate::crypto::{SecretBytes, decrypt, encrypt, secret_bytes};
8use crate::manifest::Manifest;
9
10#[derive(Debug, thiserror::Error)]
11pub enum StorageError {
12    #[error("object not found")]
13    NotFound,
14    #[error("serialization failed: {0}")]
15    Serialize(#[from] serde_json::Error),
16    #[error("crypto operation failed: {0}")]
17    Crypto(#[from] crate::crypto::CryptoError),
18    #[error("transport error: {0}")]
19    Transport(String),
20}
21
22pub type StorageResult<T> = Result<T, StorageError>;
23
24#[allow(async_fn_in_trait)]
25pub trait StorageProvider: Send + Sync {
26    async fn get_manifest(&self, project_id: &str) -> StorageResult<Option<Vec<u8>>>;
27    async fn put_manifest(&self, project_id: &str, manifest_payload: &[u8]) -> StorageResult<()>;
28    async fn upload_blob(&self, hash: &str, data: Vec<u8>) -> StorageResult<()>;
29    async fn download_blob(&self, hash: &str) -> StorageResult<Option<Vec<u8>>>;
30    async fn create_invite(
31        &self,
32        user_id: &str,
33        vault_id: &str,
34        payload: Vec<u8>,
35    ) -> StorageResult<()>;
36    async fn get_invites(&self, user_id: &str, vault_id: &str) -> StorageResult<Vec<Vec<u8>>>;
37}
38
39#[derive(Clone, Default)]
40pub struct InMemoryStorageProvider {
41    manifests: Arc<RwLock<HashMap<String, Vec<u8>>>>,
42    blobs: Arc<RwLock<HashMap<String, Vec<u8>>>>,
43    invites: Arc<RwLock<HashMap<String, Vec<Vec<u8>>>>>,
44}
45
46impl InMemoryStorageProvider {
47    pub fn new() -> Self {
48        Self::default()
49    }
50}
51
52impl StorageProvider for InMemoryStorageProvider {
53    async fn get_manifest(&self, project_id: &str) -> StorageResult<Option<Vec<u8>>> {
54        Ok(self.manifests.read().await.get(project_id).cloned())
55    }
56
57    async fn put_manifest(&self, project_id: &str, manifest_payload: &[u8]) -> StorageResult<()> {
58        self.manifests
59            .write()
60            .await
61            .insert(project_id.to_string(), manifest_payload.to_vec());
62        Ok(())
63    }
64
65    async fn upload_blob(&self, hash: &str, data: Vec<u8>) -> StorageResult<()> {
66        self.blobs.write().await.insert(hash.to_string(), data);
67        Ok(())
68    }
69
70    async fn download_blob(&self, hash: &str) -> StorageResult<Option<Vec<u8>>> {
71        Ok(self.blobs.read().await.get(hash).cloned())
72    }
73
74    async fn create_invite(
75        &self,
76        user_id: &str,
77        vault_id: &str,
78        payload: Vec<u8>,
79    ) -> StorageResult<()> {
80        let key = format!("{user_id}:{vault_id}");
81        let mut invites = self.invites.write().await;
82        invites.entry(key).or_default().push(payload);
83        Ok(())
84    }
85
86    async fn get_invites(&self, user_id: &str, vault_id: &str) -> StorageResult<Vec<Vec<u8>>> {
87        let key = format!("{user_id}:{vault_id}");
88        Ok(self
89            .invites
90            .read()
91            .await
92            .get(&key)
93            .cloned()
94            .unwrap_or_default())
95    }
96}
97
98pub fn encrypt_manifest(manifest: &Manifest, key: &SecretBytes) -> StorageResult<Vec<u8>> {
99    let bytes = serde_json::to_vec(manifest)?;
100    let encrypted = encrypt(key, &secret_bytes(bytes))?;
101
102    let mut payload = Vec::with_capacity(encrypted.nonce.len() + encrypted.ciphertext.len());
103    payload.extend_from_slice(&encrypted.nonce);
104    payload.extend_from_slice(&encrypted.ciphertext);
105    Ok(payload)
106}
107
108pub fn decrypt_manifest(payload: &[u8], key: &SecretBytes) -> StorageResult<Manifest> {
109    const NONCE_LEN: usize = 12;
110    if payload.len() < NONCE_LEN {
111        return Err(StorageError::NotFound);
112    }
113
114    let mut nonce = [0u8; NONCE_LEN];
115    nonce.copy_from_slice(&payload[..NONCE_LEN]);
116    let encrypted = crate::crypto::EncryptedData {
117        nonce,
118        ciphertext: payload[NONCE_LEN..].to_vec(),
119    };
120
121    let plaintext = decrypt(key, &encrypted)?;
122    Ok(serde_json::from_slice(plaintext.expose_secret())?)
123}
124
125#[cfg(test)]
126mod tests {
127    use super::{InMemoryStorageProvider, StorageProvider, decrypt_manifest, encrypt_manifest};
128    use crate::crypto::secret_bytes;
129    use crate::manifest::{FileEntry, Manifest};
130
131    #[test]
132    fn manifest_encryption_roundtrip() {
133        let manifest = Manifest {
134            version: 1,
135            files: vec![FileEntry {
136                path: ".env".to_string(),
137                sha256: "abc".to_string(),
138                size: 42,
139                mode: 0o644,
140            }],
141        };
142        let key = secret_bytes(vec![7u8; 32]);
143
144        let encrypted =
145            encrypt_manifest(&manifest, &key).expect("manifest encryption should succeed");
146        let restored =
147            decrypt_manifest(&encrypted, &key).expect("manifest decryption should succeed");
148
149        assert_eq!(manifest, restored);
150    }
151
152    #[tokio::test]
153    async fn push_pull_roundtrip() {
154        let provider = InMemoryStorageProvider::new();
155        let key = secret_bytes(vec![9u8; 32]);
156        let project = "vault-123";
157
158        let manifest = Manifest {
159            version: 1,
160            files: vec![FileEntry {
161                path: ".env".to_string(),
162                sha256: "hash-1".to_string(),
163                size: 5,
164                mode: 0o644,
165            }],
166        };
167
168        let manifest_payload = encrypt_manifest(&manifest, &key).expect("manifest should encrypt");
169        provider
170            .put_manifest(project, &manifest_payload)
171            .await
172            .expect("manifest should upload");
173        provider
174            .upload_blob("hash-1", b"hello".to_vec())
175            .await
176            .expect("blob should upload");
177
178        let pulled_manifest_payload = provider
179            .get_manifest(project)
180            .await
181            .expect("manifest should download")
182            .expect("manifest should exist");
183        let pulled_manifest =
184            decrypt_manifest(&pulled_manifest_payload, &key).expect("manifest should decrypt");
185        let blob = provider
186            .download_blob("hash-1")
187            .await
188            .expect("blob should download")
189            .expect("blob should exist");
190
191        assert_eq!(pulled_manifest, manifest);
192        assert_eq!(blob, b"hello");
193    }
194}