Skip to main content

walletkit_db/
vault.rs

1//! Encrypted vault: opens an encrypted database with a caller-supplied
2//! schema and hands out the underlying [`Connection`].
3//!
4//! `SQLite` handles cross-process writer serialization itself in WAL mode
5//! (which `cipher::open_encrypted` configures), so `Vault` does not wrap
6//! mutations in a flock. Where flock IS load-bearing — the first-install
7//! bootstrap race in [`crate::init_or_open_envelope_key`] and any
8//! file-level orchestration on top of plaintext export/import — callers
9//! acquire a [`crate::Lock`] explicitly. Keeping the lock out of `Vault`
10//! avoids belt-and-suspenders flock acquisitions that don't add safety
11//! beyond what `SQLite`'s own locking already provides.
12
13use std::path::Path;
14
15use secrecy::SecretBox;
16
17use crate::error::{StoreError, StoreResult};
18use crate::sqlite::{cipher, Connection, DbResult};
19
20/// Open encrypted database wrapper.
21///
22/// Exposes the underlying [`Connection`] via [`Vault::connection`].
23#[derive(Debug)]
24pub struct Vault {
25    conn: Connection,
26}
27
28impl Vault {
29    /// Opens (or creates) the encrypted database at `db_path`, runs
30    /// `ensure_schema`, and verifies integrity.
31    ///
32    /// `ensure_schema` runs after the database is opened and keyed but
33    /// before the integrity check.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`StoreError::Db`] if open / key / schema fails or
38    /// [`StoreError::IntegrityCheckFailed`] on corruption.
39    pub fn open<F>(
40        db_path: &Path,
41        key: &SecretBox<[u8; 32]>,
42        ensure_schema: F,
43    ) -> StoreResult<Self>
44    where
45        F: FnOnce(&Connection) -> DbResult<()>,
46    {
47        let conn = cipher::open_encrypted(db_path, key, false)?;
48        ensure_schema(&conn)?;
49        if !cipher::integrity_check(&conn)? {
50            return Err(StoreError::IntegrityCheckFailed(
51                "integrity_check failed".to_string(),
52            ));
53        }
54        Ok(Self { conn })
55    }
56
57    /// Borrows the underlying connection.
58    #[must_use]
59    pub const fn connection(&self) -> &Connection {
60        &self.conn
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::Vault;
67    use crate::blobs;
68    use crate::error::StoreError;
69    use crate::test_utils::init_sqlite;
70    use secrecy::SecretBox;
71
72    #[test]
73    #[cfg(not(target_arch = "wasm32"))]
74    fn test_vault_open_runs_schema_callback() {
75        init_sqlite();
76        let dir = tempfile::tempdir().expect("create temp dir");
77        let db_path = dir.path().join("vault.sqlite");
78        let key = SecretBox::init_with(|| [0x42u8; 32]);
79
80        let vault = Vault::open(&db_path, &key, |conn| {
81            blobs::ensure_schema(conn)?;
82            conn.execute_batch(
83                "CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY);",
84            )
85        })
86        .expect("open vault");
87
88        let cid = blobs::put(vault.connection(), 7, b"payload", 1000).expect("put");
89        let bytes = blobs::get(vault.connection(), &cid)
90            .expect("get")
91            .expect("present");
92        assert_eq!(bytes, b"payload");
93    }
94
95    #[test]
96    #[cfg(not(target_arch = "wasm32"))]
97    fn test_vault_open_rejects_wrong_key() {
98        init_sqlite();
99        let dir = tempfile::tempdir().expect("create temp dir");
100        let db_path = dir.path().join("vault.sqlite");
101        let key = SecretBox::init_with(|| [0x11u8; 32]);
102        let _ =
103            Vault::open(&db_path, &key, blobs::ensure_schema).expect("create vault");
104        let wrong = SecretBox::init_with(|| [0x22u8; 32]);
105        let err = Vault::open(&db_path, &wrong, |_| Ok(())).expect_err("wrong key");
106        assert!(matches!(err, StoreError::Db(_)));
107    }
108}