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/// Monotonically advance a numeric setting in ONE statement — the stored
25/// value only ever grows. For reconcile cursors and similar floors, where a
26/// read-modify-write window would let a stale or concurrent writer regress
27/// the value.
28pub fn advance_u64_setting(key: String, value: u64) -> Result<(), String> {
29    let conn = super::get_write_connection_guard_static()?;
30    conn.execute(
31        "INSERT INTO settings (key, value) VALUES (?1, ?2)
32         ON CONFLICT(key) DO UPDATE SET value = excluded.value
33         WHERE CAST(excluded.value AS INTEGER) > CAST(value AS INTEGER)",
34        rusqlite::params![key, value.to_string()],
35    ).map_err(|e| format!("Failed to advance setting: {}", e))?;
36    Ok(())
37}
38
39/// Remove a setting by key.
40pub fn remove_setting(key: &str) -> Result<(), String> {
41    let conn = super::get_write_connection_guard_static()?;
42    conn.execute("DELETE FROM settings WHERE key = ?1", rusqlite::params![key])
43        .map_err(|e| format!("Failed to remove setting: {}", e))?;
44    Ok(())
45}
46
47/// Get the stored private key (bech32 nsec).
48pub fn get_pkey() -> Result<Option<String>, String> {
49    let conn = super::get_db_connection_guard_static()?;
50    Ok(conn.query_row(
51        "SELECT value FROM settings WHERE key = 'pkey'",
52        [],
53        |row| row.get(0),
54    ).ok())
55}
56
57/// Set the stored private key.
58pub fn set_pkey(pkey: &str) -> Result<(), String> {
59    let conn = super::get_write_connection_guard_static()?;
60    conn.execute(
61        "INSERT OR REPLACE INTO settings (key, value) VALUES ('pkey', ?1)",
62        rusqlite::params![pkey],
63    ).map_err(|e| format!("Failed to set pkey: {}", e))?;
64    Ok(())
65}
66
67/// Get the stored seed phrase (may be encrypted).
68pub fn get_seed() -> Result<Option<String>, String> {
69    let conn = super::get_db_connection_guard_static()?;
70    Ok(conn.query_row(
71        "SELECT value FROM settings WHERE key = 'seed'",
72        [],
73        |row| row.get(0),
74    ).ok())
75}
76
77/// Set the seed phrase (should be encrypted before calling).
78pub fn set_seed(seed: &str) -> Result<(), String> {
79    let conn = super::get_write_connection_guard_static()?;
80    conn.execute(
81        "INSERT OR REPLACE INTO settings (key, value) VALUES ('seed', ?1)",
82        rusqlite::params![seed],
83    ).map_err(|e| format!("Failed to set seed: {}", e))?;
84    Ok(())
85}
86
87/// Atomically commit the four settings written during new-account setup:
88/// the (possibly-encrypted) pkey, the `encryption_enabled` flag, the
89/// `security_type` (only when encrypted), and the (already-encrypted) seed
90/// phrase. Wrapping these in a single transaction makes the new-account
91/// flow crash-safe: either all four land or none do. The previous design
92/// wrote them through four separate `set_sql_setting` calls, which left a
93/// window where pkey was persisted but `encryption_enabled` was not — the
94/// next boot would then mis-interpret the encrypted blob as plaintext nsec
95/// and brick the account.
96///
97/// `security_type` is `Some(_)` for encrypted accounts and `None` for
98/// skip-encryption flows (passing `Some("")` would write an empty string,
99/// which `resolve_encryption_enabled` treats as encrypted — not what we
100/// want for the skip path).
101pub fn commit_account_setup(
102    pkey: &str,
103    encryption_enabled: bool,
104    security_type: Option<&str>,
105    encrypted_seed: Option<&str>,
106) -> Result<(), String> {
107    let mut conn = super::get_write_connection_guard_static()?;
108    let tx = conn.transaction()
109        .map_err(|e| format!("Failed to begin tx: {}", e))?;
110    tx.execute(
111        "INSERT OR REPLACE INTO settings (key, value) VALUES ('pkey', ?1)",
112        rusqlite::params![pkey],
113    ).map_err(|e| format!("Failed to set pkey: {}", e))?;
114    tx.execute(
115        "INSERT OR REPLACE INTO settings (key, value) VALUES ('encryption_enabled', ?1)",
116        rusqlite::params![if encryption_enabled { "true" } else { "false" }],
117    ).map_err(|e| format!("Failed to set encryption_enabled: {}", e))?;
118    if let Some(st) = security_type {
119        tx.execute(
120            "INSERT OR REPLACE INTO settings (key, value) VALUES ('security_type', ?1)",
121            rusqlite::params![st],
122        ).map_err(|e| format!("Failed to set security_type: {}", e))?;
123    } else {
124        // Skip path: ensure no stale security_type from a previous setup
125        // attempt lingers (would mis-route `resolve_encryption_enabled`).
126        tx.execute(
127            "DELETE FROM settings WHERE key = 'security_type'",
128            [],
129        ).map_err(|e| format!("Failed to clear security_type: {}", e))?;
130    }
131    if let Some(seed) = encrypted_seed {
132        tx.execute(
133            "INSERT OR REPLACE INTO settings (key, value) VALUES ('seed', ?1)",
134            rusqlite::params![seed],
135        ).map_err(|e| format!("Failed to set seed: {}", e))?;
136    }
137    // Write an explicit signer_type='local' row so the post-migration
138    // invariant "every account has a discriminator on disk" holds for
139    // freshly-created local accounts too (the migration only backfills
140    // pre-existing rows).
141    tx.execute(
142        "INSERT OR REPLACE INTO settings (key, value) VALUES ('signer_type', 'local')",
143        [],
144    ).map_err(|e| format!("Failed to set signer_type: {}", e))?;
145    tx.commit().map_err(|e| format!("Failed to commit tx: {}", e))?;
146    Ok(())
147}
148
149// ============================================================================
150// NIP-46 remote-signer settings (added in migration 27)
151// ============================================================================
152//
153// Three keys back the bunker login flow:
154//   - `signer_type`         — "local" | "bunker"
155//   - `bunker_url`          — `bunker://...` URI, encrypted-at-rest when the
156//                             account uses pin/pass encryption (same path as
157//                             pkey). Contains the connection secret.
158//   - `bunker_remote_pubkey`— signer pubkey, plaintext (routing info only).
159//
160// The `bunker_url` getter/setter is `async` because `maybe_encrypt`/
161// `maybe_decrypt` await on Argon2id key derivation when the user is logged
162// into an encrypted account. The two plaintext fields stay sync.
163
164/// Read the active signer kind from settings. Missing rows pre-date migration
165/// 27 and are treated as `"local"` so pre-NIP-46 accounts behave unchanged.
166pub fn get_signer_type() -> Result<String, String> {
167    let conn = super::get_db_connection_guard_static()?;
168    Ok(conn.query_row(
169        "SELECT value FROM settings WHERE key = 'signer_type'",
170        [],
171        |row| row.get::<_, String>(0),
172    ).unwrap_or_else(|_| "local".to_string()))
173}
174
175/// Persist the signer kind. Accepts the discriminator's `as_setting_str()`
176/// form ("local" or "bunker"); other values are accepted but `get_signer_type`
177/// will treat them as `local` downstream.
178pub fn set_signer_type(value: &str) -> Result<(), String> {
179    let conn = super::get_write_connection_guard_static()?;
180    conn.execute(
181        "INSERT OR REPLACE INTO settings (key, value) VALUES ('signer_type', ?1)",
182        rusqlite::params![value],
183    ).map_err(|e| format!("Failed to set signer_type: {}", e))?;
184    Ok(())
185}
186
187/// Read the `bunker://` URL, decrypting it if the account uses encryption.
188/// Returns `Ok(None)` for local accounts (no row), or when decryption fails
189/// against an obviously-encrypted blob (likely the user hasn't unlocked yet).
190pub async fn get_bunker_url() -> Result<Option<String>, String> {
191    let raw: Option<String> = {
192        let conn = super::get_db_connection_guard_static()?;
193        conn.query_row(
194            "SELECT value FROM settings WHERE key = 'bunker_url'",
195            [],
196            |row| row.get::<_, String>(0),
197        ).ok()
198    };
199    match raw {
200        Some(s) => match crate::crypto::maybe_decrypt(s).await {
201            Ok(plain) => Ok(Some(plain)),
202            Err(_) => Err("bunker_url decryption failed (account locked?)".into()),
203        },
204        None => Ok(None),
205    }
206}
207
208/// Persist the `bunker://` URL, encrypting if the account uses encryption.
209/// The plaintext form is never written to disk for encrypted accounts.
210pub async fn set_bunker_url(url: &str) -> Result<(), String> {
211    let stored = crate::crypto::maybe_encrypt(url.to_string()).await;
212    let conn = super::get_write_connection_guard_static()?;
213    conn.execute(
214        "INSERT OR REPLACE INTO settings (key, value) VALUES ('bunker_url', ?1)",
215        rusqlite::params![stored],
216    ).map_err(|e| format!("Failed to set bunker_url: {}", e))?;
217    Ok(())
218}
219
220/// Read the cached remote signer pubkey (hex). Plaintext on disk — it's
221/// public-key material with no secrecy implications, and keeping it readable
222/// before unlock lets the UI display "Connected to <pubkey>" on the locked
223/// account picker without prompting for a password.
224pub fn get_bunker_remote_pubkey() -> Result<Option<String>, String> {
225    let conn = super::get_db_connection_guard_static()?;
226    Ok(conn.query_row(
227        "SELECT value FROM settings WHERE key = 'bunker_remote_pubkey'",
228        [],
229        |row| row.get::<_, String>(0),
230    ).ok())
231}
232
233/// Persist the cached remote signer pubkey (hex form). Updated after each
234/// successful bunker bootstrap — the bootstrap response carries the canonical
235/// pubkey, which may differ from any user-supplied form.
236pub fn set_bunker_remote_pubkey(pubkey_hex: &str) -> Result<(), String> {
237    let conn = super::get_write_connection_guard_static()?;
238    conn.execute(
239        "INSERT OR REPLACE INTO settings (key, value) VALUES ('bunker_remote_pubkey', ?1)",
240        rusqlite::params![pubkey_hex],
241    ).map_err(|e| format!("Failed to set bunker_remote_pubkey: {}", e))?;
242    Ok(())
243}
244
245/// Atomically commit the four settings written during *bunker* new-account
246/// setup: the (possibly-encrypted) client keypair pkey, `encryption_enabled`,
247/// `security_type`, plus `signer_type='bunker'`, the (possibly-encrypted)
248/// `bunker_url`, and the plaintext `bunker_remote_pubkey`. Wraps the whole
249/// commit in a transaction for the same reason as `commit_account_setup` —
250/// a half-written bunker account would brick login.
251///
252/// The seed is intentionally absent: bunker accounts have no local mnemonic
253/// (the user's nsec lives on the remote signer; we only hold a client keypair
254/// with no recovery phrase).
255pub fn commit_bunker_account_setup(
256    pkey: &str,
257    encryption_enabled: bool,
258    security_type: Option<&str>,
259    bunker_url_stored: &str,
260    bunker_remote_pubkey_hex: &str,
261) -> Result<(), String> {
262    let mut conn = super::get_write_connection_guard_static()?;
263    let tx = conn.transaction()
264        .map_err(|e| format!("Failed to begin tx: {}", e))?;
265    tx.execute(
266        "INSERT OR REPLACE INTO settings (key, value) VALUES ('pkey', ?1)",
267        rusqlite::params![pkey],
268    ).map_err(|e| format!("Failed to set pkey: {}", e))?;
269    tx.execute(
270        "INSERT OR REPLACE INTO settings (key, value) VALUES ('encryption_enabled', ?1)",
271        rusqlite::params![if encryption_enabled { "true" } else { "false" }],
272    ).map_err(|e| format!("Failed to set encryption_enabled: {}", e))?;
273    if let Some(st) = security_type {
274        tx.execute(
275            "INSERT OR REPLACE INTO settings (key, value) VALUES ('security_type', ?1)",
276            rusqlite::params![st],
277        ).map_err(|e| format!("Failed to set security_type: {}", e))?;
278    } else {
279        tx.execute(
280            "DELETE FROM settings WHERE key = 'security_type'",
281            [],
282        ).map_err(|e| format!("Failed to clear security_type: {}", e))?;
283    }
284    tx.execute(
285        "INSERT OR REPLACE INTO settings (key, value) VALUES ('signer_type', 'bunker')",
286        [],
287    ).map_err(|e| format!("Failed to set signer_type: {}", e))?;
288    tx.execute(
289        "INSERT OR REPLACE INTO settings (key, value) VALUES ('bunker_url', ?1)",
290        rusqlite::params![bunker_url_stored],
291    ).map_err(|e| format!("Failed to set bunker_url: {}", e))?;
292    tx.execute(
293        "INSERT OR REPLACE INTO settings (key, value) VALUES ('bunker_remote_pubkey', ?1)",
294        rusqlite::params![bunker_remote_pubkey_hex],
295    ).map_err(|e| format!("Failed to set bunker_remote_pubkey: {}", e))?;
296    // Drop any stale seed from a previous local-account setup on this DB.
297    tx.execute("DELETE FROM settings WHERE key = 'seed'", [])
298        .map_err(|e| format!("Failed to clear stale seed: {}", e))?;
299    tx.commit().map_err(|e| format!("Failed to commit tx: {}", e))?;
300    Ok(())
301}
302
303// ============================================================================
304// NIP-55 offline-signer settings
305// ============================================================================
306//
307// A NIP-55 (Amber) account keeps NOTHING secret on this device — not even the
308// client keypair a bunker account holds. So there is no `pkey` row at all; the
309// only account-identifying material is public:
310//   - `signer_type`         — "nip55"
311//   - `nip55_user_pubkey`   — identity pubkey hex, plaintext (public material;
312//                             lets the locked account picker render the npub
313//                             pre-unlock, same as `bunker_remote_pubkey`).
314//   - `nip55_signer_package`— the signer app's Android package name, plaintext.
315//
316// `encryption_enabled`/`security_type` still apply, but they gate ONLY the
317// local at-rest DB encryption (messages Vector stores) — orthogonal to signing,
318// which never touches this device's storage.
319
320/// Read the cached NIP-55 identity pubkey (hex). Plaintext on disk (public-key
321/// material); readable before unlock so the account picker can show the npub.
322pub fn get_nip55_user_pubkey() -> Result<Option<String>, String> {
323    let conn = super::get_db_connection_guard_static()?;
324    Ok(conn.query_row(
325        "SELECT value FROM settings WHERE key = 'nip55_user_pubkey'",
326        [],
327        |row| row.get::<_, String>(0),
328    ).ok())
329}
330
331/// Persist the NIP-55 identity pubkey (hex).
332pub fn set_nip55_user_pubkey(pubkey_hex: &str) -> Result<(), String> {
333    let conn = super::get_write_connection_guard_static()?;
334    conn.execute(
335        "INSERT OR REPLACE INTO settings (key, value) VALUES ('nip55_user_pubkey', ?1)",
336        rusqlite::params![pubkey_hex],
337    ).map_err(|e| format!("Failed to set nip55_user_pubkey: {}", e))?;
338    Ok(())
339}
340
341/// Read the paired signer app's Android package name. Pinned on every intent +
342/// as the ContentResolver authority so a second signer app can't intercept.
343pub fn get_nip55_signer_package() -> Result<Option<String>, String> {
344    let conn = super::get_db_connection_guard_static()?;
345    Ok(conn.query_row(
346        "SELECT value FROM settings WHERE key = 'nip55_signer_package'",
347        [],
348        |row| row.get::<_, String>(0),
349    ).ok())
350}
351
352/// Persist the paired signer app's package name. Updated on re-pair.
353pub fn set_nip55_signer_package(package: &str) -> Result<(), String> {
354    let conn = super::get_write_connection_guard_static()?;
355    conn.execute(
356        "INSERT OR REPLACE INTO settings (key, value) VALUES ('nip55_signer_package', ?1)",
357        rusqlite::params![package],
358    ).map_err(|e| format!("Failed to set nip55_signer_package: {}", e))?;
359    Ok(())
360}
361
362/// Atomically commit NIP-55 new-account setup: `encryption_enabled`,
363/// `security_type`, `signer_type='nip55'`, and the two plaintext public fields.
364/// No `pkey` is written (nothing secret exists), and any stale key material
365/// from a prior local/bunker setup on this DB is scrubbed so login can't
366/// mis-route through a leftover pkey/bunker row. Transactional for the same
367/// reason as the sibling commits — a half-written account bricks login.
368pub fn commit_nip55_account_setup(
369    user_pubkey_hex: &str,
370    signer_package: &str,
371    encryption_enabled: bool,
372    security_type: Option<&str>,
373) -> Result<(), String> {
374    let mut conn = super::get_write_connection_guard_static()?;
375    let tx = conn.transaction()
376        .map_err(|e| format!("Failed to begin tx: {}", e))?;
377    tx.execute(
378        "INSERT OR REPLACE INTO settings (key, value) VALUES ('encryption_enabled', ?1)",
379        rusqlite::params![if encryption_enabled { "true" } else { "false" }],
380    ).map_err(|e| format!("Failed to set encryption_enabled: {}", e))?;
381    if let Some(st) = security_type {
382        tx.execute(
383            "INSERT OR REPLACE INTO settings (key, value) VALUES ('security_type', ?1)",
384            rusqlite::params![st],
385        ).map_err(|e| format!("Failed to set security_type: {}", e))?;
386    } else {
387        tx.execute(
388            "DELETE FROM settings WHERE key = 'security_type'",
389            [],
390        ).map_err(|e| format!("Failed to clear security_type: {}", e))?;
391    }
392    tx.execute(
393        "INSERT OR REPLACE INTO settings (key, value) VALUES ('signer_type', 'nip55')",
394        [],
395    ).map_err(|e| format!("Failed to set signer_type: {}", e))?;
396    tx.execute(
397        "INSERT OR REPLACE INTO settings (key, value) VALUES ('nip55_user_pubkey', ?1)",
398        rusqlite::params![user_pubkey_hex],
399    ).map_err(|e| format!("Failed to set nip55_user_pubkey: {}", e))?;
400    tx.execute(
401        "INSERT OR REPLACE INTO settings (key, value) VALUES ('nip55_signer_package', ?1)",
402        rusqlite::params![signer_package],
403    ).map_err(|e| format!("Failed to set nip55_signer_package: {}", e))?;
404    // Scrub any secret/bunker material a prior setup on this DB may have left —
405    // a NIP-55 account must never fall back to a stale key at boot.
406    for stale in ["pkey", "seed", "bunker_url", "bunker_remote_pubkey"] {
407        tx.execute(
408            "DELETE FROM settings WHERE key = ?1",
409            rusqlite::params![stale],
410        ).map_err(|e| format!("Failed to clear stale {}: {}", stale, e))?;
411    }
412    tx.commit().map_err(|e| format!("Failed to commit tx: {}", e))?;
413    Ok(())
414}