Skip to main content

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(&current_derived, &wrapped_old, Some(&verifier_old))
598            .map_err(|_| eyre!("Invalid current master key"))?;
599        let vault_key = unwrap_vault_key(&current_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 &registry.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}