reddb_engine/query/local_database/
vault.rs1use super::*;
6use crate::catalog::CollectionModel;
7use crate::storage::entity::{EntityId, RowData, UnifiedEntity};
8use reddb_crypto::{
9 aes_gcm::{aes256_gcm_decrypt, aes256_gcm_encrypt},
10 os_random,
11};
12use zeroize::Zeroizing;
13
14const COLLECTION: &str = "red.local.vault";
15const PAYLOAD_BYTES_MAX: usize = 1024 * 1024;
16
17pub struct LocalVaultKey {
20 key: Zeroizing<[u8; 32]>,
21 salt: [u8; 16],
22}
23
24fn failure(code: &'static str) -> LocalReadError {
25 LocalReadError::Vault(code)
26}
27
28impl LocalVaultKey {
29 fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
30 if plaintext.len() > PAYLOAD_BYTES_MAX {
31 return Err(failure("VAULT_SIZE_LIMIT"));
32 }
33 let mut nonce = [0u8; 12];
34 os_random::fill_bytes(&mut nonce).map_err(|_| failure("VAULT_RANDOM_FAILED"))?;
35 let ciphertext = aes256_gcm_encrypt(
36 &self.key,
37 &nonce,
38 reddb_file::VAULT_LOGICAL_EXPORT_AAD,
39 plaintext,
40 );
41 hex::decode(reddb_file::encode_vault_logical_export(
42 &self.salt,
43 &nonce,
44 &ciphertext,
45 ))
46 .map_err(|_| failure("VAULT_CORRUPT"))
47 }
48
49 fn unseal(&self, bytes: &[u8]) -> Result<Zeroizing<String>> {
50 if bytes.len() > PAYLOAD_BYTES_MAX + 64 {
51 return Err(failure("VAULT_SIZE_LIMIT"));
52 }
53 let envelope = reddb_file::decode_vault_logical_export(&hex::encode(bytes))
54 .map_err(|_| failure("VAULT_CORRUPT"))?;
55 if envelope.salt != self.salt {
56 return Err(failure("VAULT_LOCKED"));
57 }
58 let plaintext = Zeroizing::new(
59 aes256_gcm_decrypt(
60 &self.key,
61 &envelope.nonce,
62 reddb_file::VAULT_LOGICAL_EXPORT_AAD,
63 &envelope.ciphertext,
64 )
65 .map_err(|_| failure("VAULT_AUTHENTICATION_FAILED"))?,
66 );
67 let text = std::str::from_utf8(&plaintext).map_err(|_| failure("VAULT_CORRUPT"))?;
68 Ok(Zeroizing::new(text.to_string()))
69 }
70}
71
72fn key_valid(key: &str) -> Result<()> {
73 if key.is_empty() || key.len() > 512 || key.chars().any(char::is_control) {
74 return Err(failure("VAULT_INVALID_KEY"));
75 }
76 Ok(())
77}
78
79fn payload_entries(payload: &str) -> Result<HashMap<String, Zeroizing<String>>> {
82 let mut entries = HashMap::new();
83 for line in payload.lines() {
84 if let Some(record) = line.strip_prefix("KV:") {
85 let (key, value) = record
86 .split_once('\t')
87 .ok_or_else(|| failure("VAULT_CORRUPT"))?;
88 key_valid(key)?;
89 let bytes = Zeroizing::new(hex::decode(value).map_err(|_| failure("VAULT_CORRUPT"))?);
90 let value = std::str::from_utf8(&bytes).map_err(|_| failure("VAULT_CORRUPT"))?;
91 if entries
92 .insert(key.into(), Zeroizing::new(value.into()))
93 .is_some()
94 {
95 return Err(failure("VAULT_CORRUPT"));
96 }
97 }
98 }
99 Ok(entries)
100}
101
102impl<S: StorageStrategy> Database<S> {
103 fn vault_blob(&self) -> Result<Option<Vec<u8>>> {
104 self.check_healthy()?;
105 if self
106 .transaction
107 .as_ref()
108 .is_some_and(|transaction| transaction.aborted)
109 {
110 return Err(query_error("transaction is aborted; ROLLBACK is required"));
111 }
112 let (store, _) = self.visible()?;
113 if !store.contains_collection(COLLECTION)? {
114 return Ok(None);
115 }
116 let entity = store
117 .get_entity(COLLECTION, EntityId::new(1))?
118 .ok_or_else(|| failure("VAULT_CORRUPT"))?;
119 match entity
120 .data
121 .as_row()
122 .and_then(|row| row.get_by_name("value"))
123 {
124 Some(Value::Secret(bytes)) => Ok(Some(bytes.clone())),
125 _ => Err(failure("VAULT_CORRUPT")),
126 }
127 }
128
129 pub fn vault_initialized(&self) -> Result<bool> {
131 Ok(self.vault_blob()?.is_some())
132 }
133
134 pub fn vault_unlock(&mut self, password: &str, create: bool) -> Result<LocalVaultKey> {
137 let blob = self.vault_blob()?;
138 let mut salt = [0u8; 16];
139 if let Some(bytes) = &blob {
140 salt = reddb_file::decode_vault_logical_export(&hex::encode(bytes))
141 .map_err(|_| failure("VAULT_CORRUPT"))?
142 .salt;
143 } else {
144 if !create {
145 return Err(failure("VAULT_NOT_INITIALIZED"));
146 }
147 if self.transaction.is_some() {
148 return Err(query_error("initialize Vault before BEGIN"));
149 }
150 os_random::fill_bytes(&mut salt).map_err(|_| failure("VAULT_RANDOM_FAILED"))?;
151 }
152 let key = LocalVaultKey {
153 salt,
154 key: reddb_crypto::vault::passphrase_key(password, &salt)
155 .map_err(|_| failure("VAULT_INVALID_PASSWORD"))?,
156 };
157 if let Some(bytes) = blob {
158 payload_entries(&key.unseal(&bytes)?)?;
159 } else {
160 let mut contracts = self.contracts.clone();
161 let QueryExpr::CreateTable(query) =
162 reddb_rql::parse_frontend("CREATE TABLE local_vault (value TEXT)")
163 .map_err(LocalReadError::Parse)?
164 .into_query_expr()
165 else {
166 unreachable!("static DDL")
167 };
168 let mut contract =
169 crate::catalog::ddl::collection_contract_from_create_table(&query, 0)
170 .map_err(query_error)?;
171 contract.name = COLLECTION.into();
172 contract.declared_model = CollectionModel::Vault;
173 contract.table_def = None;
174 contract.declared_columns.clear();
175 contracts.insert(COLLECTION.into(), contract);
176 let actions = [
177 StoreWalAction::CreateCollection {
178 name: COLLECTION.into(),
179 },
180 vault_action(key.seal(b"SEALED:false\n")?),
181 ];
182 self.storage
183 .publish_catalog_actions(&actions, catalog_bytes(&contracts))?;
184 self.contracts = contracts;
185 }
186 Ok(key)
187 }
188
189 pub fn vault_keys(&self, key: &LocalVaultKey) -> Result<Vec<String>> {
190 let blob = self
191 .vault_blob()?
192 .ok_or_else(|| failure("VAULT_NOT_INITIALIZED"))?;
193 let mut keys: Vec<_> = payload_entries(&key.unseal(&blob)?)?.into_keys().collect();
194 keys.sort();
195 Ok(keys)
196 }
197
198 pub fn vault_reveal(&self, key: &LocalVaultKey, name: &str) -> Result<Option<String>> {
200 key_valid(name)?;
201 let blob = self
202 .vault_blob()?
203 .ok_or_else(|| failure("VAULT_NOT_INITIALIZED"))?;
204 Ok(payload_entries(&key.unseal(&blob)?)?
205 .remove(name)
206 .map(|value| value.to_string()))
207 }
208
209 pub fn vault_put(
212 &mut self,
213 key: &LocalVaultKey,
214 name: &str,
215 value: Option<&str>,
216 ) -> Result<()> {
217 let result = self.vault_put_inner(key, name, value);
218 if result.is_err() {
219 if let Some(transaction) = &mut self.transaction {
220 transaction.aborted = true;
221 }
222 }
223 result
224 }
225
226 fn vault_put_inner(
227 &mut self,
228 key: &LocalVaultKey,
229 name: &str,
230 value: Option<&str>,
231 ) -> Result<()> {
232 key_valid(name)?;
233 if value.is_some_and(|value| value.len() > PAYLOAD_BYTES_MAX / 2) {
234 return Err(failure("VAULT_SIZE_LIMIT"));
235 }
236 let blob = self
237 .vault_blob()?
238 .ok_or_else(|| failure("VAULT_NOT_INITIALIZED"))?;
239 let plaintext = key.unseal(&blob)?;
240 let mut entries = payload_entries(&plaintext)?;
241 match value {
242 Some(value) => {
243 entries.insert(name.into(), Zeroizing::new(value.into()));
244 }
245 None => {
246 entries.remove(name);
247 }
248 }
249 let mut updated = Zeroizing::new(String::new());
250 for line in plaintext.lines().filter(|line| !line.starts_with("KV:")) {
251 updated.push_str(line);
252 updated.push('\n');
253 }
254 for (name, value) in entries {
255 updated.push_str("KV:");
256 updated.push_str(&name);
257 updated.push('\t');
258 let encoded = Zeroizing::new(hex::encode(value.as_bytes()));
259 updated.push_str(&encoded);
260 updated.push('\n');
261 }
262 let action = vault_action(key.seal(updated.as_bytes())?);
263 if let Some(transaction) = &mut self.transaction {
264 if transaction.actions.len() >= super::super::local_read::LOCAL_READ_ROWS_MAX {
265 return Err(LocalReadError::ResourceLimit);
266 }
267 self.storage
268 .apply_transaction(&transaction.store, std::slice::from_ref(&action))?;
269 transaction.actions.push(action);
270 } else {
271 self.storage.publish_actions(&[action])?;
272 }
273 Ok(())
274 }
275}
276
277impl<F: VfsFile> LocalDatabase<F> {
278 pub fn vault_initialized(&self) -> Result<bool> {
280 self.database.vault_initialized()
281 }
282
283 pub fn vault_unlock(&mut self, password: &str, create: bool) -> Result<LocalVaultKey> {
286 self.database.vault_unlock(password, create)
287 }
288
289 pub fn vault_keys(&self, key: &LocalVaultKey) -> Result<Vec<String>> {
290 self.database.vault_keys(key)
291 }
292
293 pub fn vault_reveal(&self, key: &LocalVaultKey, name: &str) -> Result<Option<String>> {
295 self.database.vault_reveal(key, name)
296 }
297
298 pub fn vault_put(
301 &mut self,
302 key: &LocalVaultKey,
303 name: &str,
304 value: Option<&str>,
305 ) -> Result<()> {
306 self.database.vault_put(key, name, value)
307 }
308}
309
310fn vault_action(blob: Vec<u8>) -> StoreWalAction {
311 let mut entity = UnifiedEntity::table_row(EntityId::new(1), COLLECTION, 1, Vec::new());
312 entity.data = crate::storage::entity::EntityData::Row(RowData::with_names(
313 vec![Value::Secret(blob)],
314 vec!["value".into()],
315 ));
316 StoreWalAction::upsert_entity(COLLECTION, &entity, None, reddb_file::STORE_VERSION_CURRENT)
317}