From 84229330e2cbebd2e0ea631fc5f28404f3f8025b Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Tue, 17 Mar 2026 09:05:18 +0700 Subject: [PATCH] add backup encryption key --- crates/common/src/paths.rs | 13 +++---- crates/coop/src/workspace.rs | 66 ++++++++++++++++++++++++++++++++---- crates/device/src/lib.rs | 22 ++++++++++-- 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/crates/common/src/paths.rs b/crates/common/src/paths.rs index 3017be2..1f23e62 100644 --- a/crates/common/src/paths.rs +++ b/crates/common/src/paths.rs @@ -7,6 +7,13 @@ pub fn home_dir() -> &'static PathBuf { HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory")) } +/// Returns the path to the user's download directory. +pub fn download_dir() -> &'static PathBuf { + static DOWNLOAD_DIR: OnceLock = OnceLock::new(); + DOWNLOAD_DIR + .get_or_init(|| dirs::download_dir().expect("failed to determine download directory")) +} + /// Returns the path to the configuration directory used by Coop. pub fn config_dir() -> &'static PathBuf { static CONFIG_DIR: OnceLock = OnceLock::new(); @@ -56,9 +63,3 @@ pub fn support_dir() -> &'static PathBuf { config_dir().clone() }) } - -/// Returns the path to the `nostr` file. -pub fn nostr_file() -> &'static PathBuf { - static NOSTR_FILE: OnceLock = OnceLock::new(); - NOSTR_FILE.get_or_init(|| support_dir().join("nostr-db")) -} diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 6ea8928..3ee3a7f 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use ::settings::AppSettings; use chat::{ChatEvent, ChatRegistry}; +use common::download_dir; use device::{DeviceEvent, DeviceRegistry}; use gpui::prelude::FluentBuilder; use gpui::{ @@ -47,9 +48,11 @@ enum Command { ToggleTheme, ToggleAccount, - RefreshEncryption, RefreshRelayList, RefreshMessagingRelays, + BackupEncryption, + ImportEncryption, + RefreshEncryption, ResetEncryption, ShowRelayList, @@ -428,6 +431,42 @@ impl Workspace { Command::ToggleAccount => { self.account_selector(window, cx); } + Command::BackupEncryption => { + let device = DeviceRegistry::global(cx).downgrade(); + let save_dialog = cx.prompt_for_new_path(download_dir(), Some("encryption.txt")); + + cx.spawn_in(window, async move |_this, cx| { + // Get the output path from the save dialog + let output_path = match save_dialog.await { + Ok(Ok(Some(path))) => path, + Ok(Ok(None)) | Err(_) => return Ok(()), + Ok(Err(error)) => { + cx.update(|window, cx| { + let message = format!("Failed to pick save location: {error:#}"); + let note = Notification::error(message).autohide(false); + window.push_notification(note, cx); + })?; + return Ok(()); + } + }; + + // Get the backup task + let backup = + device.read_with(cx, |this, cx| this.backup(output_path.clone(), cx))?; + + // Run the backup task + backup.await?; + + // Open the backup file with the system's default application + cx.update(|_window, cx| { + cx.open_with_system(output_path.as_path()); + })?; + + Ok::<_, anyhow::Error>(()) + }) + .detach(); + } + _ => {} } } @@ -444,7 +483,7 @@ impl Workspace { .child( div() .italic() - .text_color(cx.theme().warning_active) + .text_color(cx.theme().text_danger) .child(SharedString::from(ENC_WARN)), ), ) @@ -708,6 +747,7 @@ impl Workspace { let requesting = device.read(cx).requesting; this.min_w(px(260.)) + .label("Encryption Key") .when(requesting, |this| { this.item(PopupMenuItem::element(move |_window, cx| { h_flex() @@ -730,6 +770,9 @@ impl Workspace { .w_full() .gap_2() .text_sm() + .when(!subscribing, |this| { + this.text_color(cx.theme().text_muted) + }) .child(div().size_1p5().rounded_full().map(|this| { if subscribing { this.bg(cx.theme().icon_accent) @@ -739,13 +782,24 @@ impl Workspace { })) .map(|this| { if subscribing { - this.child(SharedString::from("Getting messages...")) + this.child("Listening for messages") } else { - this.child(SharedString::from("Not getting messages")) + this.child("Idle") } }) })) .separator() + .menu_with_icon( + "Backup", + IconName::Shield, + Box::new(Command::BackupEncryption), + ) + .menu_with_icon( + "Restore from secret key", + IconName::Usb, + Box::new(Command::ImportEncryption), + ) + .separator() .menu_with_icon( "Reload", IconName::Refresh, @@ -766,7 +820,7 @@ impl Workspace { .loading(!inbox_connected) .disabled(!inbox_connected) .when(!inbox_connected, |this| { - this.tooltip("Connecting to user's messaging relays...") + this.tooltip("Connecting to the user's messaging relays...") }) .when(inbox_connected, |this| this.indicator()) .dropdown_menu(move |this, _window, cx| { @@ -838,7 +892,7 @@ impl Workspace { .loading(!relay_connected) .disabled(!relay_connected) .when(!relay_connected, |this| { - this.tooltip("Connecting to user's relay list...") + this.tooltip("Connecting to the user's relay list...") }) .when(relay_connected, |this| this.indicator()) .dropdown_menu(move |this, _window, _cx| { diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 70ef7eb..ac5070b 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -1,5 +1,6 @@ use std::cell::Cell; use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; use std::rc::Rc; use std::time::Duration; @@ -34,8 +35,8 @@ impl Global for GlobalDeviceRegistry {} pub enum DeviceEvent { /// A new encryption signer has been set Set, - /// The encryption key has been reset - Reset, + /// The device is requesting an encryption key + Requesting, /// Encryption key is not set NotSet { reason: SharedString }, /// An event to notify that Coop isn't subscribed to gift wrap events @@ -288,6 +289,21 @@ impl DeviceRegistry { }) } + /// Backup the encryption's secret key to a file + pub fn backup(&self, path: PathBuf, cx: &App) -> Task> { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + cx.background_spawn(async move { + let keys = get_keys(&client).await?; + let content = keys.secret_key().to_bech32()?; + + smol::fs::write(path, &content).await?; + + Ok(()) + }) + } + /// Get device announcement for current user pub fn get_announcement(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); @@ -507,6 +523,8 @@ impl DeviceRegistry { this.update(cx, |this, cx| { this.set_requesting(true, cx); this.wait_for_approval(cx); + + cx.emit(DeviceEvent::Requesting); })?; } Err(e) => {