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}