Skip to main content

vector_core/db/
settings.rs

1//! Settings key-value store operations.
2
3/// Get a SQL setting by key.
4pub fn get_sql_setting(key: String) -> Result<Option<String>, String> {
5    let conn = super::get_db_connection_guard_static()?;
6    let result: Option<String> = conn.query_row(
7        "SELECT value FROM settings WHERE key = ?1",
8        rusqlite::params![key],
9        |row| row.get(0),
10    ).ok();
11    Ok(result)
12}
13
14/// Set a SQL setting key-value pair.
15pub fn set_sql_setting(key: String, value: String) -> Result<(), String> {
16    let conn = super::get_write_connection_guard_static()?;
17    conn.execute(
18        "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
19        rusqlite::params![key, value],
20    ).map_err(|e| format!("Failed to set setting: {}", e))?;
21    Ok(())
22}
23
24/// Remove a setting by key.
25pub fn remove_setting(key: &str) -> Result<(), String> {
26    let conn = super::get_write_connection_guard_static()?;
27    conn.execute("DELETE FROM settings WHERE key = ?1", rusqlite::params![key])
28        .map_err(|e| format!("Failed to remove setting: {}", e))?;
29    Ok(())
30}
31
32/// Get the stored private key (bech32 nsec).
33pub fn get_pkey() -> Result<Option<String>, String> {
34    let conn = super::get_db_connection_guard_static()?;
35    Ok(conn.query_row(
36        "SELECT value FROM settings WHERE key = 'pkey'",
37        [],
38        |row| row.get(0),
39    ).ok())
40}
41
42/// Set the stored private key.
43pub fn set_pkey(pkey: &str) -> Result<(), String> {
44    let conn = super::get_write_connection_guard_static()?;
45    conn.execute(
46        "INSERT OR REPLACE INTO settings (key, value) VALUES ('pkey', ?1)",
47        rusqlite::params![pkey],
48    ).map_err(|e| format!("Failed to set pkey: {}", e))?;
49    Ok(())
50}
51
52/// Get the stored seed phrase (may be encrypted).
53pub fn get_seed() -> Result<Option<String>, String> {
54    let conn = super::get_db_connection_guard_static()?;
55    Ok(conn.query_row(
56        "SELECT value FROM settings WHERE key = 'seed'",
57        [],
58        |row| row.get(0),
59    ).ok())
60}
61
62/// Set the seed phrase (should be encrypted before calling).
63pub fn set_seed(seed: &str) -> Result<(), String> {
64    let conn = super::get_write_connection_guard_static()?;
65    conn.execute(
66        "INSERT OR REPLACE INTO settings (key, value) VALUES ('seed', ?1)",
67        rusqlite::params![seed],
68    ).map_err(|e| format!("Failed to set seed: {}", e))?;
69    Ok(())
70}
71
72/// Atomically commit the four settings written during new-account setup:
73/// the (possibly-encrypted) pkey, the `encryption_enabled` flag, the
74/// `security_type` (only when encrypted), and the (already-encrypted) seed
75/// phrase. Wrapping these in a single transaction makes the new-account
76/// flow crash-safe: either all four land or none do. The previous design
77/// wrote them through four separate `set_sql_setting` calls, which left a
78/// window where pkey was persisted but `encryption_enabled` was not — the
79/// next boot would then mis-interpret the encrypted blob as plaintext nsec
80/// and brick the account.
81///
82/// `security_type` is `Some(_)` for encrypted accounts and `None` for
83/// skip-encryption flows (passing `Some("")` would write an empty string,
84/// which `resolve_encryption_enabled` treats as encrypted — not what we
85/// want for the skip path).
86pub fn commit_account_setup(
87    pkey: &str,
88    encryption_enabled: bool,
89    security_type: Option<&str>,
90    encrypted_seed: Option<&str>,
91) -> Result<(), String> {
92    let mut conn = super::get_write_connection_guard_static()?;
93    let tx = conn.transaction()
94        .map_err(|e| format!("Failed to begin tx: {}", e))?;
95    tx.execute(
96        "INSERT OR REPLACE INTO settings (key, value) VALUES ('pkey', ?1)",
97        rusqlite::params![pkey],
98    ).map_err(|e| format!("Failed to set pkey: {}", e))?;
99    tx.execute(
100        "INSERT OR REPLACE INTO settings (key, value) VALUES ('encryption_enabled', ?1)",
101        rusqlite::params![if encryption_enabled { "true" } else { "false" }],
102    ).map_err(|e| format!("Failed to set encryption_enabled: {}", e))?;
103    if let Some(st) = security_type {
104        tx.execute(
105            "INSERT OR REPLACE INTO settings (key, value) VALUES ('security_type', ?1)",
106            rusqlite::params![st],
107        ).map_err(|e| format!("Failed to set security_type: {}", e))?;
108    } else {
109        // Skip path: ensure no stale security_type from a previous setup
110        // attempt lingers (would mis-route `resolve_encryption_enabled`).
111        tx.execute(
112            "DELETE FROM settings WHERE key = 'security_type'",
113            [],
114        ).map_err(|e| format!("Failed to clear security_type: {}", e))?;
115    }
116    if let Some(seed) = encrypted_seed {
117        tx.execute(
118            "INSERT OR REPLACE INTO settings (key, value) VALUES ('seed', ?1)",
119            rusqlite::params![seed],
120        ).map_err(|e| format!("Failed to set seed: {}", e))?;
121    }
122    // Write an explicit signer_type='local' row so the post-migration
123    // invariant "every account has a discriminator on disk" holds for
124    // freshly-created local accounts too (the migration only backfills
125    // pre-existing rows).
126    tx.execute(
127        "INSERT OR REPLACE INTO settings (key, value) VALUES ('signer_type', 'local')",
128        [],
129    ).map_err(|e| format!("Failed to set signer_type: {}", e))?;
130    tx.commit().map_err(|e| format!("Failed to commit tx: {}", e))?;
131    Ok(())
132}
133
134// ============================================================================
135// NIP-46 remote-signer settings (added in migration 27)
136// ============================================================================
137//
138// Three keys back the bunker login flow:
139//   - `signer_type`         — "local" | "bunker"
140//   - `bunker_url`          — `bunker://...` URI, encrypted-at-rest when the
141//                             account uses pin/pass encryption (same path as
142//                             pkey). Contains the connection secret.
143//   - `bunker_remote_pubkey`— signer pubkey, plaintext (routing info only).
144//
145// The `bunker_url` getter/setter is `async` because `maybe_encrypt`/
146// `maybe_decrypt` await on Argon2id key derivation when the user is logged
147// into an encrypted account. The two plaintext fields stay sync.
148
149/// Read the active signer kind from settings. Missing rows pre-date migration
150/// 27 and are treated as `"local"` so pre-NIP-46 accounts behave unchanged.
151pub fn get_signer_type() -> Result<String, String> {
152    let conn = super::get_db_connection_guard_static()?;
153    Ok(conn.query_row(
154        "SELECT value FROM settings WHERE key = 'signer_type'",
155        [],
156        |row| row.get::<_, String>(0),
157    ).unwrap_or_else(|_| "local".to_string()))
158}
159
160/// Persist the signer kind. Accepts the discriminator's `as_setting_str()`
161/// form ("local" or "bunker"); other values are accepted but `get_signer_type`
162/// will treat them as `local` downstream.
163pub fn set_signer_type(value: &str) -> Result<(), String> {
164    let conn = super::get_write_connection_guard_static()?;
165    conn.execute(
166        "INSERT OR REPLACE INTO settings (key, value) VALUES ('signer_type', ?1)",
167        rusqlite::params![value],
168    ).map_err(|e| format!("Failed to set signer_type: {}", e))?;
169    Ok(())
170}
171
172/// Read the `bunker://` URL, decrypting it if the account uses encryption.
173/// Returns `Ok(None)` for local accounts (no row), or when decryption fails
174/// against an obviously-encrypted blob (likely the user hasn't unlocked yet).
175pub async fn get_bunker_url() -> Result<Option<String>, String> {
176    let raw: Option<String> = {
177        let conn = super::get_db_connection_guard_static()?;
178        conn.query_row(
179            "SELECT value FROM settings WHERE key = 'bunker_url'",
180            [],
181            |row| row.get::<_, String>(0),
182        ).ok()
183    };
184    match raw {
185        Some(s) => match crate::crypto::maybe_decrypt(s).await {
186            Ok(plain) => Ok(Some(plain)),
187            Err(_) => Err("bunker_url decryption failed (account locked?)".into()),
188        },
189        None => Ok(None),
190    }
191}
192
193/// Persist the `bunker://` URL, encrypting if the account uses encryption.
194/// The plaintext form is never written to disk for encrypted accounts.
195pub async fn set_bunker_url(url: &str) -> Result<(), String> {
196    let stored = crate::crypto::maybe_encrypt(url.to_string()).await;
197    let conn = super::get_write_connection_guard_static()?;
198    conn.execute(
199        "INSERT OR REPLACE INTO settings (key, value) VALUES ('bunker_url', ?1)",
200        rusqlite::params![stored],
201    ).map_err(|e| format!("Failed to set bunker_url: {}", e))?;
202    Ok(())
203}
204
205/// Read the cached remote signer pubkey (hex). Plaintext on disk — it's
206/// public-key material with no secrecy implications, and keeping it readable
207/// before unlock lets the UI display "Connected to <pubkey>" on the locked
208/// account picker without prompting for a password.
209pub fn get_bunker_remote_pubkey() -> Result<Option<String>, String> {
210    let conn = super::get_db_connection_guard_static()?;
211    Ok(conn.query_row(
212        "SELECT value FROM settings WHERE key = 'bunker_remote_pubkey'",
213        [],
214        |row| row.get::<_, String>(0),
215    ).ok())
216}
217
218/// Persist the cached remote signer pubkey (hex form). Updated after each
219/// successful bunker bootstrap — the bootstrap response carries the canonical
220/// pubkey, which may differ from any user-supplied form.
221pub fn set_bunker_remote_pubkey(pubkey_hex: &str) -> Result<(), String> {
222    let conn = super::get_write_connection_guard_static()?;
223    conn.execute(
224        "INSERT OR REPLACE INTO settings (key, value) VALUES ('bunker_remote_pubkey', ?1)",
225        rusqlite::params![pubkey_hex],
226    ).map_err(|e| format!("Failed to set bunker_remote_pubkey: {}", e))?;
227    Ok(())
228}
229
230/// Atomically commit the four settings written during *bunker* new-account
231/// setup: the (possibly-encrypted) client keypair pkey, `encryption_enabled`,
232/// `security_type`, plus `signer_type='bunker'`, the (possibly-encrypted)
233/// `bunker_url`, and the plaintext `bunker_remote_pubkey`. Wraps the whole
234/// commit in a transaction for the same reason as `commit_account_setup` —
235/// a half-written bunker account would brick login.
236///
237/// The seed is intentionally absent: bunker accounts have no local mnemonic
238/// (the user's nsec lives on the remote signer; we only hold a client keypair
239/// with no recovery phrase).
240pub fn commit_bunker_account_setup(
241    pkey: &str,
242    encryption_enabled: bool,
243    security_type: Option<&str>,
244    bunker_url_stored: &str,
245    bunker_remote_pubkey_hex: &str,
246) -> Result<(), String> {
247    let mut conn = super::get_write_connection_guard_static()?;
248    let tx = conn.transaction()
249        .map_err(|e| format!("Failed to begin tx: {}", e))?;
250    tx.execute(
251        "INSERT OR REPLACE INTO settings (key, value) VALUES ('pkey', ?1)",
252        rusqlite::params![pkey],
253    ).map_err(|e| format!("Failed to set pkey: {}", e))?;
254    tx.execute(
255        "INSERT OR REPLACE INTO settings (key, value) VALUES ('encryption_enabled', ?1)",
256        rusqlite::params![if encryption_enabled { "true" } else { "false" }],
257    ).map_err(|e| format!("Failed to set encryption_enabled: {}", e))?;
258    if let Some(st) = security_type {
259        tx.execute(
260            "INSERT OR REPLACE INTO settings (key, value) VALUES ('security_type', ?1)",
261            rusqlite::params![st],
262        ).map_err(|e| format!("Failed to set security_type: {}", e))?;
263    } else {
264        tx.execute(
265            "DELETE FROM settings WHERE key = 'security_type'",
266            [],
267        ).map_err(|e| format!("Failed to clear security_type: {}", e))?;
268    }
269    tx.execute(
270        "INSERT OR REPLACE INTO settings (key, value) VALUES ('signer_type', 'bunker')",
271        [],
272    ).map_err(|e| format!("Failed to set signer_type: {}", e))?;
273    tx.execute(
274        "INSERT OR REPLACE INTO settings (key, value) VALUES ('bunker_url', ?1)",
275        rusqlite::params![bunker_url_stored],
276    ).map_err(|e| format!("Failed to set bunker_url: {}", e))?;
277    tx.execute(
278        "INSERT OR REPLACE INTO settings (key, value) VALUES ('bunker_remote_pubkey', ?1)",
279        rusqlite::params![bunker_remote_pubkey_hex],
280    ).map_err(|e| format!("Failed to set bunker_remote_pubkey: {}", e))?;
281    // Drop any stale seed from a previous local-account setup on this DB.
282    tx.execute("DELETE FROM settings WHERE key = 'seed'", [])
283        .map_err(|e| format!("Failed to clear stale seed: {}", e))?;
284    tx.commit().map_err(|e| format!("Failed to commit tx: {}", e))?;
285    Ok(())
286}