feat: add backup/restore for NIP-4e encryption key #22
@@ -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"))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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| {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user