feat: add backup/restore for NIP-4e encryption key #22
@@ -242,7 +242,6 @@ impl Render for ImportKey {
|
|||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.p_4()
|
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pub mod accounts;
|
pub mod accounts;
|
||||||
|
pub mod connect;
|
||||||
|
pub mod import;
|
||||||
|
pub mod restore;
|
||||||
pub mod screening;
|
pub mod screening;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|
||||||
mod connect;
|
|
||||||
mod import;
|
|
||||||
|
|||||||
130
crates/coop/src/dialogs/restore.rs
Normal file
130
crates/coop/src/dialogs/restore.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
|
use device::DeviceRegistry;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
|
Subscription, Task, Window, div,
|
||||||
|
};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
use ui::{WindowExtension, v_flex};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RestoreEncryption {
|
||||||
|
/// Secret key input
|
||||||
|
key_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Async tasks
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
|
/// Event subscription
|
||||||
|
_subscription: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RestoreEncryption {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let key_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
let error = cx.new(|_| None);
|
||||||
|
|
||||||
|
let subscription =
|
||||||
|
cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.restore(window, cx);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
key_input,
|
||||||
|
error,
|
||||||
|
tasks: vec![],
|
||||||
|
_subscription: Some(subscription),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let device = DeviceRegistry::global(cx);
|
||||||
|
let content = self.key_input.read(cx).value();
|
||||||
|
|
||||||
|
if !content.is_empty() {
|
||||||
|
self.set_error("Secret Key cannot be empty.", cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(secret) = SecretKey::parse(&content) else {
|
||||||
|
self.set_error("Secret Key is invalid.", cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
device.update(cx, |this, cx| {
|
||||||
|
this.set_announcement(Keys::new(secret), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the current modal
|
||||||
|
window.close_modal(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
|
// Update error message
|
||||||
|
self.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(message.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the error message after 3 secs
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.error.update(cx, |this, cx| {
|
||||||
|
*this = None;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for RestoreEncryption {
|
||||||
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child("Secret Key")
|
||||||
|
.child(TextInput::new(&self.key_input)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("restore")
|
||||||
|
.label("Restore")
|
||||||
|
.primary()
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.restore(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().text_danger)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ use gpui::{
|
|||||||
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||||
Render, SharedString, Styled, Subscription, Window, div, px,
|
Render, SharedString, Styled, Subscription, Window, div, px,
|
||||||
};
|
};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
@@ -24,6 +25,7 @@ use ui::menu::{DropdownMenu, PopupMenuItem};
|
|||||||
use ui::notification::{Notification, NotificationKind};
|
use ui::notification::{Notification, NotificationKind};
|
||||||
use ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
use ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
|
use crate::dialogs::restore::RestoreEncryption;
|
||||||
use crate::dialogs::{accounts, settings};
|
use crate::dialogs::{accounts, settings};
|
||||||
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
|
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
|
||||||
use crate::sidebar;
|
use crate::sidebar;
|
||||||
@@ -147,7 +149,7 @@ impl Workspace {
|
|||||||
window.push_notification(note, cx);
|
window.push_notification(note, cx);
|
||||||
}
|
}
|
||||||
StateEvent::RelayNotConfigured => {
|
StateEvent::RelayNotConfigured => {
|
||||||
this.relay_notification(window, cx);
|
this.relay_warning(window, cx);
|
||||||
}
|
}
|
||||||
StateEvent::RelayConnected => {
|
StateEvent::RelayConnected => {
|
||||||
window.clear_notification::<RelayNotifcation>(cx);
|
window.clear_notification::<RelayNotifcation>(cx);
|
||||||
@@ -167,13 +169,35 @@ impl Workspace {
|
|||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe all events emitted by the device registry
|
// Observe all events emitted by the device registry
|
||||||
cx.subscribe_in(&device, window, |_this, _device, ev, window, cx| {
|
cx.subscribe_in(&device, window, |_this, _device, event, window, cx| {
|
||||||
match ev {
|
match event {
|
||||||
|
DeviceEvent::Requesting => {
|
||||||
|
const MSG: &str =
|
||||||
|
"Please open the other client and approve the encryption key request";
|
||||||
|
|
||||||
|
let note = Notification::new()
|
||||||
|
.id::<DeviceNotifcation>()
|
||||||
|
.title("Wait for approval")
|
||||||
|
.message(MSG)
|
||||||
|
.with_kind(NotificationKind::Info);
|
||||||
|
|
||||||
|
window.push_notification(note, cx);
|
||||||
|
}
|
||||||
|
DeviceEvent::Creating => {
|
||||||
|
let note = Notification::new()
|
||||||
|
.id::<DeviceNotifcation>()
|
||||||
|
.message("Creating encryption key")
|
||||||
|
.with_kind(NotificationKind::Info);
|
||||||
|
|
||||||
|
window.push_notification(note, cx);
|
||||||
|
}
|
||||||
DeviceEvent::Set => {
|
DeviceEvent::Set => {
|
||||||
window.push_notification(
|
let note = Notification::new()
|
||||||
Notification::success("Encryption Key has been set"),
|
.id::<DeviceNotifcation>()
|
||||||
cx,
|
.message("Encryption Key has been set")
|
||||||
);
|
.with_kind(NotificationKind::Success);
|
||||||
|
|
||||||
|
window.push_notification(note, cx);
|
||||||
}
|
}
|
||||||
DeviceEvent::NotSet { reason } => {
|
DeviceEvent::NotSet { reason } => {
|
||||||
let note = Notification::new()
|
let note = Notification::new()
|
||||||
@@ -198,9 +222,6 @@ impl Workspace {
|
|||||||
DeviceEvent::Error(error) => {
|
DeviceEvent::Error(error) => {
|
||||||
window.push_notification(Notification::error(error).autohide(false), cx);
|
window.push_notification(Notification::error(error).autohide(false), cx);
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -466,15 +487,22 @@ impl Workspace {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
_ => {}
|
Command::ImportEncryption => {
|
||||||
|
self.import_encryption(window, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm_reset_encryption(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn confirm_reset_encryption(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
window.open_modal(cx, |this, _window, cx| {
|
let device = DeviceRegistry::global(cx);
|
||||||
|
let ent = device.downgrade();
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, cx| {
|
||||||
|
let ent = ent.clone();
|
||||||
|
|
||||||
this.confirm()
|
this.confirm()
|
||||||
.show_close(true)
|
.show_close(true)
|
||||||
.title("Reset Encryption Keys")
|
.title("Reset Encryption Key")
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
@@ -488,16 +516,26 @@ impl Workspace {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.on_ok(move |_ev, _window, cx| {
|
.on_ok(move |_ev, _window, cx| {
|
||||||
let device = DeviceRegistry::global(cx);
|
ent.update(cx, |this, cx| {
|
||||||
device.update(cx, |this, cx| {
|
this.set_announcement(Keys::generate(), cx);
|
||||||
this.set_announcement(cx);
|
})
|
||||||
});
|
.ok();
|
||||||
// true to close modal
|
// true to close modal
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn import_encryption(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let restore = cx.new(|cx| RestoreEncryption::new(window, cx));
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
|
this.width(px(520.))
|
||||||
|
.title("Restore Encryption")
|
||||||
|
.child(restore.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn account_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn account_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let accounts = accounts::init(window, cx);
|
let accounts = accounts::init(window, cx);
|
||||||
|
|
||||||
@@ -507,7 +545,6 @@ impl Workspace {
|
|||||||
.show_close(false)
|
.show_close(false)
|
||||||
.keyboard(false)
|
.keyboard(false)
|
||||||
.overlay_closable(false)
|
.overlay_closable(false)
|
||||||
.pb_2()
|
|
||||||
.child(accounts.clone())
|
.child(accounts.clone())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -520,7 +557,6 @@ impl Workspace {
|
|||||||
this.width(px(520.))
|
this.width(px(520.))
|
||||||
.show_close(true)
|
.show_close(true)
|
||||||
.title("Select theme")
|
.title("Select theme")
|
||||||
.pb_2()
|
|
||||||
.child(v_flex().gap_2().w_full().children({
|
.child(v_flex().gap_2().w_full().children({
|
||||||
let mut items = vec![];
|
let mut items = vec![];
|
||||||
|
|
||||||
@@ -593,7 +629,7 @@ impl Workspace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn relay_notification(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn relay_warning(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
const BODY: &str = "Coop cannot found your gossip relay list. \
|
const BODY: &str = "Coop cannot found your gossip relay list. \
|
||||||
Maybe you haven't set it yet or relay not responsed";
|
Maybe you haven't set it yet or relay not responsed";
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ pub enum DeviceEvent {
|
|||||||
Set,
|
Set,
|
||||||
/// The device is requesting an encryption key
|
/// The device is requesting an encryption key
|
||||||
Requesting,
|
Requesting,
|
||||||
|
/// The device is creating a new encryption key
|
||||||
|
Creating,
|
||||||
/// 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
|
||||||
@@ -345,7 +347,7 @@ impl DeviceRegistry {
|
|||||||
Err(_) => {
|
Err(_) => {
|
||||||
// User has no announcement, create a new one
|
// User has no announcement, create a new one
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_announcement(cx);
|
this.set_announcement(Keys::generate(), cx);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,8 +357,11 @@ impl DeviceRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new device signer and announce it to user's relay list
|
/// Create a new device signer and announce it to user's relay list
|
||||||
pub fn set_announcement(&mut self, cx: &mut Context<Self>) {
|
pub fn set_announcement(&mut self, keys: Keys, cx: &mut Context<Self>) {
|
||||||
let task = self.new_encryption(cx);
|
let task = self.create_encryption(keys, cx);
|
||||||
|
|
||||||
|
// Notify that we're creating a new encryption key
|
||||||
|
cx.emit(DeviceEvent::Creating);
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match task.await {
|
match task.await {
|
||||||
@@ -376,12 +381,11 @@ impl DeviceRegistry {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create new encryption keys
|
/// Create new encryption key and announce it to user's relay list
|
||||||
fn new_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
|
fn create_encryption(&self, keys: Keys, cx: &App) -> Task<Result<Keys, Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let keys = Keys::generate();
|
|
||||||
let secret = keys.secret_key().to_secret_hex();
|
let secret = keys.secret_key().to_secret_hex();
|
||||||
let n = keys.public_key();
|
let n = keys.public_key();
|
||||||
|
|
||||||
@@ -396,7 +400,11 @@ impl DeviceRegistry {
|
|||||||
let event = client.sign_event_builder(builder).await?;
|
let event = client.sign_event_builder(builder).await?;
|
||||||
|
|
||||||
// Publish announcement
|
// Publish announcement
|
||||||
client.send_event(&event).to_nip65().await?;
|
client
|
||||||
|
.send_event(&event)
|
||||||
|
.to_nip65()
|
||||||
|
.ack_policy(AckPolicy::none())
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Save device keys to the database
|
// Save device keys to the database
|
||||||
set_keys(&client, &secret).await?;
|
set_keys(&client, &secret).await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user