From 68015ead1cfc3c4c52be26e041190e200e4c2cf7 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 8 Jan 2026 17:49:58 +0700 Subject: [PATCH] wip: dekey --- crates/chat_ui/src/lib.rs | 3 - crates/coop/src/chatspace.rs | 2 +- crates/person/src/lib.rs | 46 +++++- crates/person/src/person.rs | 11 ++ crates/state/src/device.rs | 26 ++-- crates/state/src/identity.rs | 48 ++++--- crates/state/src/lib.rs | 269 +++++++++++++++++++++++++++-------- 7 files changed, 311 insertions(+), 94 deletions(-) diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 5a001da..86baaf4 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -39,7 +39,6 @@ use crate::text::RenderedText; mod actions; mod emoji; -mod subject; mod text; pub fn init(room: WeakEntity, window: &mut Window, cx: &mut App) -> Entity { @@ -601,7 +600,6 @@ impl ChatPanel { text: AnyElement, cx: &Context, ) -> AnyElement { - let proxy = AppSettings::get_proxy_user_avatars(cx); let hide_avatar = AppSettings::get_hide_user_avatars(cx); let id = message.id; @@ -1132,7 +1130,6 @@ impl Panel for ChatPanel { fn title(&self, cx: &App) -> AnyElement { self.room .read_with(cx, |this, cx| { - let proxy = AppSettings::get_proxy_user_avatars(cx); let label = this.display_name(cx); let url = this.display_image(cx); diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index dddb4a2..4616486 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -541,7 +541,7 @@ impl ChatSpace { }), ) }) - .when_some(identity.read(cx).option_public_key(), |this, public_key| { + .when_some(identity.read(cx).public_key, |this, public_key| { let persons = PersonRegistry::global(cx); let profile = persons.read(cx).get(&public_key, cx); diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index e214cd3..f1b8424 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -9,7 +9,7 @@ use gpui::{App, AppContext, Context, Entity, Global, Task}; use nostr_sdk::prelude::*; pub use person::*; use smallvec::{smallvec, SmallVec}; -use state::{NostrRegistry, TIMEOUT}; +use state::{Announcement, NostrRegistry, TIMEOUT}; mod person; @@ -21,6 +21,12 @@ struct GlobalPersonRegistry(Entity); impl Global for GlobalPersonRegistry {} +#[derive(Debug, Clone)] +enum Dispatch { + Person(Box), + Announcement(Box), +} + /// Person Registry #[derive(Debug)] pub struct PersonRegistry { @@ -54,7 +60,7 @@ impl PersonRegistry { let client = nostr.read(cx).client(); // Channel for communication between nostr and gpui - let (tx, rx) = flume::bounded::(100); + let (tx, rx) = flume::bounded::(100); let (mta_tx, mta_rx) = flume::bounded::(100); let mut tasks = smallvec![]; @@ -84,9 +90,16 @@ impl PersonRegistry { tasks.push( // Update GPUI state cx.spawn(async move |this, cx| { - while let Ok(person) = rx.recv_async().await { + while let Ok(event) = rx.recv_async().await { this.update(cx, |this, cx| { - this.insert(person, cx); + match event { + Dispatch::Person(person) => { + this.insert(*person, cx); + } + Dispatch::Announcement(event) => { + this.set_announcement(&event, cx); + } + }; }) .ok(); } @@ -124,7 +137,7 @@ impl PersonRegistry { } /// Handle nostr notifications - async fn handle_notifications(client: &Client, tx: &flume::Sender) { + async fn handle_notifications(client: &Client, tx: &flume::Sender) { let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); @@ -144,12 +157,21 @@ impl PersonRegistry { Kind::Metadata => { let metadata = Metadata::from_json(&event.content).unwrap_or_default(); let person = Person::new(event.pubkey, metadata); + let val = Box::new(person); - tx.send_async(person).await.ok(); + // Send + tx.send_async(Dispatch::Person(val)).await.ok(); + } + Kind::Custom(10044) => { + let val = Box::new(event.into_owned()); + + // Send + tx.send_async(Dispatch::Announcement(val)).await.ok(); } Kind::ContactList => { let public_keys = event.extract_public_keys(); + // Get metadata for all public keys Self::get_metadata(client, public_keys).await.ok(); } _ => {} @@ -232,6 +254,18 @@ impl PersonRegistry { Ok(persons) } + /// Set profile encryption keys announcement + fn set_announcement(&mut self, event: &Event, cx: &mut App) { + if let Some(person) = self.persons.get(&event.pubkey) { + let announcement = Announcement::from(event); + + person.update(cx, |person, cx| { + person.set_announcement(announcement); + cx.notify(); + }); + } + } + /// Insert batch of persons fn bulk_inserts(&mut self, persons: Vec, cx: &mut Context) { for person in persons.into_iter() { diff --git a/crates/person/src/person.rs b/crates/person/src/person.rs index 0362825..a66a2eb 100644 --- a/crates/person/src/person.rs +++ b/crates/person/src/person.rs @@ -8,8 +8,13 @@ use state::Announcement; /// Person #[derive(Debug, Clone)] pub struct Person { + /// Public Key public_key: PublicKey, + + /// Metadata (profile) metadata: Metadata, + + /// Dekey (NIP-4e) announcement announcement: Option, } @@ -69,6 +74,12 @@ impl Person { self.announcement.clone() } + /// Set profile encryption keys announcement + pub fn set_announcement(&mut self, announcement: Announcement) { + self.announcement = Some(announcement); + log::info!("Updated announcement for: {}", self.public_key()); + } + /// Get profile avatar pub fn avatar(&self) -> SharedString { self.metadata() diff --git a/crates/state/src/device.rs b/crates/state/src/device.rs index 798cb5f..f809c38 100644 --- a/crates/state/src/device.rs +++ b/crates/state/src/device.rs @@ -1,10 +1,21 @@ use gpui::SharedString; use nostr_sdk::prelude::*; +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub enum DeviceState { + #[default] + Initial, + Requesting, + Set, +} + +/// Announcement #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Announcement { - id: EventId, + /// The public key of the device that created this announcement. public_key: PublicKey, + + /// The name of the device that created this announcement. client_name: Option, } @@ -24,27 +35,24 @@ impl From<&Event> for Announcement { .and_then(|tag| tag.content()) .map(|c| c.to_string()); - Self::new(val.id, client_name, public_key) + Self::new(public_key, client_name) } } impl Announcement { - pub fn new(id: EventId, client_name: Option, public_key: PublicKey) -> Self { + pub fn new(public_key: PublicKey, client_name: Option) -> Self { Self { - id, - client_name, public_key, + client_name, } } - pub fn id(&self) -> EventId { - self.id - } - + /// Returns the public key of the device that created this announcement. pub fn public_key(&self) -> PublicKey { self.public_key } + /// Returns the client name of the device that created this announcement. pub fn client_name(&self) -> SharedString { self.client_name .as_ref() diff --git a/crates/state/src/identity.rs b/crates/state/src/identity.rs index fccc202..b2927d1 100644 --- a/crates/state/src/identity.rs +++ b/crates/state/src/identity.rs @@ -1,6 +1,6 @@ -use nostr_sdk::prelude::*; +use std::sync::Arc; -use crate::Announcement; +use nostr_sdk::prelude::*; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum RelayState { @@ -16,13 +16,15 @@ impl RelayState { } } -#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Default)] pub struct Identity { /// The public key of the account - public_key: Option, + pub public_key: Option, - /// Encryption key announcement - announcement: Option, + /// Decoupled encryption key + /// + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + dekey: Option>, /// Status of the current user NIP-65 relays relay_list: RelayState, @@ -41,7 +43,7 @@ impl Identity { pub fn new() -> Self { Self { public_key: None, - announcement: None, + dekey: None, relay_list: RelayState::default(), messaging_relays: RelayState::default(), } @@ -57,6 +59,7 @@ impl Identity { self.relay_list } + /// Sets the state of the NIP-17 relays. pub fn set_messaging_relays_state(&mut self, state: RelayState) { self.messaging_relays = state; } @@ -66,6 +69,26 @@ impl Identity { self.messaging_relays } + /// Returns the decoupled encryption key. + pub fn dekey(&self) -> Option> { + self.dekey.clone() + } + + /// Sets the decoupled encryption key. + pub fn set_dekey(&mut self, dekey: S) + where + S: NostrSigner + 'static, + { + self.dekey = Some(Arc::new(dekey)); + } + + /// Force getting the public key of the identity. + /// + /// Panics if the public key is not set. + pub fn public_key(&self) -> PublicKey { + self.public_key.unwrap() + } + /// Returns true if the identity has a public key. pub fn has_public_key(&self) -> bool { self.public_key.is_some() @@ -80,15 +103,4 @@ impl Identity { pub fn unset_public_key(&mut self) { self.public_key = None; } - - /// Returns the public key of the identity. - pub fn option_public_key(&self) -> Option { - self.public_key - } - - /// Returns the public key of the identity. - pub fn public_key(&self) -> PublicKey { - // This method is safe to unwrap because the public key is always called when the identity is created. - self.public_key.unwrap() - } } diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index de90e9e..750ea2b 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,8 +1,8 @@ use std::collections::HashSet; use std::time::Duration; -use anyhow::Error; -use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; +use anyhow::{anyhow, Context as AnyhowContext, Error}; +use common::{app_name, config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_lmdb::NostrLmdb; use nostr_sdk::prelude::*; @@ -52,6 +52,11 @@ pub struct NostrRegistry { /// Gossip implementation gossip: Entity, + /// Device state + /// + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + device_state: Entity, + /// Tasks for asynchronous operations tasks: Vec>>, @@ -108,6 +113,7 @@ impl NostrRegistry { // Construct the identity entity let identity = cx.new(|_| Identity::default()); + let device_state = cx.new(|_| DeviceState::default()); // Channel for communication between nostr and gpui let (tx, rx) = flume::bounded::(2048); @@ -123,16 +129,18 @@ impl NostrRegistry { RelayState::Initial => { this.get_relay_list(cx); } - RelayState::Set => match state.read(cx).messaging_relays_state() { - RelayState::Initial => { - this.get_profile(cx); - this.get_messaging_relays(cx); - } - RelayState::Set => { - this.get_messages(cx); - } - _ => {} - }, + RelayState::Set => { + match state.read(cx).messaging_relays_state() { + RelayState::Initial => { + this.get_profile(cx); + this.get_messaging_relays(cx); + } + RelayState::Set => { + this.get_messages(state.read(cx).dekey(), cx); + } + _ => {} + }; + } _ => {} } } @@ -150,7 +158,7 @@ impl NostrRegistry { tasks.push( // Update GPUI states - cx.spawn(async move |_this, cx| { + cx.spawn(async move |this, cx| { while let Ok(event) = rx.recv_async().await { match event.kind { Kind::RelayList => { @@ -165,6 +173,11 @@ impl NostrRegistry { cx.notify(); })?; } + Kind::Custom(10044) => { + this.update(cx, |this, cx| { + this.init_dekey(&event, cx); + })?; + } _ => {} } } @@ -176,6 +189,7 @@ impl NostrRegistry { Self { client, identity, + device_state, gossip, app_keys, _subscriptions: subscriptions, @@ -183,7 +197,7 @@ impl NostrRegistry { } } - // Handle nostr notifications + /// Handle nostr notifications async fn handle_notifications(client: &Client, tx: &flume::Sender) -> Result<(), Error> { // Add bootstrap relay to the relay pool for url in BOOTSTRAP_RELAYS.into_iter() { @@ -198,6 +212,7 @@ impl NostrRegistry { // Connect to all added relays client.connect().await; + // Handle nostr notifications let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); @@ -217,7 +232,7 @@ impl NostrRegistry { Kind::RelayList => { // Automatically get messaging relays for each member when the user opens a room if subscription_id.as_str().starts_with("room-") { - Self::get_messaging_relays_by(client, event.as_ref()).await?; + Self::get_adv_events_by(client, event.as_ref()).await?; } tx.send_async(event.into_owned()).await?; @@ -225,6 +240,16 @@ impl NostrRegistry { Kind::InboxRelays => { tx.send_async(event.into_owned()).await?; } + Kind::Custom(10044) => { + if let Ok(signer) = client.signer().await { + if let Ok(public_key) = signer.get_public_key().await { + // Only send if the event is from the current user + if public_key == event.pubkey { + tx.send_async(event.into_owned()).await?; + } + } + } + } _ => {} } } @@ -251,8 +276,8 @@ impl NostrRegistry { Ok(()) } - /// Automatically get messaging relays from a received relay list - async fn get_messaging_relays_by(client: &Client, event: &Event) -> Result<(), Error> { + /// Automatically get messaging relays and encryption announcement from a received relay list + async fn get_adv_events_by(client: &Client, event: &Event) -> Result<(), Error> { // Subscription options let opts = SubscribeAutoCloseOptions::default() .timeout(Some(Duration::from_secs(TIMEOUT))) @@ -276,16 +301,20 @@ impl NostrRegistry { } // Construct filter for inbox relays - let filter = Filter::new() + let inbox = Filter::new() .kind(Kind::InboxRelays) .author(event.pubkey) .limit(1); - client - .subscribe_to(write_relays, vec![filter], Some(opts)) - .await?; + // Construct filter for encryption announcement + let announcement = Filter::new() + .kind(Kind::Custom(10044)) + .author(event.pubkey) + .limit(1); - log::info!("Getting inbox relays for: {}", event.pubkey); + client + .subscribe_to(write_relays, vec![inbox, announcement], Some(opts)) + .await?; Ok(()) } @@ -528,18 +557,8 @@ impl NostrRegistry { .limit(1) .author(public_key); - // Filter for encryption keys announcement - let encryption_keys = Filter::new() - .kind(Kind::Custom(10044)) - .limit(1) - .author(public_key); - client - .subscribe_to( - urls, - vec![metadata, contact_list, encryption_keys], - Some(opts), - ) + .subscribe_to(urls, vec![metadata, contact_list], Some(opts)) .await?; Ok(()) @@ -604,7 +623,10 @@ impl NostrRegistry { } /// Continuously get gift wrap events for the current user in their messaging relays - fn get_messages(&mut self, cx: &mut Context) { + fn get_messages(&mut self, dekey: Option, cx: &mut Context) + where + T: NostrSigner + 'static, + { let client = self.client(); let public_key = self.identity().read(cx).public_key(); let messaging_relays = self.messaging_relays(&public_key, cx); @@ -612,43 +634,176 @@ impl NostrRegistry { cx.background_spawn(async move { let urls = messaging_relays.await; let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION); - let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); + let mut filters = vec![]; - if let Err(e) = client - .subscribe_with_id_to(urls, id, vec![filter], None) - .await - { + // Construct a filter to get user messages + filters.push(Filter::new().kind(Kind::GiftWrap).pubkey(public_key)); + + // Construct a filter to get dekey messages if available + if let Some(signer) = dekey { + if let Ok(pubkey) = signer.get_public_key().await { + filters.push(Filter::new().kind(Kind::GiftWrap).pubkey(pubkey)); + } + } + + if let Err(e) = client.subscribe_with_id_to(urls, id, filters, None).await { log::error!("Failed to subscribe to gift wrap events: {e}"); } }) .detach(); } - /// Subscribe to event kinds to author's write relays - pub fn subscribe(&self, kinds: I, author: PublicKey, cx: &App) + /// Set the decoupled encryption key for the current user + fn set_dekey(&mut self, dekey: T, cx: &mut Context) where - I: Into>, + T: NostrSigner + 'static, { + self.identity.update(cx, |this, cx| { + this.set_dekey(dekey); + cx.notify(); + }); + self.device_state.update(cx, |this, cx| { + *this = DeviceState::Set; + cx.notify(); + }); + } + + /// Initialize dekey (decoupled encryption key) for the current user + fn init_dekey(&mut self, event: &Event, cx: &mut Context) { let client = self.client(); - let write_relays = self.write_relays(&author, cx); + let announcement = Announcement::from(event); + let dekey = announcement.public_key(); - // Construct filters based on event kinds - let filters: Vec = kinds - .into() - .into_iter() - .map(|kind| Filter::new().kind(kind).author(author).limit(1)) - .collect(); + let task: Task> = cx.background_spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; - cx.background_spawn(async move { - let urls = write_relays.await; + let filter = Filter::new() + .identifier("coop:device") + .kind(Kind::ApplicationSpecificData) + .author(public_key) + .limit(1); - // Construct subscription options - let opts = SubscribeAutoCloseOptions::default() - .timeout(Some(Duration::from_secs(TIMEOUT))) - .exit_policy(ReqExitPolicy::ExitOnEOSE); + if let Some(event) = client.database().query(filter).await?.first() { + let content = signer.nip44_decrypt(&public_key, &event.content).await?; + let secret = SecretKey::parse(&content)?; + let keys = Keys::new(secret); - if let Err(e) = client.subscribe_to(urls, filters, Some(opts)).await { - log::error!("Failed to create a subscription: {e}"); + if keys.public_key() == dekey { + Ok(keys) + } else { + Err(anyhow!("Key mismatch")) + } + } else { + Err(anyhow!("Key not found")) + } + }); + + cx.spawn(async move |this, cx| { + match task.await { + Ok(keys) => { + this.update(cx, |this, cx| { + this.set_dekey(keys, cx); + }) + .ok(); + } + Err(e) => { + log::warn!("Failed to initialize dekey: {e}"); + this.update(cx, |this, cx| { + this.request_dekey(cx); + }) + .ok(); + } + }; + }) + .detach(); + } + + /// Request dekey from other device + fn request_dekey(&mut self, cx: &mut Context) { + let client = self.client(); + let device_state = self.device_state.downgrade(); + let public_key = self.identity().read(cx).public_key(); + let write_relays = self.write_relays(&public_key, cx); + + let app_keys = self.app_keys().clone(); + let app_pubkey = app_keys.public_key(); + + let task: Task, Error>> = cx.background_spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + let filter = Filter::new() + .kind(Kind::Custom(4455)) + .author(public_key) + .pubkey(app_pubkey) + .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)) + } + None => { + let urls = write_relays.await; + + // Construct an event for device key request + let event = EventBuilder::new(Kind::Custom(4454), "") + .tags(vec![ + Tag::client(app_name()), + Tag::custom(TagKind::custom("P"), vec![app_pubkey]), + ]) + .sign(&signer) + .await?; + + // Send the event to write relays + client.send_event_to(&urls, &event).await?; + + // Construct a filter to get the approval response event + let filter = Filter::new() + .kind(Kind::Custom(4455)) + .author(public_key) + .since(Timestamp::now()); + + // Subscribe to the approval response event + client.subscribe_to(&urls, vec![filter], None).await?; + + Ok(None) + } + } + }); + + cx.spawn(async move |this, cx| { + match task.await { + Ok(Some(keys)) => { + this.update(cx, |this, cx| { + this.set_dekey(keys, cx); + }) + .ok(); + } + Ok(None) => { + device_state + .update(cx, |this, cx| { + *this = DeviceState::Requesting; + cx.notify(); + }) + .ok(); + } + Err(e) => { + log::error!("Failed to request the encryption key: {e}"); + } }; }) .detach();