chamber_vault/lib.rs
1mod autolock;
2mod autolock_service;
3pub mod config;
4pub mod crypto;
5pub mod db;
6mod manager;
7mod registry;
8
9// Re-export commonly used types and functions for easier access
10pub use crypto::{
11 HmacSha256, KdfParams, KeyMaterial, WrappedVaultKey, aead_decrypt, aead_encrypt, derive_key, unwrap_vault_key,
12 wrap_vault_key,
13};
14
15pub use db::{Db, ItemRow};
16
17pub use crate::autolock::AutoLockConfig;
18pub use crate::autolock_service::AutoLockCallback;
19pub use crate::autolock_service::AutoLockService;
20pub use crate::config::BackupConfig;
21pub use crate::manager::VaultManager;
22pub use crate::registry::{VaultCategory, VaultInfo, VaultRegistry};
23use color_eyre::Result;
24use color_eyre::eyre::Error;
25use color_eyre::eyre::eyre;
26use serde::{Deserialize, Serialize};
27use std::path::{Path, PathBuf};
28use std::str::FromStr;
29use time::OffsetDateTime;
30#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
31pub enum ItemKind {
32 Password,
33 EnvVar,
34 Note,
35 ApiKey,
36 SshKey,
37 Certificate,
38 Database,
39 CreditCard,
40 SecureNote,
41 Identity,
42 BankAccount,
43 Document,
44 Recovery,
45 OAuth,
46 License,
47 WifiPassword,
48 Server,
49}
50
51impl FromStr for ItemKind {
52 type Err = Error;
53
54 fn from_str(s: &str) -> Result<Self, Self::Err> {
55 let result = match s.to_lowercase().as_str() {
56 "password" | "pass" | "pwd" => ItemKind::Password,
57 "envvar" | "env" => ItemKind::EnvVar,
58 "note" => ItemKind::Note,
59 "apikey" | "api_key" | "api-key" | "token" | "api" => ItemKind::ApiKey,
60 "sshkey" | "ssh" => ItemKind::SshKey,
61 "certificate" | "cert" | "ssl" | "tls" => ItemKind::Certificate,
62 "database" | "db" => ItemKind::Database,
63 "creditcard" | "credit" | "card" => ItemKind::CreditCard,
64 "securenote" | "secure" => ItemKind::SecureNote,
65 "identity" | "id" | "passport" | "ssn" => ItemKind::Identity,
66 "bankaccount" | "bank" | "account" => ItemKind::BankAccount,
67 "document" | "doc" | "file" => ItemKind::Document,
68 "recovery" | "recoverycode" | "backup" => ItemKind::Recovery,
69 "oauth" | "oauth2" => ItemKind::OAuth,
70 "license" | "lic" | "key" => ItemKind::License,
71 "server" | "srv" | "host" => ItemKind::Server,
72 "wifi" | "wifi-password" | "wifi-pass" => ItemKind::WifiPassword,
73 _ => return Err(eyre!("Invalid item type: '{}'", s)),
74 };
75 Ok(result)
76 }
77}
78
79impl ItemKind {
80 #[must_use]
81 pub const fn as_str(self) -> &'static str {
82 match self {
83 Self::Password => "password",
84 Self::EnvVar => "env",
85 Self::Note => "note",
86 Self::ApiKey => "apikey",
87 Self::SshKey => "sshkey",
88 Self::Certificate => "certificate",
89 Self::Database => "database",
90 Self::CreditCard => "creditcard",
91 Self::SecureNote => "securenote",
92 Self::Identity => "identity",
93 Self::Server => "server",
94 Self::WifiPassword => "wifi",
95 Self::License => "license",
96 Self::BankAccount => "bankaccount",
97 Self::Document => "document",
98 Self::Recovery => "recovery",
99 Self::OAuth => "oauth",
100 }
101 }
102
103 #[must_use]
104 pub const fn all() -> &'static [ItemKind] {
105 &[
106 ItemKind::Password,
107 ItemKind::EnvVar,
108 ItemKind::Note,
109 ItemKind::ApiKey,
110 ItemKind::SshKey,
111 ItemKind::Certificate,
112 ItemKind::Database,
113 ItemKind::CreditCard,
114 ItemKind::SecureNote,
115 ItemKind::Identity,
116 ItemKind::Server,
117 ItemKind::WifiPassword,
118 ItemKind::License,
119 ItemKind::BankAccount,
120 ItemKind::Document,
121 ItemKind::Recovery,
122 ItemKind::OAuth,
123 ]
124 }
125
126 /// Returns a user-friendly display name
127 #[must_use]
128 pub const fn display_name(self) -> &'static str {
129 match self {
130 ItemKind::Password => "Password",
131 ItemKind::EnvVar => "Environment Variable",
132 ItemKind::Note => "Note",
133 ItemKind::ApiKey => "API Key",
134 ItemKind::SshKey => "SSH Key",
135 ItemKind::Certificate => "Certificate",
136 ItemKind::Database => "Database Connection",
137 ItemKind::CreditCard => "Credit Card",
138 ItemKind::SecureNote => "Secure Note",
139 ItemKind::Identity => "Identity",
140 ItemKind::Server => "Server",
141 ItemKind::WifiPassword => "Wifi",
142 ItemKind::License => "License",
143 ItemKind::BankAccount => "Bank Account",
144 ItemKind::Document => "Document",
145 ItemKind::Recovery => "Recovery",
146 ItemKind::OAuth => "OAuth",
147 }
148 }
149}
150
151#[derive(Debug, Clone)]
152pub struct Item {
153 pub id: u64,
154 pub name: String,
155 pub kind: ItemKind,
156 pub value: String,
157 pub created_at: OffsetDateTime,
158 pub updated_at: OffsetDateTime,
159}
160
161#[derive(Debug, Clone)]
162pub struct NewItem {
163 pub name: String,
164 pub kind: ItemKind,
165 pub value: String,
166}
167
168#[derive(Debug)]
169pub struct Vault {
170 db: Db,
171 key: Option<KeyMaterial>,
172 db_path: PathBuf,
173}
174
175impl Vault {
176 /// Open an existing database or create a new one if it doesn't exist.
177 ///
178 /// This function initializes a database instance from a given file system path.
179 /// If a path is provided, it attempts to use that specific path.
180 /// If no path is provided (`None` is passed), it will determine a default database path
181 /// using the `default_db_path` function.
182 ///
183 /// # Parameters
184 /// * `path` - An optional reference to a path (`Option<&Path>`). If `Some`, the database
185 /// will be opened or created at the specified path. If `None`, a default path is used.
186 ///
187 /// # Returns
188 /// * `Result<Self>` - On success, returns an instance of `Self` with an open database.
189 /// If any operation fails (e.g., obtaining the default path or opening the database),
190 /// an error is returned.
191 ///
192 /// # Errors
193 /// This function returns an error if:
194 /// * Obtaining the default database path via `default_db_path` fails.
195 /// * Opening the database at the specified or default path fails.
196 pub fn open_or_create(path: Option<&Path>) -> Result<Self> {
197 let db_path = match path {
198 Some(p) => p.to_path_buf(),
199 None => default_db_path()?,
200 };
201
202 // Ensure the parent directory exists before trying to open the database
203 if let Some(parent) = db_path.parent() {
204 std::fs::create_dir_all(parent)?;
205 }
206
207 let db = Db::open(&db_path)?;
208 Ok(Self { db, key: None, db_path })
209 }
210
211 /// Opens the default instance of a resource.
212 ///
213 /// This function attempts to open the default instance of a resource. If the default instance
214 /// does not exist, it will create a new one. The specific behavior of this function depends
215 /// on the implementation of `Self::open_or_create`.
216 ///
217 /// # Returns
218 ///
219 /// * `Result<Self>` - If successful, returns an instance of the resource wrapped in `Ok`.
220 /// If an error occurs during the opening or creation process, it returns an `Err` containing
221 /// the corresponding error.
222 ///
223 /// # Errors
224 ///
225 /// This function will return an error if the resource cannot be opened or created for any
226 /// reason, such as insufficient permissions or missing configuration.
227 pub fn open_default() -> Result<Self> {
228 Self::open_or_create(None)
229 }
230
231 pub fn db_path(&self) -> &Path {
232 &self.db_path
233 }
234
235 /// Retrieves the backup configuration for the system.
236 ///
237 /// This function attempts to load the backup configuration from a JSON file
238 /// stored in the directory containing the database. If the configuration file
239 /// does not exist, a default `BackupConfig` instance is returned.
240 ///
241 /// # Behavior
242 ///
243 /// - The configuration file is named `backup_config.json`, and its location
244 /// is inferred based on the parent directory of the `db_path` provided.
245 /// - If the parent directory cannot be determined, the current directory (`"."`)
246 /// is used as the fallback location to look for the configuration file.
247 /// - The function reads the contents of the file and deserializes it into a
248 /// `BackupConfig` object using `serde_json`.
249 /// - If the file does not exist, the function returns the default
250 /// `BackupConfig` object.
251 ///
252 /// # Errors
253 ///
254 /// This function may return an error in the following scenarios:
255 /// - If the configuration file exists, but reading its contents fails,
256 /// an `std::io::Error` is returned.
257 /// - If deserialization of the file content into a `BackupConfig` object fails,
258 /// a `serde_json::Error` is returned.
259 ///
260 /// # Returns
261 ///
262 /// A `Result` wrapping the backup configuration:
263 /// - `Ok(BackupConfig)` if the configuration is successfully loaded or the
264 /// default configuration is returned.
265 /// - `Err` if an error occurs while reading or deserializing the configuration file.
266 pub fn get_backup_config(&self) -> Result<BackupConfig> {
267 // Try to read backup config from the database
268 // For now, we'll store it as a JSON string in a special meta-table or file
269 let config_path = self
270 .db_path
271 .parent()
272 .unwrap_or_else(|| Path::new("."))
273 .join("backup_config.json");
274
275 if config_path.exists() {
276 let content = std::fs::read_to_string(config_path)?;
277 let config: BackupConfig = serde_json::from_str(&content)?;
278 Ok(config)
279 } else {
280 Ok(BackupConfig::default())
281 }
282 }
283
284 /// Sets the backup configuration for the database by saving it to a file named `backup_config.json`
285 /// in the parent directory of the database path.
286 ///
287 /// The method performs the following steps:
288 /// 1. Resolves the `backup_config.json` file path in the parent directory of `self.db_path`.
289 /// 2. Ensures the parent directory for the file exists, creating it if necessary.
290 /// 3. Serializes the `BackupConfig` object to a pretty-printed JSON string.
291 /// 4. Writes the serialized JSON string to the file.
292 ///
293 /// # Arguments
294 /// * `config` - A reference to a `BackupConfig` object containing the backup configuration settings
295 /// that will be saved to the file.
296 ///
297 /// # Returns
298 /// * `Ok(())` if the backup configuration is successfully saved.
299 /// * `Err` if any file system or serialization operation fails.
300 ///
301 /// # Errors
302 /// This function can return an error in the following cases:
303 /// * Failure to create the parent directory for the config file.
304 /// * Failure to serialize the `BackupConfig` object into a JSON string.
305 /// * Failure to write the serialized JSON string to the file.
306 pub fn set_backup_config(&self, config: &BackupConfig) -> Result<()> {
307 let config_path = self
308 .db_path
309 .parent()
310 .unwrap_or_else(|| Path::new("."))
311 .join("backup_config.json");
312
313 // Ensure directory exists
314 if let Some(parent) = config_path.parent() {
315 std::fs::create_dir_all(parent)?;
316 }
317
318 let content = serde_json::to_string_pretty(&config)?;
319 std::fs::write(config_path, content)?;
320 Ok(())
321 }
322
323 /// Get the database path for this vault
324 pub fn get_db_path(&self) -> &std::path::Path {
325 &self.db_path
326 }
327
328 pub fn is_initialized(&self) -> bool {
329 !self.db.is_meta_empty().unwrap_or(false)
330 }
331
332 /// Initializes the storage system with a master key.
333 ///
334 /// This function is used to initialize the storage backend only if it has not been
335 /// initialized already. It generates a vault key that is securely wrapped using the
336 /// derived master key, and then stores the necessary metadata in the database.
337 ///
338 /// # Parameters
339 /// - `master`: A reference to a string slice representing the master key. This key is
340 /// used as the input to derive a secure key for wrapping the vault key.
341 ///
342 /// # Returns
343 /// - `Ok(())`: If the storage is successfully initialized or was already initialized.
344 /// - `Err`: If any step in the initialization process fails (e.g., key derivation,
345 /// key wrapping, or database write).
346 ///
347 /// # Implementation Details
348 /// - First, the function checks whether the storage system has already been initialized
349 /// using the `is_initialized()` method. If true, it immediately returns `Ok(())`.
350 /// - The function then generates secure key derivation parameters (`KdfParams`) using
351 /// `KdfParams::default_secure()`.
352 /// - Using the provided master key and the key derivation parameters, a derived key
353 /// is computed (`derive_key` function).
354 /// - A new random vault key is generated (`KeyMaterial::random()`), which serves as the
355 /// key to encrypt secure data.
356 /// - The vault key is securely wrapped using the derived master key, resulting in the
357 /// wrapped key and a verifier (`wrap_vault_key` function).
358 /// - The derived key, wrapped vault key, and verifier are then persisted into the
359 /// database by calling `db.write_meta`.
360 ///
361 /// # Errors
362 /// - Fails with an error if:
363 /// - The master key derivation fails.
364 /// - The vault key wrapping fails.
365 /// - Writing the required metadata to the database fails.
366 /// - Errors are returned as a `Result::Err`, allowing the caller to handle the failure.
367 pub fn initialize(&mut self, master: &str) -> Result<()> {
368 if self.is_initialized() {
369 return Ok(());
370 }
371 let kdf = KdfParams::default_secure();
372 let master_derived = derive_key(master, &kdf)?;
373 let vault_key = KeyMaterial::random();
374 let (wrapped, verifier) = wrap_vault_key(&master_derived, &vault_key)?;
375 self.db.write_meta(&kdf, &wrapped, &verifier)?;
376 Ok(())
377 }
378
379 /// Unlocks the vault using the provided master key.
380 ///
381 /// This function attempts to unlock a vault instance by using the given `master` key.
382 /// It reads the metadata from the vault, validates the provided master key against the verifier,
383 /// and upon successful verification, derives and stores the corresponding vault key.
384 ///
385 /// # Arguments
386 ///
387 /// * `master` - A string slice representing the master key used to unlock the vault.
388 ///
389 /// # Returns
390 ///
391 /// * `Ok(())` - If the vault is successfully unlocked and the vault key is derived.
392 /// * `Err` - If the vault metadata is uninitialized, the derived master key is invalid,
393 /// or there is an issue during the unlocking process.
394 ///
395 /// # Errors
396 ///
397 /// This function can return the following errors wrapped in a `Result`:
398 /// * `eyre!("Vault not initialized")` - If the vault metadata is not present.
399 /// * `eyre!("Invalid master key")` - If the provided master key is invalid or fails verification.
400 /// * Other errors arising from key derivation or vault key unwrapping.
401 ///
402 /// # Implementation Details
403 ///
404 /// 1. Reads metadata from the vault, including a key derivation function (KDF) configuration (`kdf`),
405 /// a wrapped vault key (`wrapped`), and a verifier.
406 /// 2. Derives a key from the provided `master` key using the KDF.
407 /// 3. Verifies the derived key against the verifier. If the verification fails, returns an error.
408 /// 4. Attempts to unwrap the vault key using the derived key. Upon success, stores the derived
409 /// vault key (`vk`) within the instance's `key` field.
410 ///
411 pub fn unlock(&mut self, master: &str) -> Result<()> {
412 let (kdf, wrapped, verifier) = self.db.read_meta()?.ok_or_else(|| eyre!("Vault not initialized"))?;
413 let master_derived = derive_key(master, &kdf)?;
414 // Verify first
415 unwrap_vault_key(&master_derived, &wrapped, Some(&verifier)).map_err(|_| eyre!("Invalid master key"))?;
416 let vk = unwrap_vault_key(&master_derived, &wrapped, None)?;
417 self.key = Some(vk);
418 Ok(())
419 }
420
421 pub const fn is_unlocked(&self) -> bool {
422 self.key.is_some()
423 }
424
425 /// Retrieves a list of items from the database, decrypting their stored values.
426 ///
427 /// # Returns
428 /// - `Ok(Vec<Item>)`: A vector of decrypted items if the operation is successful.
429 /// - `Err(anyhow::Error)`: An error if the database is locked, decryption fails, or another issue occurs.
430 ///
431 /// # Implementation Details
432 /// 1. The function attempts to retrieve the encryption key (`vk`) from the `self.key` field.
433 /// If the key is unavailable, an error is returned with the message `"Locked"`.
434 /// 2. The database records are fetched using `self.db.list_items()`.
435 /// 3. For each record:
436 /// - The ciphertext is decrypted using `aead_decrypt` with the provided nonce, ciphertext, and additional authentication data (AAD).
437 /// - The decrypted plaintext is converted to a UTF-8 string to derive the item's `value`.
438 /// - An `Item` is constructed with the decrypted and existing meta-information, such as `id`, `name`, `kind`, and timestamps.
439 /// 4. The constructed items are aggregated into a `Vec<Item>` and returned.
440 ///
441 /// # Errors
442 /// - Returns an error if:
443 /// - The encryption key is not available (e.g., when the data store is locked).
444 /// - The database query fails.
445 /// - The decryption operation fails (e.g., due to invalid data).
446 /// - The plaintext fails UTF-8 validation.
447 /// - An invalid `ItemKind` is provided.
448 ///
449 /// # Dependencies
450 /// - `aead_decrypt`: A function used to decrypt the encrypted data.
451 /// - `String::from_utf8`: Used to convert decrypted data into a String.
452 /// - `ItemKind::from_str`: Parses the `kind` field into its corresponding `ItemKind` enum variant.
453 ///
454 /// # Notes
455 /// - Ensure `self.key` is properly initialized before calling this method.
456 /// - The database schema must provide the required fields for each item: `id`, `name`, `kind`, `ciphertext`, `nonce`, and timestamps.
457 pub fn list_items(&self) -> Result<Vec<Item>> {
458 let vk = self.key.as_ref().ok_or_else(|| eyre!("Locked"))?;
459 let rows = self.db.list_items()?;
460 let mut out = Vec::with_capacity(rows.len());
461 for r in rows {
462 let plaintext = aead_decrypt(vk, &r.nonce, &r.ciphertext, &r.ad())?;
463 let value = String::from_utf8(plaintext)?;
464 out.push(Item {
465 id: r.id,
466 name: r.name,
467 kind: ItemKind::from_str(&r.kind)?,
468 value,
469 created_at: r.created_at,
470 updated_at: r.updated_at,
471 });
472 }
473 Ok(out)
474 }
475
476 /// Retrieves an item by its name from the list of items.
477 ///
478 /// This method searches for an item in the collection of items maintained by the instance.
479 /// It returns the first item that matches the provided name, if it exists.
480 ///
481 /// # Parameters
482 /// - `name`: The name of the item to search for, provided as a string slice (`&str`).
483 ///
484 /// # Returns
485 /// - `Ok(Some(Item))`: If an item with the given name is found.
486 /// - `Ok(None)`: If no item with the given name exists in the collection.
487 /// - `Err(Error)`: If an error occurs while retrieving the list of items.
488 ///
489 /// # Errors
490 /// This function will return an error if the call to `self.list_items()` fails.
491 pub fn get_item_by_name(&self, name: &str) -> Result<Option<Item>> {
492 let items = self.list_items()?;
493 Ok(items.into_iter().find(|i| i.name == name))
494 }
495
496 /// Creates a new item and inserts it into the database.
497 ///
498 /// This function encrypts the value of the provided item using an AEAD encryption
499 /// scheme and then stores the encrypted data, along with additional metadata, in
500 /// the database. The encryption process uses the key stored within the struct and
501 /// associates the encrypted value with the provided item's name and kind.
502 ///
503 /// # Parameters
504 /// - `item`: A reference to a `NewItem`, containing the data required to create the item.
505 ///
506 /// # Returns
507 /// - `Ok(())` on successful encryption and insertion of the item.
508 /// - `Err(anyhow::Error)` if any step of the process fails, including:
509 /// - The key is not available (when the struct is in a "Locked" state).
510 /// - Failure during the encryption process.
511 /// - Errors encountered during the database insertion.
512 ///
513 /// # Errors
514 /// This function returns an error in the following scenarios:
515 /// - If the struct's `key` field is `None`, indicating it is locked.
516 /// - If encryption fails for any reason.
517 /// - If the database insertion fails.
518 pub fn create_item(&mut self, item: &NewItem) -> Result<()> {
519 let vk = self.key.as_ref().ok_or_else(|| eyre!("Locked"))?;
520 let nonce_cipher = aead_encrypt(
521 vk,
522 item.value.as_bytes(),
523 ItemRow::ad_for_name_kind(&item.name, item.kind.as_str()).as_ref(),
524 )?;
525 self.db
526 .insert_item(&item.name, item.kind.as_str(), &nonce_cipher.0, &nonce_cipher.1)?;
527 Ok(())
528 }
529
530 /// Deletes an item from the database with the specified ID.
531 ///
532 /// # Parameters
533 /// - `id` (i64): The unique identifier of the item to be deleted.
534 ///
535 /// # Returns
536 /// - `Result<()>`: Returns `Ok(())` if the item is successfully deleted, or an error if the deletion fails.
537 ///
538 /// # Errors
539 /// This function will return an error if:
540 /// - The item with the specified ID does not exist.
541 /// - There is a failure in the underlying database operation.
542 pub fn delete_item(&mut self, id: u64) -> Result<()> {
543 self.db.delete_item(id)
544 }
545
546 /// Changes the master key for the vault.
547 ///
548 /// This function updates the master key used to protect the vault by verifying the current
549 /// master key and re-encrypting the vault key using the new master key. It also updates
550 /// the key derivation function (KDF) parameters for the new master key and persists the
551 /// updated metadata in the database.
552 ///
553 /// # Arguments
554 ///
555 /// * `current_master` - The currently active master key used to protect the vault. This must
556 /// be provided to verify the existing setup and decrypt the vault key.
557 /// * `new_master` - The new master key to which the vault will be re-encrypted. It must conform
558 /// to the same security requirements as the old master key.
559 ///
560 /// # Returns
561 ///
562 /// * `Result<()>` - Returns `Ok(())` on successful master key change, or an error if the operation
563 /// fails. Possible error cases include:
564 /// - The vault is not initialized.
565 /// - The provided `current_master` key is invalid.
566 /// - Issues with deriving keys, unwrapping the vault key, or re-wrapping with the new key.
567 /// - Errors while persisting the updated metadata.
568 ///
569 /// # Behavior
570 ///
571 /// 1. Reads the current metadata (KDF parameters, wrapped key, and verifier) from the database.
572 /// 2. Validates the `current_master` key using the stored KDF parameters and verifier.
573 /// 3. Unwraps the vault key using the `current_master` key.
574 /// 4. Generates new secure KDF parameters for the `new_master` key.
575 /// 5. Derives a key from the `new_master` and wraps the vault key with it, generating a new verifier.
576 /// 6. Writes the new KDF parameters, wrapped key, and verifier to the metadata in the database.
577 /// 7. Updates the in-memory vault key if it is already unlocked.
578 ///
579 /// # Errors
580 ///
581 /// * Returns an error if the vault has not been initialized (e.g., no metadata exists yet).
582 /// * Returns an error if the `current_master` key fails verification or cannot unwrap the vault key.
583 /// * Returns an error for any issues in key operations (e.g., deriving, wrapping, or unwrapping).
584 /// * Returns an error if writing the updated metadata to the database fails.
585 ///
586 /// # Notes
587 ///
588 /// The updated master key takes effect immediately upon successful execution of this function.
589 /// Ensure that the `new_master` key is securely stored and managed to prevent loss of access
590 /// to the vault.
591 pub fn change_master_key(&mut self, current_master: &str, new_master: &str) -> Result<()> {
592 let (kdf_old, wrapped_old, verifier_old) =
593 self.db.read_meta()?.ok_or_else(|| eyre!("Vault not initialized"))?;
594
595 // Verify the current master and unwrap the existing vault key
596 let current_derived = derive_key(current_master, &kdf_old)?;
597 let _ = unwrap_vault_key(¤t_derived, &wrapped_old, Some(&verifier_old))
598 .map_err(|_| eyre!("Invalid current master key"))?;
599 let vault_key = unwrap_vault_key(¤t_derived, &wrapped_old, None)?;
600
601 // Generate fresh KDF params and wrap with a new master-derived key
602 let kdf_new = KdfParams::default_secure();
603 let new_derived = derive_key(new_master, &kdf_new)?;
604 let (wrapped_new, verifier_new) = wrap_vault_key(&new_derived, &vault_key)?;
605
606 // Persist new meta
607 self.db.write_meta(&kdf_new, &wrapped_new, &verifier_new)?;
608
609 // Keep the in-memory vault key usable if we were unlocked
610 self.key = Some(vault_key);
611 Ok(())
612 }
613
614 /// Updates an item in the database with a new value, preserving the item's associated metadata.
615 ///
616 /// # Parameters
617 /// - `id`: The unique identifier of the item to be updated.
618 /// - `new_value`: A reference to the new string value that will replace the current value of the item.
619 ///
620 /// # Returns
621 /// - `Ok(())`: If the operation is successful.
622 /// - `Err`: Returns an error in the following scenarios:
623 /// - If the encryption key (`self.key`) is not available (locked).
624 /// - If the specified item with the given `id` is not found in the list.
625 /// - If there's any failure during the encryption or database update.
626 ///
627 /// # Behavior
628 /// 1. Verifies that the encryption key (`self.key`) is available. If not, an error is returned.
629 /// 2. Retrieves the list of items stored and searches for the item matching the provided `id`.
630 /// 3. If the item is found, constructs authenticated data (AD) using the item's metadata (name and kind).
631 /// 4. Encrypts the `new_value` using the encryption key (`vk`), the new value, and the constructed AD.
632 /// 5. Updates the item in the database with the newly encrypted value and its corresponding nonce.
633 ///
634 /// # Errors
635 /// - If the encryption key is missing, an `eyre!("Locked")` error is returned.
636 /// - If the item is not found, an `eyre!("Item not found")` error is returned.
637 /// - Any failures during encryption or database operations propagate as errors.
638 pub fn update_item(&mut self, id: u64, new_value: &str) -> Result<()> {
639 let vk = self.key.as_ref().ok_or_else(|| eyre!("Locked"))?;
640
641 // Get the item to preserve name and kind for AD
642 let items = self.list_items()?;
643 let item = items
644 .iter()
645 .find(|i| i.id == id)
646 .ok_or_else(|| eyre!("Item not found"))?;
647
648 // Encrypt new value with same AD (name and kind)
649 let nonce_cipher = aead_encrypt(
650 vk,
651 new_value.as_bytes(),
652 ItemRow::ad_for_name_kind(&item.name, item.kind.as_str()).as_ref(),
653 )?;
654
655 self.db.update_item(id, &nonce_cipher.0, &nonce_cipher.1)?;
656 Ok(())
657 }
658
659 /// Opens an existing vault by its ID.
660 ///
661 /// This function attempts to load the `VaultRegistry` and retrieves the vault information
662 /// associated with the given `vault_id`. If the specified vault ID does not exist in the
663 /// registry, it returns an error. If found, it proceeds to open or create the vault
664 /// at the associated path.
665 ///
666 /// # Arguments
667 ///
668 /// * `vault_id` - A string slice that represents the unique identifier of the vault to open.
669 ///
670 /// # Returns
671 ///
672 /// * `Ok(Self)` - Returns an instance of the vault if the operation succeeds.
673 /// * `Err(anyhow::Error)` - Returns an error if the vault cannot be found, if the registry
674 /// fails to load, or if the vault cannot be opened or created.
675 ///
676 /// # Errors
677 ///
678 /// This function will return an error in the following cases:
679 /// - The `VaultRegistry` fails to load.
680 /// - The vault with the given `vault_id` is not found
681 pub fn open_by_id(vault_id: &str) -> Result<Self> {
682 let registry = VaultRegistry::load()?;
683 let vault_info = registry
684 .get_vault(vault_id)
685 .ok_or_else(|| eyre!("Vault '{}' not found", vault_id))?;
686
687 Self::open_or_create(Some(&vault_info.path))
688 }
689
690 /// Creates a new vault with the specified parameters.
691 ///
692 /// # Parameters
693 /// - `name`: The name of the vault to be created.
694 /// - `path`: An optional `PathBuf` specifying the directory path of the vault. If `None`, a default path is used.
695 /// - `category`: An instance of `VaultCategory` indicating the category for the vault (e.g., Personal, Business).
696 /// - `description`: An optional description providing additional details about the vault.
697 /// - `master_password`: A reference to a string containing the master password used for securing the vault.
698 ///
699 /// # Returns
700 /// - `Ok((String, Self))`: Returns a tuple containing:
701 /// - `String`: The unique ID of the newly created vault.
702 /// - `Self`: The newly initialized vault instance.
703 /// - `Err(_)`: Returns an error if the vault creation or initialization process fails.
704 ///
705 /// # Errors
706 /// This function may return an error in the following cases:
707 /// - If the `VaultRegistry` fails to load.
708 /// - If the vault cannot be created in the registry (e.g., due to duplicate names or invalid paths).
709 /// - If the vault cannot be opened or initialized (e.g., due to issues with the provided path or master password).
710 ///
711 /// # Panics
712 ///
713 /// # Notes
714 /// - The vault ID generated is unique and can be used to reference the vault in the future.
715 /// - Initializing the vault with the master password is required to securely store and access its contents.
716 #[allow(clippy::expect_fun_call)]
717 pub fn create_new_vault(
718 name: String,
719 path: Option<PathBuf>,
720 category: VaultCategory,
721 description: Option<String>,
722 master_password: &str,
723 ) -> Result<(String, Self)> {
724 let mut registry = VaultRegistry::load()?;
725 let vault_id = registry.create_vault(name, path, category, description)?;
726
727 let vault_info = registry
728 .get_vault(&vault_id)
729 .expect(format!("Cannot find vault {vault_id}").as_str());
730 let mut vault = Self::open_or_create(Some(&vault_info.path))?;
731
732 // Initialize the vault with the master password
733 vault.initialize(master_password)?;
734
735 Ok((vault_id, vault))
736 }
737
738 /// Retrieves the vault ID associated with the current instance's database path (`db_path`).
739 ///
740 /// This function searches through the `VaultRegistry` to locate a vault entry whose path matches
741 /// the `db_path` of the current instance. If a match is found, the corresponding vault ID is returned.
742 /// If no match is found, `None` is returned.
743 ///
744 /// # Returns
745 /// - `Ok(Some(String))` if a matching vault ID is found in the registry.
746 /// - `Ok(None)` if no matching vault ID is found.
747 /// - `Err` if there is an issue loading the `VaultRegistry`.
748 ///
749 /// # Errors
750 /// This function may return an error if the `VaultRegistry` cannot be loaded properly.
751 pub fn get_vault_id(&self) -> Result<Option<String>> {
752 let registry = VaultRegistry::load()?;
753 for (id, vault_info) in ®istry.vaults {
754 if vault_info.path == self.db_path {
755 return Ok(Some(id.clone()));
756 }
757 }
758 Ok(None)
759 }
760
761 /// Opens the currently active vault.
762 ///
763 /// This function retrieves the active vault from the `VaultRegistry`
764 /// and attempts to open it. If no active vault is found in the registry,
765 /// an error is returned. If a vault exists, it is either opened or created
766 /// at the path associated with the active vault.
767 ///
768 /// # Returns
769 ///
770 /// * `Ok(Self)` - If the active vault is successfully opened or created.
771 /// * `Err(anyhow::Error)` - If no active vault is found or an error occurs
772 /// during the opening or creation process.
773 ///
774 /// # Errors
775 ///
776 /// This function will return an error in the following cases:
777 /// - The `VaultRegistry` cannot be loaded.
778 /// - No active vault exists in the registry.
779 /// - An error occurs while trying to open or create the vault.
780 pub fn open_active() -> Result<Self> {
781 let registry = VaultRegistry::load()?;
782 let active_vault = registry
783 .get_active_vault()
784 .ok_or_else(|| eyre!("No active vault found"))?;
785
786 Self::open_or_create(Some(&active_vault.path))
787 }
788}
789
790// Add Clone implementation for Vault if it doesn't exist
791impl Clone for Vault {
792 #[allow(clippy::expect_used)]
793 fn clone(&self) -> Self {
794 // Note: This creates a new database connection
795 // The key material is not cloned for security reasons
796 Self {
797 db: Db::open(&self.db_path).expect("Failed to open database connection"),
798 key: None, // Don't clone the key for security
799 db_path: self.db_path.clone(),
800 }
801 }
802}
803
804fn default_db_path() -> Result<PathBuf> {
805 let base = dirs::config_dir().ok_or_else(|| eyre!("No config dir"))?;
806 let dir = base.join("chamber");
807 std::fs::create_dir_all(&dir)?;
808 Ok(dir.join("vault.sqlite3"))
809}
810
811// Rust
812#[cfg(test)]
813mod lib_module_tests {
814 #![allow(clippy::unwrap_used)]
815 #![allow(clippy::unwrap_in_result)]
816 #![allow(clippy::panic)]
817 #![allow(clippy::panic_in_result_fn)]
818 #![allow(clippy::expect_used)]
819 use super::*;
820 use std::fs;
821 use std::str::FromStr;
822
823 fn tmp_db(name: &str) -> PathBuf {
824 let now = time::OffsetDateTime::now_utc().unix_timestamp_nanos();
825 let pid = std::process::id();
826 std::env::temp_dir().join(format!("chamber_vault_lib_{name}_{pid}_{now}.sqlite3"))
827 }
828
829 #[test]
830 fn test_itemkind_as_str_and_from_str() {
831 // Round-trip known variants
832 for (kind, s) in [
833 (ItemKind::Password, "password"),
834 (ItemKind::EnvVar, "env"),
835 (ItemKind::Note, "note"),
836 (ItemKind::ApiKey, "apikey"),
837 (ItemKind::SshKey, "sshkey"),
838 (ItemKind::Certificate, "certificate"),
839 (ItemKind::Database, "database"),
840 ] {
841 assert_eq!(kind.as_str(), s);
842 // Fuzzy forms should still parse
843 assert_eq!(ItemKind::from_str(s).unwrap(), kind);
844 assert_eq!(ItemKind::from_str(&s.to_uppercase()).unwrap(), kind);
845 }
846
847 // Aliases
848 assert_eq!(ItemKind::from_str("pass").unwrap(), ItemKind::Password);
849 assert_eq!(ItemKind::from_str("pwd").unwrap(), ItemKind::Password);
850 assert_eq!(ItemKind::from_str("envvar").unwrap(), ItemKind::EnvVar);
851 assert_eq!(ItemKind::from_str("api").unwrap(), ItemKind::ApiKey);
852 assert_eq!(ItemKind::from_str("ssh").unwrap(), ItemKind::SshKey);
853 assert_eq!(ItemKind::from_str("cert").unwrap(), ItemKind::Certificate);
854 assert_eq!(ItemKind::from_str("db").unwrap(), ItemKind::Database);
855
856 // Unknown -> Note
857 assert!(ItemKind::from_str("something-else").is_err());
858 }
859
860 #[test]
861 fn test_itemkind_all_and_display_names() {
862 let all = ItemKind::all();
863 // Ensure all variants present
864 assert!(all.contains(&ItemKind::Password));
865 assert!(all.contains(&ItemKind::EnvVar));
866 assert!(all.contains(&ItemKind::Note));
867 assert!(all.contains(&ItemKind::ApiKey));
868 assert!(all.contains(&ItemKind::SshKey));
869 assert!(all.contains(&ItemKind::Certificate));
870 assert!(all.contains(&ItemKind::Database));
871
872 // Display names are human-friendly and stable
873 assert_eq!(ItemKind::Password.display_name(), "Password");
874 assert_eq!(ItemKind::EnvVar.display_name(), "Environment Variable");
875 assert_eq!(ItemKind::Note.display_name(), "Note");
876 assert_eq!(ItemKind::ApiKey.display_name(), "API Key");
877 assert_eq!(ItemKind::SshKey.display_name(), "SSH Key");
878 assert_eq!(ItemKind::Certificate.display_name(), "Certificate");
879 assert_eq!(ItemKind::Database.display_name(), "Database Connection");
880 }
881
882 #[test]
883 fn test_vault_initialize_and_is_initialized() -> Result<()> {
884 let path = tmp_db("init");
885 let mut v = Vault::open_or_create(Some(&path))?;
886 assert!(!v.is_initialized());
887
888 v.initialize("master-1")?;
889 assert!(v.is_initialized());
890
891 // Initialize again should be no-op and not fail
892 v.initialize("master-1")?;
893 assert!(v.is_initialized());
894
895 fs::remove_file(path).ok();
896 Ok(())
897 }
898
899 #[test]
900 fn test_vault_unlock_success_and_failure_paths() -> Result<()> {
901 let path = tmp_db("unlock");
902 let mut v = Vault::open_or_create(Some(&path))?;
903 v.initialize("secret")?;
904
905 // Good master unlocks
906 v.unlock("secret")?;
907
908 // Wrong master returns error
909 let mut v2 = Vault::open_or_create(Some(&path))?;
910 let err = v2.unlock("wrong").unwrap_err().to_string();
911 assert!(!err.is_empty());
912
913 fs::remove_file(path).ok();
914 Ok(())
915 }
916
917 #[test]
918 fn test_vault_create_list_get_update_delete() -> Result<()> {
919 let path = tmp_db("crud");
920 let mut v = Vault::open_or_create(Some(&path))?;
921 v.initialize("m")?;
922 v.unlock("m")?;
923
924 // Initially empty
925 let items = v.list_items()?;
926 assert!(items.is_empty());
927
928 // Create a few items
929 v.create_item(&NewItem {
930 name: "alpha".into(),
931 kind: ItemKind::Password,
932 value: "A1".into(),
933 })?;
934 v.create_item(&NewItem {
935 name: "beta".into(),
936 kind: ItemKind::EnvVar,
937 value: "B2".into(),
938 })?;
939
940 // List sorts by name
941 let items = v.list_items()?;
942 assert_eq!(items.len(), 2);
943 assert_eq!(items[0].name, "alpha");
944 assert_eq!(items[1].name, "beta");
945
946 // Get by name
947 let got = v.get_item_by_name("alpha")?.expect("exists");
948 assert_eq!(got.value, "A1");
949 assert_eq!(got.kind, ItemKind::Password);
950
951 // Update item value
952 v.update_item(got.id, "A1-updated")?;
953 let got2 = v.get_item_by_name("alpha")?.expect("exists");
954 assert_eq!(got2.value, "A1-updated");
955 assert!(got2.updated_at >= got.updated_at);
956
957 // Delete beta
958 let beta = v.get_item_by_name("beta")?.expect("exists");
959 v.delete_item(beta.id)?;
960 let items_after = v.list_items()?;
961 assert_eq!(items_after.len(), 1);
962 assert_eq!(items_after[0].name, "alpha");
963
964 fs::remove_file(path).ok();
965 Ok(())
966 }
967
968 #[test]
969 fn test_vault_persistence_across_reopen() -> Result<()> {
970 let path = tmp_db("persist");
971 {
972 let mut v = Vault::open_or_create(Some(&path))?;
973 v.initialize("k")?;
974 v.unlock("k")?;
975 v.create_item(&NewItem {
976 name: "one".into(),
977 kind: ItemKind::Note,
978 value: "first".into(),
979 })?;
980 }
981
982 // Reopen and unlock, item should be there
983 {
984 let mut v = Vault::open_or_create(Some(&path))?;
985 v.unlock("k")?;
986 let items = v.list_items()?;
987 assert_eq!(items.len(), 1);
988 assert_eq!(items[0].name, "one");
989 assert_eq!(items[0].value, "first");
990 }
991
992 fs::remove_file(path).ok();
993 Ok(())
994 }
995
996 #[test]
997 fn test_change_master_key_preserves_items_and_allows_new_unlock() -> Result<()> {
998 let path = tmp_db("change_key");
999
1000 // Initialize, add items
1001 let mut v = Vault::open_or_create(Some(&path))?;
1002 v.initialize("old-master")?;
1003 v.unlock("old-master")?;
1004 v.create_item(&NewItem {
1005 name: "svc".into(),
1006 kind: ItemKind::ApiKey,
1007 value: "token-123".into(),
1008 })?;
1009
1010 // Change master key
1011 v.change_master_key("old-master", "new-master")?;
1012
1013 // Old master should no longer unlock; new one should
1014 let mut v2 = Vault::open_or_create(Some(&path))?;
1015 let err = v2.unlock("old-master").unwrap_err().to_string();
1016 assert!(!err.is_empty());
1017
1018 v2.unlock("new-master")?;
1019 let items = v2.list_items()?;
1020 assert_eq!(items.len(), 1);
1021 assert_eq!(items[0].name, "svc");
1022 assert_eq!(items[0].value, "token-123");
1023
1024 fs::remove_file(path).ok();
1025 Ok(())
1026 }
1027
1028 #[test]
1029 fn test_get_item_by_name_not_found() -> Result<()> {
1030 let path = tmp_db("get_missing");
1031 let mut v = Vault::open_or_create(Some(&path))?;
1032 v.initialize("m")?;
1033 v.unlock("m")?;
1034 v.create_item(&NewItem {
1035 name: "exists".into(),
1036 kind: ItemKind::Note,
1037 value: "v".into(),
1038 })?;
1039
1040 assert!(v.get_item_by_name("nope")?.is_none());
1041
1042 fs::remove_file(path).ok();
1043 Ok(())
1044 }
1045}