add backup encryption key

This commit is contained in:
2026-03-17 09:05:18 +07:00
parent f075a83320
commit 84229330e2
3 changed files with 87 additions and 14 deletions

View File

@@ -7,6 +7,13 @@ pub fn home_dir() -> &'static PathBuf {
HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory")) 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<PathBuf> = 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. /// Returns the path to the configuration directory used by Coop.
pub fn config_dir() -> &'static PathBuf { pub fn config_dir() -> &'static PathBuf {
static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new(); static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
@@ -56,9 +63,3 @@ pub fn support_dir() -> &'static PathBuf {
config_dir().clone() config_dir().clone()
}) })
} }
/// Returns the path to the `nostr` file.
pub fn nostr_file() -> &'static PathBuf {
static NOSTR_FILE: OnceLock<PathBuf> = OnceLock::new();
NOSTR_FILE.get_or_init(|| support_dir().join("nostr-db"))
}

View File

@@ -4,6 +4,7 @@ use std::sync::Arc;
use ::settings::AppSettings; use ::settings::AppSettings;
use chat::{ChatEvent, ChatRegistry}; use chat::{ChatEvent, ChatRegistry};
use common::download_dir;
use device::{DeviceEvent, DeviceRegistry}; use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
@@ -47,9 +48,11 @@ enum Command {
ToggleTheme, ToggleTheme,
ToggleAccount, ToggleAccount,
RefreshEncryption,
RefreshRelayList, RefreshRelayList,
RefreshMessagingRelays, RefreshMessagingRelays,
BackupEncryption,
ImportEncryption,
RefreshEncryption,
ResetEncryption, ResetEncryption,
ShowRelayList, ShowRelayList,
@@ -428,6 +431,42 @@ impl Workspace {
Command::ToggleAccount => { Command::ToggleAccount => {
self.account_selector(window, cx); 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( .child(
div() div()
.italic() .italic()
.text_color(cx.theme().warning_active) .text_color(cx.theme().text_danger)
.child(SharedString::from(ENC_WARN)), .child(SharedString::from(ENC_WARN)),
), ),
) )
@@ -708,6 +747,7 @@ impl Workspace {
let requesting = device.read(cx).requesting; let requesting = device.read(cx).requesting;
this.min_w(px(260.)) this.min_w(px(260.))
.label("Encryption Key")
.when(requesting, |this| { .when(requesting, |this| {
this.item(PopupMenuItem::element(move |_window, cx| { this.item(PopupMenuItem::element(move |_window, cx| {
h_flex() h_flex()
@@ -730,6 +770,9 @@ impl Workspace {
.w_full() .w_full()
.gap_2() .gap_2()
.text_sm() .text_sm()
.when(!subscribing, |this| {
this.text_color(cx.theme().text_muted)
})
.child(div().size_1p5().rounded_full().map(|this| { .child(div().size_1p5().rounded_full().map(|this| {
if subscribing { if subscribing {
this.bg(cx.theme().icon_accent) this.bg(cx.theme().icon_accent)
@@ -739,13 +782,24 @@ impl Workspace {
})) }))
.map(|this| { .map(|this| {
if subscribing { if subscribing {
this.child(SharedString::from("Getting messages...")) this.child("Listening for messages")
} else { } else {
this.child(SharedString::from("Not getting messages")) this.child("Idle")
} }
}) })
})) }))
.separator() .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( .menu_with_icon(
"Reload", "Reload",
IconName::Refresh, IconName::Refresh,
@@ -766,7 +820,7 @@ impl Workspace {
.loading(!inbox_connected) .loading(!inbox_connected)
.disabled(!inbox_connected) .disabled(!inbox_connected)
.when(!inbox_connected, |this| { .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()) .when(inbox_connected, |this| this.indicator())
.dropdown_menu(move |this, _window, cx| { .dropdown_menu(move |this, _window, cx| {
@@ -838,7 +892,7 @@ impl Workspace {
.loading(!relay_connected) .loading(!relay_connected)
.disabled(!relay_connected) .disabled(!relay_connected)
.when(!relay_connected, |this| { .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()) .when(relay_connected, |this| this.indicator())
.dropdown_menu(move |this, _window, _cx| { .dropdown_menu(move |this, _window, _cx| {

View File

@@ -1,5 +1,6 @@
use std::cell::Cell; use std::cell::Cell;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
@@ -34,8 +35,8 @@ impl Global for GlobalDeviceRegistry {}
pub enum DeviceEvent { pub enum DeviceEvent {
/// A new encryption signer has been set /// A new encryption signer has been set
Set, Set,
/// The encryption key has been reset /// The device is requesting an encryption key
Reset, Requesting,
/// Encryption key is not set /// Encryption key is not set
NotSet { reason: SharedString }, NotSet { reason: SharedString },
/// An event to notify that Coop isn't subscribed to gift wrap events /// 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<Result<(), Error>> {
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 /// Get device announcement for current user
pub fn get_announcement(&mut self, cx: &mut Context<Self>) { pub fn get_announcement(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
@@ -507,6 +523,8 @@ impl DeviceRegistry {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_requesting(true, cx); this.set_requesting(true, cx);
this.wait_for_approval(cx); this.wait_for_approval(cx);
cx.emit(DeviceEvent::Requesting);
})?; })?;
} }
Err(e) => { Err(e) => {