diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 4071dd9..6ea8928 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -37,6 +37,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Workspace::new(window, cx)) } +struct DeviceNotifcation; struct SignerNotifcation; struct RelayNotifcation; @@ -171,9 +172,32 @@ impl Workspace { cx, ); } + DeviceEvent::NotSet { reason } => { + let note = Notification::new() + .id::() + .title("Cannot setup the encryption key") + .message(reason) + .autohide(false) + .with_kind(NotificationKind::Error); + + window.push_notification(note, cx); + } + DeviceEvent::NotSubscribe { reason } => { + let note = Notification::new() + .id::() + .title("Cannot getting messages") + .message(reason) + .autohide(false) + .with_kind(NotificationKind::Error); + + window.push_notification(note, cx); + } DeviceEvent::Error(error) => { window.push_notification(Notification::error(error).autohide(false), cx); } + _ => { + // TODO + } }; }), ); @@ -424,35 +448,13 @@ impl Workspace { .child(SharedString::from(ENC_WARN)), ), ) - .on_ok(move |_ev, window, cx| { + .on_ok(move |_ev, _window, cx| { let device = DeviceRegistry::global(cx); - let task = device.read(cx).create_encryption(cx); - - window - .spawn(cx, async move |cx| { - let result = task.await; - - cx.update(|window, cx| match result { - Ok(keys) => { - device.update(cx, |this, cx| { - this.set_signer(keys, cx); - this.listen_request(cx); - }); - window.close_modal(cx); - } - Err(e) => { - window.push_notification( - Notification::error(e.to_string()).autohide(false), - cx, - ); - } - }) - .ok(); - }) - .detach(); - - // false to keep modal open - false + device.update(cx, |this, cx| { + this.set_announcement(cx); + }); + // true to close modal + true }) }); } @@ -702,25 +704,46 @@ impl Workspace { .ghost() .dropdown_menu(move |this, _window, cx| { let device = DeviceRegistry::global(cx); - let state = device.read(cx).state(); + let subscribing = device.read(cx).subscribing; + let requesting = device.read(cx).requesting; this.min_w(px(260.)) + .when(requesting, |this| { + this.item(PopupMenuItem::element(move |_window, cx| { + h_flex() + .px_1() + .w_full() + .gap_2() + .text_sm() + .child( + div() + .size_1p5() + .rounded_full() + .bg(cx.theme().icon_accent), + ) + .child(SharedString::from("Waiting for approval...")) + })) + }) .item(PopupMenuItem::element(move |_window, cx| { h_flex() .px_1() .w_full() .gap_2() .text_sm() - .child( - div() - .size_1p5() - .rounded_full() - .when(state.set(), |this| this.bg(gpui::green())) - .when(state.requesting(), |this| { - this.bg(cx.theme().icon_accent) - }), - ) - .child(SharedString::from(state.to_string())) + .child(div().size_1p5().rounded_full().map(|this| { + if subscribing { + this.bg(cx.theme().icon_accent) + } else { + this.bg(cx.theme().icon_muted) + } + })) + .map(|this| { + if subscribing { + this.child(SharedString::from("Getting messages...")) + } else { + this.child(SharedString::from("Not getting messages")) + } + }) })) .separator() .menu_with_icon( diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 9a6fe2d..70ef7eb 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -10,9 +10,7 @@ use gpui::{ }; use nostr_sdk::prelude::*; use person::PersonRegistry; -use state::{ - Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, StateEvent, TIMEOUT, app_name, -}; +use state::{Announcement, DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, app_name}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; @@ -36,17 +34,53 @@ impl Global for GlobalDeviceRegistry {} pub enum DeviceEvent { /// A new encryption signer has been set Set, + /// The encryption key has been reset + Reset, + /// Encryption key is not set + NotSet { reason: SharedString }, + /// An event to notify that Coop isn't subscribed to gift wrap events + NotSubscribe { reason: SharedString }, /// An error occurred Error(SharedString), } +impl DeviceEvent { + pub fn error(error: T) -> Self + where + T: Into, + { + Self::Error(error.into()) + } + + pub fn not_subscribe(reason: T) -> Self + where + T: Into, + { + Self::NotSubscribe { + reason: reason.into(), + } + } + + pub fn not_set(reason: T) -> Self + where + T: Into, + { + Self::NotSet { + reason: reason.into(), + } + } +} + /// Device Registry /// /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md #[derive(Debug)] pub struct DeviceRegistry { - /// Device state - state: DeviceState, + /// Whether the registry is currently subscribing to gift wrap events + pub subscribing: bool, + + /// Whether the registry is waiting for encryption key approval from other devices + pub requesting: bool, /// Async tasks tasks: Vec>>, @@ -71,30 +105,30 @@ impl DeviceRegistry { /// Create a new device registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); - let state = DeviceState::default(); - let subscription = Some(cx.subscribe_in( - &nostr, - window, - |this, _state, event, _window, cx| match event { + // Get announcement when signer is set + let subscription = cx.subscribe_in(&nostr, window, |this, _e, event, _window, cx| { + match event { StateEvent::SignerSet => { - this.reset(cx); + this.set_subscribing(false, cx); + this.set_requesting(false, cx); } StateEvent::RelayConnected => { this.get_announcement(cx); } _ => {} - }, - )); + }; + }); cx.defer_in(window, |this, window, cx| { this.handle_notifications(window, cx); }); Self { - state, + subscribing: false, + requesting: false, tasks: vec![], - _subscription: subscription, + _subscription: Some(subscription), } } @@ -140,13 +174,13 @@ impl DeviceRegistry { self.tasks.push(cx.spawn_in(window, async move |this, cx| { while let Ok(event) = rx.recv_async().await { match event.kind { - // New request event + // New request event from other device Kind::Custom(4454) => { this.update_in(cx, |this, window, cx| { this.ask_for_approval(event, window, cx); })?; } - // New response event + // New response event from the master device Kind::Custom(4455) => { this.update(cx, |this, cx| { this.extract_encryption(event, cx); @@ -155,24 +189,24 @@ impl DeviceRegistry { _ => {} } } - Ok(()) })); } - /// Get the device state - pub fn state(&self) -> DeviceState { - self.state.clone() + /// Set whether the registry is currently subscribing to gift wrap events + fn set_subscribing(&mut self, subscribing: bool, cx: &mut Context) { + self.subscribing = subscribing; + cx.notify(); } - /// Set the device state - fn set_state(&mut self, state: DeviceState, cx: &mut Context) { - self.state = state; + /// Set whether the registry is waiting for encryption key approval from other devices + fn set_requesting(&mut self, requesting: bool, cx: &mut Context) { + self.requesting = requesting; cx.notify(); } /// Set the decoupled encryption key for the current user - pub fn set_signer(&mut self, new: S, cx: &mut Context) + fn set_signer(&mut self, new: S, cx: &mut Context) where S: NostrSigner + 'static, { @@ -184,7 +218,7 @@ impl DeviceRegistry { // Update state this.update(cx, |this, cx| { - this.set_state(DeviceState::Set, cx); + cx.emit(DeviceEvent::Set); this.get_messages(cx); })?; @@ -192,12 +226,6 @@ impl DeviceRegistry { })); } - /// Reset the device state - fn reset(&mut self, cx: &mut Context) { - self.state = DeviceState::Idle; - cx.notify(); - } - /// Get all messages for encryption keys fn get_messages(&mut self, cx: &mut Context) { let task = self.subscribe_to_giftwrap_events(cx); @@ -205,59 +233,50 @@ impl DeviceRegistry { self.tasks.push(cx.spawn(async move |this, cx| { if let Err(e) = task.await { this.update(cx, |_this, cx| { - cx.emit(DeviceEvent::Error(SharedString::from(e.to_string()))); + cx.emit(DeviceEvent::not_subscribe(e.to_string())); + })?; + } else { + this.update(cx, |this, cx| { + this.set_subscribing(true, cx); })?; } Ok(()) })); } - /// Get the messaging relays for the current user - fn get_user_messaging_relays(&self, cx: &App) -> Task, Error>> { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - - cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - if let Some(event) = client.database().query(filter).await?.first_owned() { - // Extract relay URLs from the event - let urls: Vec = nip17::extract_owned_relay_list(event).collect(); - - // Ensure all relays are connected - for url in urls.iter() { - client.add_relay(url).and_connect().await?; - } - - Ok(urls) - } else { - Err(anyhow!("Relays not found")) - } - }) - } - /// Continuously get gift wrap events for the current user in their messaging relays fn subscribe_to_giftwrap_events(&self, cx: &App) -> Task> { + let persons = PersonRegistry::global(cx); let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); - let urls = self.get_user_messaging_relays(cx); + + let Some(user) = signer.public_key() else { + return Task::ready(Err(anyhow!("User not found"))); + }; + + let profile = persons.read(cx).get(&user, cx); + let relays = profile.messaging_relays().clone(); cx.background_spawn(async move { - let urls = urls.await?; let encryption = signer.get_encryption_signer().await.context("not found")?; let public_key = encryption.get_public_key().await?; let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let id = SubscriptionId::new(DEVICE_GIFTWRAP); + // Ensure user has relays configured + if relays.is_empty() { + return Err(anyhow!("No messaging relays found")); + } + + // Ensure relays are connected + for url in relays.iter() { + client.add_relay(url).and_connect().await?; + } + // Construct target for subscription - let target: HashMap = urls + let target: HashMap = relays .into_iter() .map(|relay| (relay, filter.clone())) .collect(); @@ -302,13 +321,15 @@ impl DeviceRegistry { self.tasks.push(cx.spawn(async move |this, cx| { match task.await { Ok(event) => { + // Set encryption key from the announcement event this.update(cx, |this, cx| { - this.new_signer(&event, cx); + this.set_encryption(&event, cx); })?; } Err(_) => { + // User has no announcement, create a new one this.update(cx, |this, cx| { - this.announce(cx); + this.set_announcement(cx); })?; } } @@ -317,8 +338,30 @@ impl DeviceRegistry { })); } + /// Create a new device signer and announce it to user's relay list + pub fn set_announcement(&mut self, cx: &mut Context) { + let task = self.new_encryption(cx); + + self.tasks.push(cx.spawn(async move |this, cx| { + match task.await { + Ok(keys) => { + this.update(cx, |this, cx| { + this.set_signer(keys, cx); + this.wait_for_request(cx); + })?; + } + Err(e) => { + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::error(e.to_string())); + })?; + } + } + Ok(()) + })); + } + /// Create new encryption keys - pub fn create_encryption(&self, cx: &App) -> Task> { + fn new_encryption(&self, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); @@ -328,12 +371,13 @@ impl DeviceRegistry { cx.background_spawn(async move { // Construct an announcement event - let event = client - .sign_event_builder(EventBuilder::new(Kind::Custom(10044), "").tags(vec![ - Tag::custom(TagKind::custom("n"), vec![n]), - Tag::client(app_name()), - ])) - .await?; + let builder = EventBuilder::new(Kind::Custom(10044), "").tags(vec![ + Tag::custom(TagKind::custom("n"), vec![n]), + Tag::client(app_name()), + ]); + + // Sign the event with user's signer + let event = client.sign_event_builder(builder).await?; // Publish announcement client.send_event(&event).to_nip65().await?; @@ -345,39 +389,23 @@ impl DeviceRegistry { }) } - /// Create a new device signer and announce it - fn announce(&mut self, cx: &mut Context) { - let task = self.create_encryption(cx); - - self.tasks.push(cx.spawn(async move |this, cx| { - let keys = task.await?; - - // Update signer - this.update(cx, |this, cx| { - this.set_signer(keys, cx); - this.listen_request(cx); - })?; - - Ok(()) - })); - } - - /// Initialize device signer (decoupled encryption key) for the current user - pub fn new_signer(&mut self, event: &Event, cx: &mut Context) { + /// Set encryption key from the announcement event + fn set_encryption(&mut self, event: &Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let announcement = Announcement::from(event); let device_pubkey = announcement.public_key(); + // Get encryption key from the database and compare with the announcement let task: Task> = cx.background_spawn(async move { if let Ok(keys) = get_keys(&client).await { if keys.public_key() != device_pubkey { - return Err(anyhow!("Key mismatch")); + return Err(anyhow!("Encryption Key doesn't match the announcement")); }; Ok(keys) } else { - Err(anyhow!("Key not found")) + Err(anyhow!("Encryption Key not found. Please create a new key")) } }); @@ -386,74 +414,49 @@ impl DeviceRegistry { Ok(keys) => { this.update(cx, |this, cx| { this.set_signer(keys, cx); - this.listen_request(cx); + this.wait_for_request(cx); })?; } Err(e) => { - log::warn!("Failed to initialize device signer: {e}"); - this.update(cx, |this, cx| { - this.request(cx); - this.listen_approval(cx); + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::not_set(e.to_string())); })?; } }; - Ok(()) })); } - /// Listen for device key requests on user's write relays - pub fn listen_request(&mut self, cx: &mut Context) { + /// Wait for encryption key requests from now on + fn wait_for_request(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); - let Some(public_key) = signer.public_key() else { - return; - }; + self.tasks.push(cx.background_spawn(async move { + let public_key = signer.get_public_key().await?; - let task: Task> = cx.background_spawn(async move { - // Construct a filter for device key requests - let filter = Filter::new() + // Construct a filter for encryption key requests + let now = Filter::new() .kind(Kind::Custom(4454)) .author(public_key) .since(Timestamp::now()); - // Subscribe to the device key requests on user's write relays - client.subscribe(filter).await?; - - Ok(()) - }); - - task.detach(); - } - - /// Listen for device key approvals on user's write relays - fn listen_approval(&mut self, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - - let Some(public_key) = signer.public_key() else { - return; - }; - - self.tasks.push(cx.background_spawn(async move { - // Construct a filter for device key requests - let filter = Filter::new() - .kind(Kind::Custom(4455)) + // Construct a filter for the last encryption key request + let last = Filter::new() + .kind(Kind::Custom(4454)) .author(public_key) - .since(Timestamp::now()); + .limit(1); // Subscribe to the device key requests on user's write relays - client.subscribe(filter).await?; + client.subscribe(vec![now, last]).await?; Ok(()) })); } /// Request encryption keys from other device - fn request(&mut self, cx: &mut Context) { + pub fn request(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); @@ -461,9 +464,10 @@ impl DeviceRegistry { let app_keys = nostr.read(cx).keys(); let app_pubkey = app_keys.public_key(); - let task: Task, Error>> = cx.background_spawn(async move { + let task: Task, Error>> = cx.background_spawn(async move { let public_key = signer.get_public_key().await?; + // Construct a filter to get the latest approval event let filter = Filter::new() .kind(Kind::Custom(4455)) .author(public_key) @@ -471,30 +475,18 @@ impl DeviceRegistry { .limit(1); match client.database().query(filter).await?.first_owned() { - Some(event) => { - let root_device = event - .tags - .find(TagKind::custom("P")) - .and_then(|tag| tag.content()) - .and_then(|content| PublicKey::parse(content).ok()) - .context("Invalid event's tags")?; - - let payload = event.content.as_str(); - let decrypted = app_keys.nip44_decrypt(&root_device, payload).await?; - - let secret = SecretKey::from_hex(&decrypted)?; - let keys = Keys::new(secret); - - Ok(Some(keys)) - } + // Found an approval event + Some(event) => Ok(Some(event)), + // No approval event found, construct a request event None => { // Construct an event for device key request - let event = client - .sign_event_builder(EventBuilder::new(Kind::Custom(4454), "").tags(vec![ - Tag::client(app_name()), - Tag::custom(TagKind::custom("P"), vec![app_pubkey]), - ])) - .await?; + let builder = EventBuilder::new(Kind::Custom(4454), "").tags(vec![ + Tag::custom(TagKind::custom("P"), vec![app_pubkey]), + Tag::client(app_name()), + ]); + + // Sign the event with user's signer + let event = client.sign_event_builder(builder).await?; // Send the event to write relays client.send_event(&event).to_nip65().await?; @@ -506,32 +498,56 @@ impl DeviceRegistry { self.tasks.push(cx.spawn(async move |this, cx| { match task.await { - Ok(Some(keys)) => { + Ok(Some(event)) => { this.update(cx, |this, cx| { - this.set_signer(keys, cx); + this.extract_encryption(event, cx); })?; } Ok(None) => { this.update(cx, |this, cx| { - this.set_state(DeviceState::Requesting, cx); + this.set_requesting(true, cx); + this.wait_for_approval(cx); })?; } Err(e) => { - log::error!("Failed to request the encryption key: {e}"); + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::error(e.to_string())); + })?; } }; + Ok(()) + })); + } + + /// Wait for encryption key approvals + fn wait_for_approval(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + let signer = nostr.read(cx).signer(); + + self.tasks.push(cx.background_spawn(async move { + let public_key = signer.get_public_key().await?; + + // Construct a filter for device key requests + let filter = Filter::new() + .kind(Kind::Custom(4455)) + .author(public_key) + .since(Timestamp::now()); + + // Subscribe to the device key requests on user's write relays + client.subscribe(filter).await?; Ok(()) })); } - /// Parse the response event for device keys from other devices + /// Parse the approval event to get encryption key then set it fn extract_encryption(&mut self, event: Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let app_keys = nostr.read(cx).keys(); let task: Task> = cx.background_spawn(async move { - let root_device = event + let master = event .tags .find(TagKind::custom("P")) .and_then(|tag| tag.content()) @@ -539,7 +555,7 @@ impl DeviceRegistry { .context("Invalid event's tags")?; let payload = event.content.as_str(); - let decrypted = app_keys.nip44_decrypt(&root_device, payload).await?; + let decrypted = app_keys.nip44_decrypt(&master, payload).await?; let secret = SecretKey::from_hex(&decrypted)?; let keys = Keys::new(secret); @@ -548,13 +564,19 @@ impl DeviceRegistry { }); self.tasks.push(cx.spawn(async move |this, cx| { - let keys = task.await?; - - // Update signer - this.update(cx, |this, cx| { - this.set_signer(keys, cx); - })?; - + match task.await { + Ok(keys) => { + this.update(cx, |this, cx| { + this.set_signer(keys, cx); + this.set_requesting(false, cx); + })?; + } + Err(e) => { + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::not_set(e.to_string())); + })?; + } + } Ok(()) })); } diff --git a/crates/state/src/device.rs b/crates/state/src/device.rs index 11b6a3c..4aae264 100644 --- a/crates/state/src/device.rs +++ b/crates/state/src/device.rs @@ -1,40 +1,6 @@ -use std::fmt::Display; - use gpui::SharedString; use nostr_sdk::prelude::*; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] -pub enum DeviceState { - #[default] - Idle, - Requesting, - Set, -} - -impl Display for DeviceState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DeviceState::Idle => write!(f, "Idle"), - DeviceState::Requesting => write!(f, "Wait for approval"), - DeviceState::Set => write!(f, "Encryption Key is ready"), - } - } -} - -impl DeviceState { - pub fn idle(&self) -> bool { - matches!(self, DeviceState::Idle) - } - - pub fn requesting(&self) -> bool { - matches!(self, DeviceState::Requesting) - } - - pub fn set(&self) -> bool { - matches!(self, DeviceState::Set) - } -} - /// Announcement #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Announcement {