1use zeroize::Zeroize;
9use crate::state::set_encryption_enabled;
10use crate::stored_event::event_kind;
11
12pub const NIP55_PIN_CANARY: &str = "vector-nip55-pin-ok";
17
18fn encrypt_with_key(input: &str, key: &[u8; 32]) -> String {
19 crate::crypto::encrypt_with_key(input, key).expect("Encryption should not fail")
20}
21
22fn decrypt_with_key(ciphertext: &str, key: &[u8; 32]) -> Result<String, ()> {
23 crate::crypto::decrypt_with_key(ciphertext, key).map_err(|_| ())
24}
25
26#[derive(serde::Serialize, Clone)]
28pub struct MigrationProgress {
29 pub total: usize,
30 pub completed: usize,
31 pub phase: String,
32}
33
34pub fn disable(
47 key: &[u8; 32],
48 progress: &dyn Fn(MigrationProgress),
49) -> Result<(), String> {
50 let mut conn = crate::db::get_write_connection_guard_static()?;
51 let tx = conn.transaction()
52 .map_err(|e| format!("Failed to begin transaction: {}", e))?;
53
54 tx.execute(
56 "INSERT OR REPLACE INTO settings (key, value) VALUES ('migration_state', 'decrypting')",
57 [],
58 ).map_err(|e| format!("Failed to set migration_state: {}", e))?;
59
60 let all_ids: Vec<String> = {
62 let mut stmt = tx.prepare(
63 r#"SELECT id FROM events
64 WHERE kind IN (?1, ?2, ?3)
65 AND length(content) >= 56
66 AND content NOT GLOB '*[^0-9a-f]*'"#,
67 ).map_err(|e| format!("Failed to prepare ID query: {}", e))?;
68
69 let rows = stmt.query_map(
70 rusqlite::params![
71 event_kind::CHAT_MESSAGE as i32,
72 event_kind::PRIVATE_DIRECT_MESSAGE as i32,
73 event_kind::MESSAGE_EDIT as i32,
74 ],
75 |row| row.get::<_, String>(0),
76 ).map_err(|e| format!("Failed to query IDs: {}", e))?;
77
78 rows.filter_map(|r| r.ok()).collect()
79 };
80
81 let total = all_ids.len();
82 let mut completed = 0;
83 let mut last_emitted_percent: i32 = -1;
84 let mut skipped = 0;
85
86 for id in &all_ids {
88 let content: Option<String> = tx.query_row(
89 "SELECT content FROM events WHERE id = ?1",
90 rusqlite::params![id],
91 |row| row.get(0),
92 ).ok();
93
94 if let Some(content) = content {
95 match decrypt_with_key(&content, key) {
96 Ok(plaintext) => {
97 tx.execute(
98 "UPDATE events SET content = ?1 WHERE id = ?2",
99 rusqlite::params![plaintext, id],
100 ).map_err(|e| format!("Failed to update event: {}", e))?;
101 }
102 Err(_) => {
103 crate::log_info!("[Encryption] Skipping event {} - looks encrypted but isn't", id);
105 skipped += 1;
106 }
107 }
108 }
109
110 completed += 1;
111 let current_percent = if total > 0 {
112 ((completed as f64 / total as f64) * 100.0) as i32
113 } else {
114 100
115 };
116
117 if current_percent >= last_emitted_percent + 5 {
118 last_emitted_percent = current_percent;
119 progress(MigrationProgress {
120 total,
121 completed,
122 phase: "decrypting".to_string(),
123 });
124 }
125 }
126
127 progress(MigrationProgress {
129 total,
130 completed,
131 phase: "finalizing".to_string(),
132 });
133
134 decrypt_setting_in_tx(&tx, "seed", key, |v| v.contains(' '))?;
135 decrypt_setting_in_tx(&tx, "pkey", key, |v| v.starts_with("nsec"))?;
136 decrypt_setting_in_tx(&tx, "bunker_url", key, |v| v.starts_with("bunker://"))?;
137 decrypt_pivx_in_tx(&tx, key)?;
138 decrypt_community_in_tx(&tx, key)?;
139
140 verify_plaintext_state_in_tx(&tx)?;
142
143 tx.execute(
145 "INSERT OR REPLACE INTO settings (key, value) VALUES ('encryption_enabled', 'false')",
146 [],
147 ).map_err(|e| format!("Failed to update encryption_enabled: {}", e))?;
148
149 tx.execute(
152 "DELETE FROM settings WHERE key = 'nip55_pin_check'",
153 [],
154 ).map_err(|e| format!("Failed to clear pin canary: {}", e))?;
155 tx.execute(
156 "DELETE FROM settings WHERE key = 'security_type'",
157 [],
158 ).map_err(|e| format!("Failed to clear security_type: {}", e))?;
159
160 tx.execute(
163 "DELETE FROM settings WHERE key = 'biometric_wrapped_key'",
164 [],
165 ).map_err(|e| format!("Failed to clear biometric wrap: {}", e))?;
166
167 tx.execute(
168 "INSERT OR REPLACE INTO settings (key, value) VALUES ('migration_state', '')",
169 [],
170 ).map_err(|e| format!("Failed to clear migration_state: {}", e))?;
171
172 tx.commit().map_err(|e| format!("Failed to commit disable transaction: {}", e))?;
174
175 set_encryption_enabled(false);
177
178 if skipped > 0 {
179 crate::log_info!("[Encryption] Skipped {} false-positive hex events", skipped);
180 }
181 crate::log_info!("[Encryption] Disable complete: decrypted {} events", total - skipped);
182
183 Ok(())
184}
185
186pub fn enable(
195 key: &[u8; 32],
196 security_type: &str,
197 biometric_wrap: Option<&str>,
198 nip55_canary: Option<&str>,
199 progress: &dyn Fn(MigrationProgress),
200) -> Result<(), String> {
201 let mut conn = crate::db::get_write_connection_guard_static()?;
202 let tx = conn.transaction()
203 .map_err(|e| format!("Failed to begin transaction: {}", e))?;
204
205 tx.execute(
207 "INSERT OR REPLACE INTO settings (key, value) VALUES ('migration_state', 'encrypting')",
208 [],
209 ).map_err(|e| format!("Failed to set migration_state: {}", e))?;
210
211 let all_ids: Vec<String> = {
213 let mut stmt = tx.prepare(
214 r#"SELECT id FROM events
215 WHERE kind IN (?1, ?2, ?3)
216 AND (length(content) < 56 OR content GLOB '*[^0-9a-f]*')"#,
217 ).map_err(|e| format!("Failed to prepare ID query: {}", e))?;
218
219 let rows = stmt.query_map(
220 rusqlite::params![
221 event_kind::CHAT_MESSAGE as i32,
222 event_kind::PRIVATE_DIRECT_MESSAGE as i32,
223 event_kind::MESSAGE_EDIT as i32,
224 ],
225 |row| row.get::<_, String>(0),
226 ).map_err(|e| format!("Failed to query IDs: {}", e))?;
227
228 rows.filter_map(|r| r.ok()).collect()
229 };
230
231 let total = all_ids.len();
232 let mut completed = 0;
233 let mut last_emitted_percent: i32 = -1;
234
235 for id in &all_ids {
237 let content: Option<String> = tx.query_row(
238 "SELECT content FROM events WHERE id = ?1",
239 rusqlite::params![id],
240 |row| row.get(0),
241 ).ok();
242
243 if let Some(content) = content {
244 let encrypted = encrypt_with_key(&content, key);
245 tx.execute(
246 "UPDATE events SET content = ?1 WHERE id = ?2",
247 rusqlite::params![encrypted, id],
248 ).map_err(|e| format!("Failed to update event: {}", e))?;
249 }
250
251 completed += 1;
252 let current_percent = if total > 0 {
253 ((completed as f64 / total as f64) * 100.0) as i32
254 } else {
255 100
256 };
257
258 if current_percent >= last_emitted_percent + 5 {
259 last_emitted_percent = current_percent;
260 progress(MigrationProgress {
261 total,
262 completed,
263 phase: "encrypting".to_string(),
264 });
265 }
266 }
267
268 progress(MigrationProgress {
270 total,
271 completed,
272 phase: "finalizing".to_string(),
273 });
274
275 encrypt_setting_in_tx(&tx, "seed", key, |v| v.contains(' '))?;
276 encrypt_setting_in_tx(&tx, "pkey", key, |v| v.starts_with("nsec"))?;
277 encrypt_setting_in_tx(&tx, "bunker_url", key, |v| v.starts_with("bunker://"))?;
278 encrypt_pivx_in_tx(&tx, key)?;
279 encrypt_community_in_tx(&tx, key)?;
280
281 verify_encrypted_state_in_tx(&tx, key)?;
283
284 tx.execute(
286 "INSERT OR REPLACE INTO settings (key, value) VALUES ('encryption_enabled', 'true')",
287 [],
288 ).map_err(|e| format!("Failed to update encryption_enabled: {}", e))?;
289
290 tx.execute(
291 "INSERT OR REPLACE INTO settings (key, value) VALUES ('security_type', ?1)",
292 rusqlite::params![security_type],
293 ).map_err(|e| format!("Failed to update security_type: {}", e))?;
294
295 if let Some(c) = nip55_canary {
298 tx.execute(
299 "INSERT OR REPLACE INTO settings (key, value) VALUES ('nip55_pin_check', ?1)",
300 rusqlite::params![c],
301 ).map_err(|e| format!("Failed to set pin canary: {}", e))?;
302 }
303
304 match biometric_wrap {
309 Some(w) => {
310 tx.execute(
311 "INSERT OR REPLACE INTO settings (key, value) VALUES ('biometric_wrapped_key', ?1)",
312 rusqlite::params![w],
313 ).map_err(|e| format!("Failed to set biometric wrap: {}", e))?;
314 }
315 None => {
316 tx.execute(
317 "DELETE FROM settings WHERE key = 'biometric_wrapped_key'",
318 [],
319 ).map_err(|e| format!("Failed to clear biometric wrap: {}", e))?;
320 }
321 }
322
323 tx.execute(
324 "INSERT OR REPLACE INTO settings (key, value) VALUES ('migration_state', '')",
325 [],
326 ).map_err(|e| format!("Failed to clear migration_state: {}", e))?;
327
328 tx.commit().map_err(|e| format!("Failed to commit enable transaction: {}", e))?;
330
331 set_encryption_enabled(true);
333
334 crate::log_info!("[Encryption] Enable complete: encrypted {} events", total);
335
336 Ok(())
337}
338
339fn decrypt_setting_in_tx(
345 tx: &rusqlite::Transaction,
346 key_name: &str,
347 key: &[u8; 32],
348 is_plaintext: fn(&str) -> bool,
349) -> Result<(), String> {
350 let val: Option<String> = tx.query_row(
351 "SELECT value FROM settings WHERE key = ?1",
352 rusqlite::params![key_name],
353 |row| row.get(0),
354 ).ok();
355
356 if let Some(current) = val {
357 if is_plaintext(¤t) {
358 return Ok(()); }
360
361 let decrypted = decrypt_with_key(¤t, key)
362 .map_err(|_| format!("Failed to decrypt setting '{}'", key_name))?;
363
364 tx.execute(
365 "UPDATE settings SET value = ?1 WHERE key = ?2",
366 rusqlite::params![decrypted, key_name],
367 ).map_err(|e| format!("Failed to update setting '{}': {}", key_name, e))?;
368 }
369
370 Ok(())
371}
372
373fn encrypt_setting_in_tx(
376 tx: &rusqlite::Transaction,
377 key_name: &str,
378 key: &[u8; 32],
379 is_plaintext: fn(&str) -> bool,
380) -> Result<(), String> {
381 let val: Option<String> = tx.query_row(
382 "SELECT value FROM settings WHERE key = ?1",
383 rusqlite::params![key_name],
384 |row| row.get(0),
385 ).ok();
386
387 if let Some(current) = val {
388 if is_plaintext(¤t) {
389 let encrypted = encrypt_with_key(¤t, key);
390 tx.execute(
391 "UPDATE settings SET value = ?1 WHERE key = ?2",
392 rusqlite::params![encrypted, key_name],
393 ).map_err(|e| format!("Failed to update setting '{}': {}", key_name, e))?;
394 }
395 }
396
397 Ok(())
398}
399
400fn decrypt_pivx_in_tx(
402 tx: &rusqlite::Transaction,
403 key: &[u8; 32],
404) -> Result<(), String> {
405 let keys: Vec<(i64, String)> = {
406 let mut stmt = tx.prepare("SELECT id, privkey_encrypted FROM pivx_promos")
407 .map_err(|e| format!("Failed to prepare PIVX query: {}", e))?;
408 let rows = stmt.query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)))
409 .map_err(|e| format!("Failed to query PIVX promos: {}", e))?;
410 rows.filter_map(|r| r.ok()).collect()
411 };
412
413 for (id, key_val) in keys {
414 if key_val.len() > 64 {
416 let decrypted = decrypt_with_key(&key_val, key)
417 .map_err(|_| format!("Failed to decrypt PIVX key {}", id))?;
418 tx.execute(
419 "UPDATE pivx_promos SET privkey_encrypted = ?1 WHERE id = ?2",
420 rusqlite::params![decrypted, id],
421 ).map_err(|e| format!("Failed to update PIVX key: {}", e))?;
422 }
423 }
424
425 Ok(())
426}
427
428fn encrypt_pivx_in_tx(
430 tx: &rusqlite::Transaction,
431 key: &[u8; 32],
432) -> Result<(), String> {
433 let keys: Vec<(i64, String)> = {
434 let mut stmt = tx.prepare("SELECT id, privkey_encrypted FROM pivx_promos")
435 .map_err(|e| format!("Failed to prepare PIVX query: {}", e))?;
436 let rows = stmt.query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)))
437 .map_err(|e| format!("Failed to query PIVX promos: {}", e))?;
438 rows.filter_map(|r| r.ok()).collect()
439 };
440
441 for (id, key_val) in keys {
442 if key_val.len() <= 64 {
444 let encrypted = encrypt_with_key(&key_val, key);
445 tx.execute(
446 "UPDATE pivx_promos SET privkey_encrypted = ?1 WHERE id = ?2",
447 rusqlite::params![encrypted, id],
448 ).map_err(|e| format!("Failed to update PIVX key: {}", e))?;
449 }
450 }
451
452 Ok(())
453}
454
455fn xform_text(v: &str, enc: Option<&[u8; 32]>, dec: Option<&[u8; 32]>) -> Result<String, String> {
473 let mut s = v.to_string();
474 if let Some(k) = dec {
475 if let Ok(p) = decrypt_with_key(&s, k) {
477 s = p;
478 }
479 }
480 if let Some(k) = enc {
481 if decrypt_with_key(&s, k).is_err() {
483 s = encrypt_with_key(&s, k);
484 }
485 }
486 Ok(s)
487}
488
489fn xform_text_opt(v: &Option<String>, enc: Option<&[u8; 32]>, dec: Option<&[u8; 32]>) -> Result<Option<String>, String> {
490 v.as_deref().map(|s| xform_text(s, enc, dec)).transpose()
491}
492
493fn xform_blob(v: &[u8], enc: Option<&[u8; 32]>, dec: Option<&[u8; 32]>) -> Result<Vec<u8>, String> {
494 let mut b = v.to_vec();
495 if let Some(k) = dec {
496 if let Ok(p) = crate::crypto::decrypt_blob_with_key(&b, k) {
498 b = p;
499 }
500 }
501 if let Some(k) = enc {
502 if crate::crypto::decrypt_blob_with_key(&b, k).is_err() {
503 b = crate::crypto::encrypt_blob_with_key(&b, k)?;
504 }
505 }
506 Ok(b)
507}
508
509#[cfg(test)]
510mod xform_tests {
511 use super::*;
512 const K: [u8; 32] = [7u8; 32];
513 const K2: [u8; 32] = [9u8; 32];
514
515 #[test]
516 fn bare_hex_plaintext_is_encrypted_not_skipped() {
517 let token = "de".repeat(32); let wrapped = xform_text(&token, Some(&K), None).unwrap();
521 assert_ne!(wrapped, token, "bare-hex plaintext must be encrypted");
522 assert_eq!(decrypt_with_key(&wrapped, &K).unwrap(), token);
523 }
524
525 #[test]
526 fn encrypt_is_idempotent() {
527 let token = "de".repeat(32);
528 let once = xform_text(&token, Some(&K), None).unwrap();
529 let twice = xform_text(&once, Some(&K), None).unwrap();
530 assert_eq!(once, twice, "re-running encrypt must not double-wrap");
531 assert_eq!(decrypt_with_key(&twice, &K).unwrap(), token);
532 }
533
534 #[test]
535 fn disable_then_reenable_roundtrips_bare_hex() {
536 let token = "ab".repeat(32);
537 let enc = xform_text(&token, Some(&K), None).unwrap();
538 let dec = xform_text(&enc, None, Some(&K)).unwrap();
539 assert_eq!(dec, token, "disable decrypts back to plaintext");
540 let re = xform_text(&dec, Some(&K), None).unwrap();
541 assert_eq!(decrypt_with_key(&re, &K).unwrap(), token, "re-enable wraps the bare-hex again");
542 }
543
544 #[test]
545 fn rekey_rewraps_bare_hex_under_new_key() {
546 let token = "cd".repeat(32);
547 let old = xform_text(&token, Some(&K), None).unwrap();
548 let new = xform_text(&old, Some(&K2), Some(&K)).unwrap(); assert!(decrypt_with_key(&new, &K).is_err(), "no longer under old key");
550 assert_eq!(decrypt_with_key(&new, &K2).unwrap(), token, "now under new key");
551 }
552
553 #[test]
554 fn nonhex_plaintext_roundtrips() {
555 for v in ["general", "{\"roles\":[]}", "wss://relay.example", ""] {
556 let enc = xform_text(v, Some(&K), None).unwrap();
557 assert_eq!(decrypt_with_key(&enc, &K).unwrap(), v);
558 assert_eq!(xform_text(&enc, None, Some(&K)).unwrap(), v);
559 }
560 }
561
562 fn migrate_test_schema(conn: &rusqlite::Connection) {
565 conn.execute_batch(
566 "CREATE TABLE communities (community_id TEXT, server_root_key BLOB, name TEXT, relays TEXT, description TEXT, icon TEXT, banner TEXT, banlist TEXT, owner_attestation TEXT, roles TEXT, invite_registry TEXT, owner_pubkey TEXT, owner_salt TEXT, meta_extra TEXT, control_pk TEXT, control_root BLOB);
567 CREATE TABLE community_channels (channel_id TEXT, channel_key BLOB, name TEXT);
568 CREATE TABLE community_epoch_keys (key BLOB);
569 CREATE TABLE community_message_keys (outer_event_id TEXT, ephemeral_secret BLOB, relays TEXT);
570 CREATE TABLE pending_community_invites (community_id TEXT, bundle_json TEXT, inviter_npub TEXT);
571 CREATE TABLE community_public_invites (token TEXT, url TEXT, label TEXT);
572 CREATE TABLE community_invite_link_sets (creator TEXT, locators TEXT);
573 CREATE TABLE community_guestbook (community_id TEXT, events TEXT, cursor_secs INTEGER);
574 CREATE TABLE community_policies (community_id TEXT, policy_id TEXT, bytes TEXT, hash TEXT, enabled INTEGER, updated_at INTEGER);",
575 ).unwrap();
576 }
577
578 #[test]
583 fn guestbook_and_policies_survive_enable_rekey_disable() {
584 let mut conn = rusqlite::Connection::open_in_memory().unwrap();
585 migrate_test_schema(&conn);
586 conn.execute(
587 "INSERT INTO community_guestbook (community_id, events, cursor_secs) VALUES (?1, ?2, 7)",
588 rusqlite::params!["aa".repeat(32), r#"[{"rumor_id":"x"}]"#],
589 )
590 .unwrap();
591 conn.execute(
592 "INSERT INTO community_policies (community_id, policy_id, bytes, hash, enabled, updated_at)
593 VALUES (?1, 'p1', ?2, 'hh', 1, 0)",
594 rusqlite::params!["aa".repeat(32), r#"{"format":1,"name":"x"}"#],
595 )
596 .unwrap();
597 let read = |c: &rusqlite::Connection| -> (String, String) {
598 (
599 c.query_row("SELECT events FROM community_guestbook", [], |r| r.get(0)).unwrap(),
600 c.query_row("SELECT bytes FROM community_policies", [], |r| r.get(0)).unwrap(),
601 )
602 };
603 let before = read(&conn);
604 let k1 = [0x21u8; 32];
605 let k2 = [0x22u8; 32];
606 let step = |c: &mut rusqlite::Connection, enc: Option<&[u8; 32]>, dec: Option<&[u8; 32]>| {
607 let tx = c.transaction().unwrap();
608 migrate_community_in_tx(&tx, enc, dec).unwrap();
609 tx.commit().unwrap();
610 };
611 step(&mut conn, Some(&k1), None);
612 let enabled = read(&conn);
616 assert_ne!(enabled.0, before.0, "the guestbook must actually be encrypted by the sweep");
617 assert_ne!(enabled.1, before.1, "the policy bytes must actually be encrypted by the sweep");
618
619 step(&mut conn, Some(&k2), Some(&k1));
620 step(&mut conn, None, Some(&k2));
621 assert_eq!(read(&conn), before, "a rekey round-trip must return the plaintext it started with");
622 }
623
624 #[test]
629 fn v2_control_pair_survives_enable_rekey_disable() {
630 let mut conn = rusqlite::Connection::open_in_memory().unwrap();
631 migrate_test_schema(&conn);
632 let control_root: Vec<u8> = vec![0x5Au8; 32];
633 conn.execute(
634 "INSERT INTO communities (community_id, server_root_key, name, relays, banlist, roles, invite_registry, owner_pubkey, owner_salt, meta_extra, control_pk, control_root)
635 VALUES (?1, ?2, 'n', '[]', '[]', '{}', '[]', ?3, ?4, '{\"custom\":null,\"extra\":{}}', ?5, ?6)",
636 rusqlite::params!["cc".repeat(32), vec![0x11u8; 32], "ab".repeat(32), "cd".repeat(32), "ef".repeat(32), control_root],
637 ).unwrap();
638 let read = |c: &rusqlite::Connection| -> (String, String, Vec<u8>) {
639 c.query_row("SELECT owner_pubkey, control_pk, control_root FROM communities", [], |r| {
640 Ok((r.get(0)?, r.get(1)?, r.get(2)?))
641 }).unwrap()
642 };
643
644 let tx = conn.transaction().unwrap();
645 encrypt_community_in_tx(&tx, &K).unwrap();
646 tx.commit().unwrap();
647 let (owner_pk, control_pk, root) = read(&conn);
648 assert_ne!(owner_pk, "ab".repeat(32), "owner commitment must be wrapped after enable");
649 assert_ne!(control_pk, "ef".repeat(32), "control_pk must be wrapped after enable");
650 assert_ne!(root, vec![0x5Au8; 32], "control_root must be wrapped after enable");
651
652 let tx = conn.transaction().unwrap();
653 rekey_community_in_tx(&tx, &K, &K2).unwrap();
654 tx.commit().unwrap();
655
656 let tx = conn.transaction().unwrap();
657 decrypt_community_in_tx(&tx, &K2).unwrap();
658 tx.commit().unwrap();
659 let (owner_pk, control_pk, root) = read(&conn);
660 assert_eq!(owner_pk, "ab".repeat(32), "owner commitment survives enable → rekey → disable");
661 assert_eq!(control_pk, "ef".repeat(32), "control_pk survives enable → rekey → disable");
662 assert_eq!(root, vec![0x5Au8; 32], "control_root survives enable → rekey → disable");
663 }
664
665 #[test]
668 fn public_invite_label_survives_enable_rekey_disable() {
669 let mut conn = rusqlite::Connection::open_in_memory().unwrap();
670 migrate_test_schema(&conn);
671 conn.execute(
672 "INSERT INTO community_public_invites (token, url, label) VALUES (?1, ?2, ?3)",
673 rusqlite::params!["ab".repeat(32), "https://vectorapp.io/invite#x", "Reddit"],
674 ).unwrap();
675 let read_label = |c: &rusqlite::Connection| -> String {
676 c.query_row("SELECT label FROM community_public_invites", [], |r| r.get(0)).unwrap()
677 };
678
679 let tx = conn.transaction().unwrap();
680 encrypt_community_in_tx(&tx, &K).unwrap();
681 tx.commit().unwrap();
682 assert_ne!(read_label(&conn), "Reddit", "label must be wrapped after enable");
683
684 let tx = conn.transaction().unwrap();
685 rekey_community_in_tx(&tx, &K, &K2).unwrap();
686 tx.commit().unwrap();
687
688 let tx = conn.transaction().unwrap();
689 decrypt_community_in_tx(&tx, &K2).unwrap();
690 tx.commit().unwrap();
691 assert_eq!(read_label(&conn), "Reddit", "label must survive enable → rekey → disable");
692 }
693
694 #[test]
695 fn blob_roundtrip_and_idempotent() {
696 let raw = [0x42u8; 32];
697 let enc = xform_blob(&raw, Some(&K), None).unwrap();
698 assert_eq!(enc.len(), 60);
699 assert_eq!(xform_blob(&enc, Some(&K), None).unwrap(), enc, "blob encrypt idempotent");
700 assert_eq!(xform_blob(&enc, None, Some(&K)).unwrap(), raw.to_vec());
701 let re = xform_blob(&enc, Some(&K2), Some(&K)).unwrap();
703 assert_eq!(crate::crypto::decrypt_blob_with_key(&re, &K2).unwrap(), raw.to_vec());
704 }
705}
706
707fn migrate_community_in_tx(
708 tx: &rusqlite::Transaction,
709 enc: Option<&[u8; 32]>,
710 dec: Option<&[u8; 32]>,
711) -> Result<(), String> {
712 #[allow(clippy::type_complexity)]
716 let rows: Vec<(String, Vec<u8>, String, String, Option<String>, Option<String>, Option<String>, String, Option<String>, String, String, Option<String>, Option<String>, Option<String>, Option<String>, Option<Vec<u8>>)> = {
717 let mut stmt = tx.prepare(
718 "SELECT community_id, server_root_key, name, relays, description, icon, banner, banlist, owner_attestation, roles, invite_registry, owner_pubkey, owner_salt, meta_extra, control_pk, control_root FROM communities",
719 ).map_err(|e| format!("prepare communities: {e}"))?;
720 let mapped = stmt.query_map([], |r| Ok((
721 r.get::<_, String>(0)?, r.get::<_, Vec<u8>>(1)?, r.get::<_, String>(2)?, r.get::<_, String>(3)?,
722 r.get::<_, Option<String>>(4)?, r.get::<_, Option<String>>(5)?, r.get::<_, Option<String>>(6)?,
723 r.get::<_, String>(7)?, r.get::<_, Option<String>>(8)?, r.get::<_, String>(9)?, r.get::<_, String>(10)?,
724 r.get::<_, Option<String>>(11)?, r.get::<_, Option<String>>(12)?, r.get::<_, Option<String>>(13)?,
725 r.get::<_, Option<String>>(14)?, r.get::<_, Option<Vec<u8>>>(15)?,
726 ))).map_err(|e| format!("query communities: {e}"))?;
727 mapped.filter_map(|r| r.ok()).collect()
728 };
729 for (id, root, name, relays, desc, icon, banner, banlist, owner, roles, registry, owner_pk, owner_salt, meta_extra, control_pk, control_root) in rows {
730 tx.execute(
731 "UPDATE communities SET server_root_key=?1, name=?2, relays=?3, description=?4, icon=?5,
732 banner=?6, banlist=?7, owner_attestation=?8, roles=?9, invite_registry=?10,
733 owner_pubkey=?11, owner_salt=?12, meta_extra=?13, control_pk=?14, control_root=?15 WHERE community_id=?16",
734 rusqlite::params![
735 xform_blob(&root, enc, dec)?, xform_text(&name, enc, dec)?, xform_text(&relays, enc, dec)?,
736 xform_text_opt(&desc, enc, dec)?, xform_text_opt(&icon, enc, dec)?, xform_text_opt(&banner, enc, dec)?,
737 xform_text(&banlist, enc, dec)?, xform_text_opt(&owner, enc, dec)?, xform_text(&roles, enc, dec)?,
738 xform_text(®istry, enc, dec)?, xform_text_opt(&owner_pk, enc, dec)?, xform_text_opt(&owner_salt, enc, dec)?,
739 xform_text_opt(&meta_extra, enc, dec)?, xform_text_opt(&control_pk, enc, dec)?,
740 control_root.as_deref().map(|b| xform_blob(b, enc, dec)).transpose()?, id,
741 ],
742 ).map_err(|e| format!("update communities: {e}"))?;
743 }
744
745 let chans: Vec<(String, Vec<u8>, String)> = {
747 let mut stmt = tx.prepare("SELECT channel_id, channel_key, name FROM community_channels")
748 .map_err(|e| format!("prepare channels: {e}"))?;
749 let mapped = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, Vec<u8>>(1)?, r.get::<_, String>(2)?)))
750 .map_err(|e| format!("query channels: {e}"))?;
751 mapped.filter_map(|r| r.ok()).collect()
752 };
753 for (cid, key, name) in chans {
754 tx.execute(
755 "UPDATE community_channels SET channel_key=?1, name=?2 WHERE channel_id=?3",
756 rusqlite::params![xform_blob(&key, enc, dec)?, xform_text(&name, enc, dec)?, cid],
757 ).map_err(|e| format!("update channel: {e}"))?;
758 }
759
760 let eks: Vec<(i64, Vec<u8>)> = {
762 let mut stmt = tx.prepare("SELECT rowid, key FROM community_epoch_keys")
763 .map_err(|e| format!("prepare epoch keys: {e}"))?;
764 let mapped = stmt.query_map([], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, Vec<u8>>(1)?)))
765 .map_err(|e| format!("query epoch keys: {e}"))?;
766 mapped.filter_map(|r| r.ok()).collect()
767 };
768 for (rowid, key) in eks {
769 tx.execute("UPDATE community_epoch_keys SET key=?1 WHERE rowid=?2",
770 rusqlite::params![xform_blob(&key, enc, dec)?, rowid])
771 .map_err(|e| format!("update epoch key: {e}"))?;
772 }
773
774 let mks: Vec<(String, Vec<u8>, String)> = {
776 let mut stmt = tx.prepare("SELECT outer_event_id, ephemeral_secret, relays FROM community_message_keys")
777 .map_err(|e| format!("prepare msg keys: {e}"))?;
778 let mapped = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, Vec<u8>>(1)?, r.get::<_, String>(2)?)))
779 .map_err(|e| format!("query msg keys: {e}"))?;
780 mapped.filter_map(|r| r.ok()).collect()
781 };
782 for (oid, secret, relays) in mks {
783 tx.execute("UPDATE community_message_keys SET ephemeral_secret=?1, relays=?2 WHERE outer_event_id=?3",
784 rusqlite::params![xform_blob(&secret, enc, dec)?, xform_text(&relays, enc, dec)?, oid])
785 .map_err(|e| format!("update msg key: {e}"))?;
786 }
787
788 let pend: Vec<(String, String, String)> = {
790 let mut stmt = tx.prepare("SELECT community_id, bundle_json, inviter_npub FROM pending_community_invites")
791 .map_err(|e| format!("prepare pending: {e}"))?;
792 let mapped = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?, r.get::<_, String>(2)?)))
793 .map_err(|e| format!("query pending: {e}"))?;
794 mapped.filter_map(|r| r.ok()).collect()
795 };
796 for (id, bundle, inviter) in pend {
797 tx.execute("UPDATE pending_community_invites SET bundle_json=?1, inviter_npub=?2 WHERE community_id=?3",
798 rusqlite::params![xform_text(&bundle, enc, dec)?, xform_text(&inviter, enc, dec)?, id])
799 .map_err(|e| format!("update pending: {e}"))?;
800 }
801
802 let gbs: Vec<(String, String)> = {
807 let mut stmt = tx.prepare("SELECT community_id, events FROM community_guestbook")
808 .map_err(|e| format!("prepare guestbook: {e}"))?;
809 let mapped = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))
810 .map_err(|e| format!("query guestbook: {e}"))?;
811 mapped.filter_map(|r| r.ok()).collect()
812 };
813 for (id, events) in gbs {
814 tx.execute("UPDATE community_guestbook SET events=?1 WHERE community_id=?2",
815 rusqlite::params![xform_text(&events, enc, dec)?, id])
816 .map_err(|e| format!("update guestbook: {e}"))?;
817 }
818
819 let pols: Vec<(String, String, String)> = {
821 let mut stmt = tx.prepare("SELECT community_id, policy_id, bytes FROM community_policies")
822 .map_err(|e| format!("prepare policies: {e}"))?;
823 let mapped = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?, r.get::<_, String>(2)?)))
824 .map_err(|e| format!("query policies: {e}"))?;
825 mapped.filter_map(|r| r.ok()).collect()
826 };
827 for (cid, pid, bytes) in pols {
828 tx.execute("UPDATE community_policies SET bytes=?1 WHERE community_id=?2 AND policy_id=?3",
829 rusqlite::params![xform_text(&bytes, enc, dec)?, cid, pid])
830 .map_err(|e| format!("update policies: {e}"))?;
831 }
832
833 let pubs: Vec<(i64, String, String, Option<String>)> = {
835 let mut stmt = tx.prepare("SELECT rowid, token, url, label FROM community_public_invites")
836 .map_err(|e| format!("prepare public invites: {e}"))?;
837 let mapped = stmt.query_map([], |r| Ok((
838 r.get::<_, i64>(0)?, r.get::<_, String>(1)?, r.get::<_, String>(2)?, r.get::<_, Option<String>>(3)?,
839 ))).map_err(|e| format!("query public invites: {e}"))?;
840 mapped.filter_map(|r| r.ok()).collect()
841 };
842 for (rowid, token, url, label) in pubs {
843 tx.execute("UPDATE community_public_invites SET token=?1, url=?2, label=?3 WHERE rowid=?4",
844 rusqlite::params![xform_text(&token, enc, dec)?, xform_text(&url, enc, dec)?, xform_text_opt(&label, enc, dec)?, rowid])
845 .map_err(|e| format!("update public invite: {e}"))?;
846 }
847
848 let sets: Vec<(i64, String, String)> = {
850 let mut stmt = tx.prepare("SELECT rowid, creator, locators FROM community_invite_link_sets")
851 .map_err(|e| format!("prepare link sets: {e}"))?;
852 let mapped = stmt.query_map([], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, String>(1)?, r.get::<_, String>(2)?)))
853 .map_err(|e| format!("query link sets: {e}"))?;
854 mapped.filter_map(|r| r.ok()).collect()
855 };
856 for (rowid, creator, locators) in sets {
857 tx.execute("UPDATE community_invite_link_sets SET creator=?1, locators=?2 WHERE rowid=?3",
858 rusqlite::params![xform_text(&creator, enc, dec)?, xform_text(&locators, enc, dec)?, rowid])
859 .map_err(|e| format!("update link set: {e}"))?;
860 }
861
862 Ok(())
863}
864
865fn encrypt_community_in_tx(tx: &rusqlite::Transaction, key: &[u8; 32]) -> Result<(), String> {
867 migrate_community_in_tx(tx, Some(key), None)
868}
869fn decrypt_community_in_tx(tx: &rusqlite::Transaction, key: &[u8; 32]) -> Result<(), String> {
871 migrate_community_in_tx(tx, None, Some(key))
872}
873fn rekey_community_in_tx(tx: &rusqlite::Transaction, old_key: &[u8; 32], new_key: &[u8; 32]) -> Result<(), String> {
875 migrate_community_in_tx(tx, Some(new_key), Some(old_key))
876}
877
878pub fn backfill_community_at_rest() -> Result<(), String> {
884 if !crate::state::is_encryption_enabled_fast() {
885 return Ok(());
886 }
887 if crate::db::get_sql_setting("community_at_rest_encrypted".to_string())
888 .ok()
889 .flatten()
890 .as_deref()
891 == Some("1")
892 {
893 return Ok(());
894 }
895 let mut key = match crate::state::ENCRYPTION_KEY.get() {
896 Some(k) => k,
897 None => return Ok(()), };
899 let conn = crate::db::get_write_connection_guard_static()?;
900 let tx = conn.unchecked_transaction().map_err(|e| format!("backfill tx: {e}"))?;
901 let res = encrypt_community_in_tx(&tx, &key);
902 key.zeroize();
903 res?;
904 tx.execute(
905 "INSERT OR REPLACE INTO settings (key, value) VALUES ('community_at_rest_encrypted', '1')",
906 [],
907 )
908 .map_err(|e| format!("backfill flag: {e}"))?;
909 tx.commit().map_err(|e| format!("backfill commit: {e}"))?;
910 crate::log_info!("[Encryption] Community at-rest backfill complete");
911 Ok(())
912}
913
914fn verify_plaintext_state_in_tx(tx: &rusqlite::Transaction) -> Result<(), String> {
921 if let Ok(pkey) = tx.query_row::<String, _, _>(
923 "SELECT value FROM settings WHERE key = 'pkey'", [], |row| row.get(0),
924 ) {
925 if !pkey.starts_with("nsec") {
926 return Err(format!(
927 "VERIFICATION FAILED: pkey is not plaintext after decryption (len={}, prefix={}). \
928 Aborting to protect your data — encryption status was NOT changed.",
929 pkey.len(), &pkey[..pkey.len().min(8)]
930 ));
931 }
932 }
933
934 if let Ok(seed) = tx.query_row::<String, _, _>(
936 "SELECT value FROM settings WHERE key = 'seed'", [], |row| row.get(0),
937 ) {
938 if !seed.contains(' ') {
939 return Err(
940 "VERIFICATION FAILED: seed is not plaintext after decryption. \
941 Aborting to protect your data — encryption status was NOT changed."
942 .to_string(),
943 );
944 }
945 }
946
947 let mut stmt = tx.prepare("SELECT id, length(privkey_encrypted) FROM pivx_promos")
949 .map_err(|e| format!("Verification query failed: {}", e))?;
950 let bad_keys: Vec<i64> = stmt
951 .query_map([], |row| {
952 let id: i64 = row.get(0)?;
953 let len: i64 = row.get(1)?;
954 Ok((id, len))
955 })
956 .map_err(|e| format!("Verification query failed: {}", e))?
957 .filter_map(|r| r.ok())
958 .filter(|(_, len)| *len > 64)
959 .map(|(id, _)| id)
960 .collect();
961
962 if !bad_keys.is_empty() {
963 return Err(format!(
964 "VERIFICATION FAILED: {} PIVX key(s) still encrypted (IDs: {:?}). \
965 Aborting to protect your data — encryption status was NOT changed.",
966 bad_keys.len(), bad_keys
967 ));
968 }
969
970 crate::log_info!("[Encryption] Verification passed: all data confirmed plaintext");
971 Ok(())
972}
973
974fn verify_encrypted_state_in_tx(
977 tx: &rusqlite::Transaction,
978 key: &[u8; 32],
979) -> Result<(), String> {
980 if let Ok(pkey) = tx.query_row::<String, _, _>(
982 "SELECT value FROM settings WHERE key = 'pkey'", [], |row| row.get(0),
983 ) {
984 if pkey.starts_with("nsec") {
985 return Err(
986 "VERIFICATION FAILED: pkey is still plaintext after encryption. \
987 Aborting to protect your data — encryption status was NOT changed."
988 .to_string(),
989 );
990 }
991 match decrypt_with_key(&pkey, key) {
992 Ok(decrypted) if decrypted.starts_with("nsec") => {}
993 Ok(decrypted) => {
994 return Err(format!(
995 "VERIFICATION FAILED: pkey decrypts but not to nsec (len={}, prefix={}). \
996 Possible double-encryption. Aborting.",
997 decrypted.len(), &decrypted[..decrypted.len().min(6)]
998 ));
999 }
1000 Err(_) => {
1001 return Err(
1002 "VERIFICATION FAILED: pkey cannot be decrypted with current key. Aborting."
1003 .to_string(),
1004 );
1005 }
1006 }
1007 }
1008
1009 if let Ok(seed) = tx.query_row::<String, _, _>(
1011 "SELECT value FROM settings WHERE key = 'seed'", [], |row| row.get(0),
1012 ) {
1013 if seed.contains(' ') {
1014 return Err(
1015 "VERIFICATION FAILED: seed is still plaintext after encryption. Aborting."
1016 .to_string(),
1017 );
1018 }
1019 match decrypt_with_key(&seed, key) {
1020 Ok(decrypted) if decrypted.contains(' ') => {}
1021 Ok(_) => {
1022 return Err(
1023 "VERIFICATION FAILED: seed decrypts but not to BIP39 mnemonic. \
1024 Possible double-encryption. Aborting."
1025 .to_string(),
1026 );
1027 }
1028 Err(_) => {
1029 return Err(
1030 "VERIFICATION FAILED: seed cannot be decrypted with current key. Aborting."
1031 .to_string(),
1032 );
1033 }
1034 }
1035 }
1036
1037 crate::log_info!("[Encryption] Verification passed: all data confirmed encrypted and round-trips correctly");
1038 Ok(())
1039}
1040
1041pub fn key_matches_account(key: &[u8; 32]) -> bool {
1046 let Ok(conn) = crate::db::get_db_connection_guard_static() else {
1047 return false;
1048 };
1049 let pkey: Option<String> = conn
1050 .query_row("SELECT value FROM settings WHERE key = 'pkey'", [], |r| r.get(0))
1051 .ok();
1052 if let Some(p) = pkey {
1053 if !p.starts_with("nsec") {
1054 return matches!(decrypt_with_key(&p, key), Ok(d) if d.starts_with("nsec"));
1055 }
1056 }
1057 let canary: Option<String> = conn
1058 .query_row("SELECT value FROM settings WHERE key = 'nip55_pin_check'", [], |r| r.get(0))
1059 .ok();
1060 if let Some(c) = canary {
1061 return matches!(
1062 decrypt_with_key(&c, key),
1063 Ok(d) if d == NIP55_PIN_CANARY
1064 );
1065 }
1066 true
1067}
1068
1069pub fn rekey(
1079 old_key: &[u8; 32],
1080 new_key: &[u8; 32],
1081 security_type: &str,
1082 biometric_wrap: Option<&str>,
1083 progress: &dyn Fn(MigrationProgress),
1084) -> Result<(), String> {
1085 let mut conn = crate::db::get_write_connection_guard_static()?;
1086
1087 let tx = conn.transaction()
1089 .map_err(|e| format!("Failed to begin transaction: {}", e))?;
1090
1091 tx.execute(
1093 "INSERT OR REPLACE INTO settings (key, value) VALUES ('migration_state', 'rekeying')",
1094 [],
1095 ).map_err(|e| format!("Failed to set migration_state: {}", e))?;
1096
1097 let all_ids: Vec<String> = {
1099 let mut stmt = tx.prepare(
1100 r#"SELECT id FROM events
1101 WHERE kind IN (?1, ?2, ?3)
1102 AND length(content) >= 56
1103 AND content NOT GLOB '*[^0-9a-f]*'"#,
1104 ).map_err(|e| format!("Failed to prepare ID query: {}", e))?;
1105
1106 let rows = stmt.query_map(
1107 rusqlite::params![
1108 event_kind::CHAT_MESSAGE as i32,
1109 event_kind::PRIVATE_DIRECT_MESSAGE as i32,
1110 event_kind::MESSAGE_EDIT as i32,
1111 ],
1112 |row| row.get::<_, String>(0),
1113 ).map_err(|e| format!("Failed to query IDs: {}", e))?;
1114
1115 rows.filter_map(|r| r.ok()).collect()
1116 };
1117
1118 let total = all_ids.len();
1119 let mut completed = 0;
1120 let mut last_emitted_percent: i32 = -1;
1121
1122 for id in &all_ids {
1124 let content: Option<String> = tx.query_row(
1125 "SELECT content FROM events WHERE id = ?1",
1126 rusqlite::params![id],
1127 |row| row.get(0),
1128 ).ok();
1129
1130 if let Some(content) = content {
1131 match decrypt_with_key(&content, old_key) {
1132 Ok(plaintext) => {
1133 let re_encrypted = encrypt_with_key(&plaintext, new_key);
1134 tx.execute(
1135 "UPDATE events SET content = ?1 WHERE id = ?2",
1136 rusqlite::params![re_encrypted, id],
1137 ).map_err(|e| format!("Failed to update event: {}", e))?;
1138 }
1139 Err(_) => {
1140 crate::log_warn!("[Rekey] Skipping event {} - decrypt failed", id);
1142 }
1143 }
1144 }
1145
1146 completed += 1;
1147 let current_percent = if total > 0 {
1148 ((completed as f64 / total as f64) * 100.0) as i32
1149 } else {
1150 100
1151 };
1152
1153 if current_percent >= last_emitted_percent + 5 {
1154 last_emitted_percent = current_percent;
1155 progress(MigrationProgress {
1156 total,
1157 completed,
1158 phase: "rekeying".to_string(),
1159 });
1160 }
1161 }
1162
1163 progress(MigrationProgress {
1165 total,
1166 completed,
1167 phase: "finalizing".to_string(),
1168 });
1169
1170 rekey_setting_in_tx(&tx, "pkey", old_key, new_key)?;
1171 rekey_setting_in_tx(&tx, "seed", old_key, new_key)?;
1172 rekey_setting_in_tx(&tx, "nip55_pin_check", old_key, new_key)?;
1177 rekey_setting_in_tx(&tx, "bunker_url", old_key, new_key)?;
1178 rekey_pivx_in_tx(&tx, old_key, new_key)?;
1179 rekey_community_in_tx(&tx, old_key, new_key)?;
1180
1181 verify_encrypted_state_in_tx(&tx, new_key)?;
1183
1184 tx.execute(
1186 "INSERT OR REPLACE INTO settings (key, value) VALUES ('security_type', ?1)",
1187 rusqlite::params![security_type],
1188 ).map_err(|e| format!("Failed to update security_type: {}", e))?;
1189
1190 match biometric_wrap {
1195 Some(w) => {
1196 tx.execute(
1197 "INSERT OR REPLACE INTO settings (key, value) VALUES ('biometric_wrapped_key', ?1)",
1198 rusqlite::params![w],
1199 ).map_err(|e| format!("Failed to set biometric wrap: {}", e))?;
1200 }
1201 None => {
1202 tx.execute(
1203 "DELETE FROM settings WHERE key = 'biometric_wrapped_key'",
1204 [],
1205 ).map_err(|e| format!("Failed to clear biometric wrap: {}", e))?;
1206 }
1207 }
1208
1209 tx.execute(
1210 "INSERT OR REPLACE INTO settings (key, value) VALUES ('migration_state', '')",
1211 [],
1212 ).map_err(|e| format!("Failed to clear migration_state: {}", e))?;
1213
1214 tx.commit().map_err(|e| format!("Failed to commit re-key transaction: {}", e))?;
1216
1217 Ok(())
1218}
1219
1220fn rekey_setting_in_tx(
1222 tx: &rusqlite::Transaction,
1223 key: &str,
1224 old_key: &[u8; 32],
1225 new_key: &[u8; 32],
1226) -> Result<(), String> {
1227
1228 let val: Option<String> = tx.query_row(
1229 "SELECT value FROM settings WHERE key = ?1",
1230 rusqlite::params![key],
1231 |row| row.get(0),
1232 ).ok();
1233
1234 if let Some(encrypted_val) = val {
1235 let looks_encrypted = encrypted_val.len() >= 56
1239 && encrypted_val.bytes().all(|b| b.is_ascii_hexdigit());
1240 let plaintext = match decrypt_with_key(&encrypted_val, old_key) {
1241 Ok(p) => p,
1242 Err(_) if !looks_encrypted => encrypted_val.clone(),
1243 Err(_) => return Err(format!("Failed to decrypt setting '{}'", key)),
1244 };
1245
1246 let re_encrypted = encrypt_with_key(&plaintext, new_key);
1247
1248 tx.execute(
1249 "UPDATE settings SET value = ?1 WHERE key = ?2",
1250 rusqlite::params![re_encrypted, key],
1251 ).map_err(|e| format!("Failed to update setting '{}': {}", key, e))?;
1252 }
1253
1254 Ok(())
1255}
1256
1257fn rekey_pivx_in_tx(
1259 tx: &rusqlite::Transaction,
1260 old_key: &[u8; 32],
1261 new_key: &[u8; 32],
1262) -> Result<(), String> {
1263
1264 let keys: Vec<(i64, String)> = {
1265 let mut stmt = tx.prepare("SELECT id, privkey_encrypted FROM pivx_promos")
1266 .map_err(|e| format!("Failed to prepare PIVX query: {}", e))?;
1267 let rows = stmt.query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)))
1268 .map_err(|e| format!("Failed to query PIVX promos: {}", e))?;
1269 rows.filter_map(|r| r.ok()).collect()
1270 };
1271
1272 for (id, key_val) in keys {
1273 if key_val.len() > 64 {
1275 let decrypted = decrypt_with_key(&key_val, old_key)
1276 .map_err(|_| format!("Failed to decrypt PIVX key {}", id))?;
1277 let re_encrypted = encrypt_with_key(&decrypted, new_key);
1278
1279 tx.execute(
1280 "UPDATE pivx_promos SET privkey_encrypted = ?1 WHERE id = ?2",
1281 rusqlite::params![re_encrypted, id],
1282 ).map_err(|e| format!("Failed to update PIVX key: {}", e))?;
1283 }
1284 }
1285
1286 Ok(())
1287}
1288
1289#[cfg(test)]
1290mod rekey_setting_tests {
1291 use super::*;
1292 const OLD: [u8; 32] = [3u8; 32];
1293 const NEW: [u8; 32] = [5u8; 32];
1294
1295 fn conn() -> rusqlite::Connection {
1296 let c = rusqlite::Connection::open_in_memory().unwrap();
1297 c.execute_batch("CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT);").unwrap();
1298 c
1299 }
1300 fn put(c: &rusqlite::Connection, k: &str, v: &str) {
1301 c.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
1302 rusqlite::params![k, v]).unwrap();
1303 }
1304 fn get(c: &rusqlite::Connection, k: &str) -> String {
1305 c.query_row("SELECT value FROM settings WHERE key = ?1", rusqlite::params![k], |r| r.get(0)).unwrap()
1306 }
1307
1308 #[test]
1311 fn canary_survives_rekey() {
1312 let mut c = conn();
1313 put(&c, "nip55_pin_check", &encrypt_with_key(NIP55_PIN_CANARY, &OLD));
1314 let tx = c.transaction().unwrap();
1315 rekey_setting_in_tx(&tx, "nip55_pin_check", &OLD, &NEW).unwrap();
1316 tx.commit().unwrap();
1317 assert_eq!(
1318 decrypt_with_key(&get(&c, "nip55_pin_check"), &NEW).unwrap(),
1319 NIP55_PIN_CANARY
1320 );
1321 }
1322
1323 #[test]
1325 fn bunker_url_survives_rekey() {
1326 let mut c = conn();
1327 let url = "bunker://abc?relay=wss://relay.example";
1328 put(&c, "bunker_url", &encrypt_with_key(url, &OLD));
1329 let tx = c.transaction().unwrap();
1330 rekey_setting_in_tx(&tx, "bunker_url", &OLD, &NEW).unwrap();
1331 tx.commit().unwrap();
1332 assert_eq!(decrypt_with_key(&get(&c, "bunker_url"), &NEW).unwrap(), url);
1333 }
1334
1335 #[test]
1338 fn plaintext_row_is_adopted_not_fatal() {
1339 let mut c = conn();
1340 let url = "bunker://plain?relay=wss://r.example";
1341 put(&c, "bunker_url", url);
1342 let tx = c.transaction().unwrap();
1343 rekey_setting_in_tx(&tx, "bunker_url", &OLD, &NEW).unwrap();
1344 tx.commit().unwrap();
1345 assert_eq!(decrypt_with_key(&get(&c, "bunker_url"), &NEW).unwrap(), url);
1346 }
1347
1348 #[test]
1351 fn wrong_old_key_aborts() {
1352 let mut c = conn();
1353 put(&c, "pkey", &encrypt_with_key("nsec1example", &[9u8; 32]));
1354 let tx = c.transaction().unwrap();
1355 assert!(rekey_setting_in_tx(&tx, "pkey", &OLD, &NEW).is_err());
1356 }
1357
1358 #[test]
1360 fn missing_row_is_noop() {
1361 let mut c = conn();
1362 let tx = c.transaction().unwrap();
1363 rekey_setting_in_tx(&tx, "nip55_pin_check", &OLD, &NEW).unwrap();
1364 tx.commit().unwrap();
1365 assert_eq!(
1366 c.query_row("SELECT count(*) FROM settings", [], |r| r.get::<_, i64>(0)).unwrap(), 0
1367 );
1368 }
1369}