From b5ed079a0e8f5cdb423c82650be43be0c787e279 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:10:38 +0700 Subject: [PATCH] chore: improve nip-4e (#194) * update texts * update ui * . * . --- assets/icons/encryption.svg | 3 + crates/coop/src/views/chat/mod.rs | 4 +- crates/registry/src/room.rs | 154 +++++++++++++++--------------- crates/states/src/state/device.rs | 26 +++-- crates/states/src/state/mod.rs | 124 +++++++++++++----------- crates/ui/src/icon.rs | 2 + crates/ui/src/menu/popup_menu.rs | 8 +- locales/app.yml | 16 ++-- 8 files changed, 182 insertions(+), 155 deletions(-) create mode 100644 assets/icons/encryption.svg diff --git a/assets/icons/encryption.svg b/assets/icons/encryption.svg new file mode 100644 index 00000000..4f23a2d9 --- /dev/null +++ b/assets/icons/encryption.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs index 9525167c..4584e09d 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/coop/src/views/chat/mod.rs @@ -1408,8 +1408,8 @@ impl Render for Chat { ) .child(TextInput::new(&self.input)) .child( - Button::new("options") - .icon(IconName::Settings) + Button::new("encryptions") + .icon(IconName::Encryption) .ghost() .large() .popup_menu(move |this, _window, _cx| { diff --git a/crates/registry/src/room.rs b/crates/registry/src/room.rs index e383f956..cd13bd54 100644 --- a/crates/registry/src/room.rs +++ b/crates/registry/src/room.rs @@ -50,14 +50,12 @@ impl Default for SendOptions { #[derive(Debug, Clone)] pub struct SendReport { pub receiver: PublicKey, - pub status: Option>, pub error: Option, - + pub on_hold: Option, + pub encryption: bool, pub relays_not_found: bool, pub device_not_found: bool, - - pub on_hold: Option, } impl SendReport { @@ -67,6 +65,7 @@ impl SendReport { status: None, error: None, on_hold: None, + encryption: false, relays_not_found: false, device_not_found: false, } @@ -87,6 +86,11 @@ impl SendReport { self } + pub fn encryption(mut self) -> Self { + self.encryption = true; + self + } + pub fn relays_not_found(mut self) -> Self { self.relays_not_found = true; self @@ -427,8 +431,15 @@ impl Room { /// Create a new message event (unsigned) pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent { + // Get the app state + let state = app_state(); + let relay_cache = state.relay_cache.read_blocking(); + + // Get current user let registry = Registry::global(cx); let public_key = registry.read(cx).signer_pubkey().unwrap(); + + // Get room's subject let subject = self.subject.clone(); let mut tags = vec![]; @@ -437,7 +448,20 @@ impl Room { // // NOTE: current user will be removed from the list of receivers for (member, _) in self.members.iter() { - tags.push(Tag::public_key(member.to_owned())); + // Get relay hint if available + let relay_url = relay_cache + .get(member) + .and_then(|urls| urls.iter().nth(0).cloned()); + + // Construct a public key tag with relay hint + let tag = TagStandard::PublicKey { + public_key: member.to_owned(), + relay_url, + alias: None, + uppercase: false, + }; + + tags.push(Tag::from_standardized_without_cell(tag)); } // Add subject tag if it's present @@ -452,11 +476,12 @@ impl Room { tags.push(Tag::event(replies[0])) } else { for id in replies { - tags.push(Tag::from_standardized_without_cell(TagStandard::Quote { + let tag = TagStandard::Quote { event_id: id.to_owned(), relay_url: None, public_key: None, - })) + }; + tags.push(Tag::from_standardized_without_cell(tag)) } } @@ -485,91 +510,47 @@ impl Room { let opts = opts.to_owned(); cx.background_spawn(async move { - let states = app_state(); - let client = states.client(); - let device = states.device.read().await.encryption_keys.clone(); + let state = app_state(); + + let client = state.client(); + let relay_cache = state.relay_cache.read().await; + let device = state.device.read().await.encryption.clone(); let user_signer = client.signer().await?; let user_pubkey = user_signer.get_public_key().await?; - // Collect relay hints for all participants (including current user) - let mut participants: Vec = members.keys().cloned().collect(); - - if !participants.contains(&user_pubkey) { - participants.push(user_pubkey); - } - - // Initialize relay cache - let mut relay_cache: HashMap> = HashMap::new(); - - for participant in participants.iter().cloned() { - let urls = states.messaging_relays(participant).await; - relay_cache.insert(participant, urls); - } - - // Update rumor with relay hints for each receiver - let mut rumor = rumor; - let mut tags_with_hints = Vec::new(); - - for tag in rumor.tags.into_iter() { - if let Some(standard) = tag.as_standardized().cloned() { - match standard { - TagStandard::PublicKey { - public_key, - alias, - uppercase, - .. - } => { - let relay_url = relay_cache - .get(&public_key) - .and_then(|urls| urls.first().cloned()); - - let updated = TagStandard::PublicKey { - public_key, - relay_url, - alias, - uppercase, - }; - - tags_with_hints.push(Tag::from_standardized_without_cell(updated)); - } - _ => tags_with_hints.push(tag), - } - } else { - tags_with_hints.push(tag); - } - } - rumor.tags = Tags::from_list(tags_with_hints); - // Remove the current user's public key from the list of receivers // Current user will be handled separately let (public_key, device_pubkey) = members.remove_entry(&user_pubkey).unwrap(); // Determine the signer will be used based on the provided options - let signer = Self::select_signer(&opts.signer_kind, device, user_signer)?; + let (signer, signer_kind) = + Self::select_signer(&opts.signer_kind, device, user_signer)?; // Collect the send reports let mut reports: Vec = vec![]; - for (receiver, device_pubkey) in members.into_iter() { - let urls = relay_cache.get(&receiver).cloned().unwrap_or_default(); + for (user, device_pubkey) in members.into_iter() { + let urls = relay_cache.get(&user).cloned().unwrap_or_default(); // Check if there are any relays to send the message to if urls.is_empty() { - reports.push(SendReport::new(receiver).relays_not_found()); + reports.push(SendReport::new(user).relays_not_found()); continue; } // Skip sending if using encryption keys but device not found if device_pubkey.is_none() && matches!(opts.signer_kind, SignerKind::Encryption) { - reports.push(SendReport::new(receiver).device_not_found()); + reports.push(SendReport::new(user).device_not_found()); continue; } // Determine the receiver based on the signer kind + let receiver = Self::select_receiver(&opts.signer_kind, user, device_pubkey)?; + + // Construct the gift wrap event let rumor = rumor.clone(); - let target = Self::select_receiver(&opts.signer_kind, receiver, device_pubkey); - let event = EventBuilder::gift_wrap(&signer, &target, rumor, vec![]).await?; + let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, []).await?; // Send the event to the messaging relays match client.send_event_to(urls, &event).await { @@ -581,7 +562,7 @@ impl Room { if auth { // Wait for authenticated and resent event successfully for attempt in 0..=SEND_RETRY { - let retry_manager = states.tracker().read().await; + let retry_manager = state.tracker().read().await; let ids = retry_manager.resent_ids(); // Check if event was successfully resent @@ -609,10 +590,12 @@ impl Room { } } + // Select a signer based on previous usage, not from the options + let receiver = Self::select_receiver(&signer_kind, public_key, device_pubkey)?; + // Construct a gift wrap to back up to current user's owned messaging relays let rumor = rumor.clone(); - let target = Self::select_receiver(&opts.signer_kind, public_key, device_pubkey); - let event = EventBuilder::gift_wrap(&signer, &target, rumor, vec![]).await?; + let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, []).await?; // Only send a backup message to current user if sent successfully to others if opts.backup() && reports.iter().all(|r| r.is_sent_success()) { @@ -700,24 +683,37 @@ impl Room { }) } - fn select_signer(kind: &SignerKind, device: Option, user: T) -> Result + fn select_signer( + kind: &SignerKind, + device: Option, + user: T, + ) -> Result<(T, SignerKind), Error> where T: NostrSigner, { + match kind { + SignerKind::Auto => device + .map(|d| (d, SignerKind::Encryption)) + .or(Some((user, SignerKind::User))) + .ok_or_else(|| anyhow!("No signer available")), + SignerKind::Encryption => device + .map(|d| (d, SignerKind::Encryption)) + .ok_or_else(|| anyhow!("No encryption key found")), + SignerKind::User => Ok((user, SignerKind::User)), + } + } + + fn select_receiver( + kind: &SignerKind, + user: PublicKey, + device: Option, + ) -> Result { match kind { SignerKind::Encryption => { - Ok(device.ok_or_else(|| anyhow!("No encryption keys found"))?) + Ok(device.ok_or_else(|| anyhow!("Receiver's encryption key not found"))?) } SignerKind::User => Ok(user), SignerKind::Auto => Ok(device.unwrap_or(user)), } } - - fn select_receiver(kind: &SignerKind, user: PublicKey, device: Option) -> PublicKey { - match kind { - SignerKind::Encryption => device.unwrap(), - SignerKind::User => user, - SignerKind::Auto => device.unwrap_or(user), - } - } } diff --git a/crates/states/src/state/device.rs b/crates/states/src/state/device.rs index 1ff5e902..8870e16e 100644 --- a/crates/states/src/state/device.rs +++ b/crates/states/src/state/device.rs @@ -6,20 +6,34 @@ use nostr_sdk::prelude::*; pub struct Device { /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md /// - /// The client keys that used for communication between devices - pub client_keys: Option>, + /// Client Key that used for communication between devices + pub client: Option>, /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md /// - /// The encryption keys that used for encryption and decryption - pub encryption_keys: Option>, + /// Encryption key used for encryption and decryption instead of the user's identity + pub encryption: Option>, } impl Device { pub fn new() -> Self { Self { - client_keys: None, - encryption_keys: None, + client: None, + encryption: None, } } + + pub fn set_client(&mut self, keys: T) + where + T: NostrSigner, + { + self.client = Some(Arc::new(keys)); + } + + pub fn set_encryption(&mut self, keys: T) + where + T: NostrSigner, + { + self.encryption = Some(Arc::new(keys)); + } } diff --git a/crates/states/src/state/mod.rs b/crates/states/src/state/mod.rs index 4802e934..8c5770f7 100644 --- a/crates/states/src/state/mod.rs +++ b/crates/states/src/state/mod.rs @@ -1,9 +1,8 @@ use std::borrow::Cow; use std::collections::hash_map::DefaultHasher; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, Context, Error}; @@ -32,15 +31,18 @@ pub struct AppState { /// A client to interact with Nostr client: Client, - /// Tracks activity related to Nostr events - event_tracker: RwLock, - /// Signal channel for communication between Nostr and GPUI signal: Signal, /// Ingester channel for processing public keys ingester: Ingester, + /// Tracks activity related to Nostr events + event_tracker: RwLock, + + /// Cache of messaging relays for each public key + pub relay_cache: RwLock>>, + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md pub device: RwLock, @@ -80,6 +82,7 @@ impl AppState { let client = ClientBuilder::default().database(lmdb).opts(opts).build(); let device = RwLock::new(Device::default()); let event_tracker = RwLock::new(EventTracker::default()); + let relay_cache = RwLock::new(HashMap::default()); let signal = Signal::default(); let ingester = Ingester::default(); @@ -88,6 +91,7 @@ impl AppState { client, device, event_tracker, + relay_cache, signal, ingester, initialized_at: Timestamp::now(), @@ -287,15 +291,22 @@ impl AppState { } } Kind::InboxRelays => { + // Only get up to 3 relays + let urls: Vec = nip17::extract_relay_list(event.as_ref()) + .take(3) + .cloned() + .collect(); + // Subscribe to gift wrap events if messaging relays belong to the current user if let Ok(true) = self.is_self_authored(&event).await { - let urls: Vec = - nip17::extract_relay_list(event.as_ref()).cloned().collect(); - if let Err(e) = self.get_messages(event.pubkey, &urls).await { log::error!("Failed to fetch messages: {e}"); } } + + // Cache the relay list for further queries + let mut relay_cache = self.relay_cache.write().await; + relay_cache.entry(event.pubkey).or_default().extend(urls); } Kind::ContactList => { if let Ok(true) = self.is_self_authored(&event).await { @@ -573,7 +584,7 @@ impl AppState { // Initialize the client keys let mut device = self.device.write().await; - device.client_keys = Some(Arc::new(keys)); + device.set_client(keys); Ok(()) } @@ -620,7 +631,7 @@ impl AppState { // Initialize the encryption keys let mut device = self.device.write().await; - device.encryption_keys = Some(Arc::new(keys)); + device.set_encryption(keys); // Store the encryption keys for future use self.set_keys("encryption", secret).await?; @@ -637,6 +648,9 @@ impl AppState { // Send the announcement event to the relays self.client.send_event(&event).await?; + // Re-subscribes to gift wrap events + self.resubscribe_messages().await?; + Ok(()) } @@ -649,7 +663,10 @@ impl AppState { // Check if the encryption keys match the announcement if announcement.public_key() == keys.public_key() { let mut device = self.device.write().await; - device.encryption_keys = Some(Arc::new(keys)); + device.set_encryption(keys); + + // Re-subscribes to gift wrap events + self.resubscribe_messages().await?; Ok(()) } else { @@ -665,13 +682,13 @@ impl AppState { let device = self.device.read().await; // Client Keys are always known at this point - let Some(client_keys) = device.client_keys.as_ref() else { + let Some(client_key) = device.client.as_ref() else { return Err(anyhow!("Client Keys is required")); }; let signer = self.client.signer().await?; let public_key = signer.get_public_key().await?; - let client_pubkey = client_keys.get_public_key().await?; + let client_pubkey = client_key.get_public_key().await?; // Get the encryption keys response from the database first let filter = Filter::new() @@ -691,7 +708,7 @@ impl AppState { .context("Invalid event's tags")?; let payload = event.content.as_str(); - let decrypted = client_keys.nip44_decrypt(&root_device, payload).await?; + let decrypted = client_key.nip44_decrypt(&root_device, payload).await?; let secret = SecretKey::from_hex(&decrypted)?; let keys = Keys::new(secret); @@ -700,7 +717,10 @@ impl AppState { drop(device); let mut device = self.device.write().await; - device.encryption_keys = Some(Arc::new(keys)); + device.set_encryption(keys); + + // Re-subscribes to gift wrap events + self.resubscribe_messages().await?; } None => { // Construct encryption keys request event @@ -743,7 +763,7 @@ impl AppState { let device = self.device.read().await; // Client Keys are always known at this point - let Some(client_keys) = device.client_keys.as_ref() else { + let Some(client_key) = device.client.as_ref() else { return Err(anyhow!("Client Keys is required")); }; @@ -751,7 +771,7 @@ impl AppState { let payload = res.payload(); // Decrypt the payload using the client keys - let decrypted = client_keys.nip44_decrypt(&public_key, payload).await?; + let decrypted = client_key.nip44_decrypt(&public_key, payload).await?; let secret = SecretKey::parse(&decrypted)?; let keys = Keys::new(secret); @@ -759,7 +779,10 @@ impl AppState { drop(device); let mut device = self.device.write().await; - device.encryption_keys = Some(Arc::new(keys)); + device.set_encryption(keys); + + // Re-subscribes to gift wrap events + self.resubscribe_messages().await?; Ok(()) } @@ -771,15 +794,15 @@ impl AppState { let device = self.device.read().await; // Client Keys are always known at this point - let Some(client_keys) = device.client_keys.as_ref() else { + let Some(client_key) = device.client.as_ref() else { return Err(anyhow!("Client Keys is required")); }; let encryption = self.get_keys("encryption").await?; - let client_pubkey = client_keys.get_public_key().await?; + let client_pubkey = client_key.get_public_key().await?; // Encrypt the encryption keys with the client's signer - let payload = client_keys + let payload = client_key .nip44_encrypt(&target, &encryption.secret_key().to_secret_hex()) .await?; @@ -792,7 +815,7 @@ impl AppState { Tag::custom(TagKind::custom("P"), vec![client_pubkey]), Tag::public_key(target), ]) - .sign(client_keys) + .sign(client_key) .await?; // Get the current user's signer and public key @@ -1014,11 +1037,11 @@ impl AppState { } // Try to unwrap with the available signer - if let Ok(unwrapped) = self.try_unwrap_gift(gift_wrap).await { + if let Ok(unwrapped) = self.try_unwrap_gift_wrap(gift_wrap).await { let sender = unwrapped.sender; let mut rumor_unsigned = unwrapped.rumor; - if !self.verify_rumor_sender(sender, &rumor_unsigned) { + if !self.verify_sender(sender, &rumor_unsigned).await { return Err(anyhow!("Invalid rumor")); }; @@ -1035,17 +1058,19 @@ impl AppState { } // Helper method to try unwrapping with different signers - async fn try_unwrap_gift(&self, gift_wrap: &Event) -> Result { + async fn try_unwrap_gift_wrap(&self, gift_wrap: &Event) -> Result { // Try to unwrap with the device's encryption keys first // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - if let Some(signer) = self.device.read().await.encryption_keys.as_ref() { + if let Some(signer) = self.device.read().await.encryption.as_ref() { if let Ok(unwrapped) = UnwrappedGift::from_gift_wrap(signer, gift_wrap).await { return Ok(unwrapped); } } - // Try to unwrap with the user's signer + // Get user's signer let signer = self.client.signer().await?; + + // Try to unwrap with the user's signer if let Ok(unwrapped) = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await { return Ok(unwrapped); } @@ -1087,7 +1112,22 @@ impl AppState { } /// Verify that the sender of a rumor is the same as the sender of the event. - fn verify_rumor_sender(&self, sender: PublicKey, rumor: &UnsignedEvent) -> bool { + async fn verify_sender(&self, sender: PublicKey, rumor: &UnsignedEvent) -> bool { + // If we have encryption keys, verify the sender matches the device's public key + if let Some(keys) = self.device.read().await.encryption.as_ref() { + if let Ok(public_key) = keys.get_public_key().await { + let status = public_key == sender; + // Only return if the status is true + // else fallback to basic sender verification + if status { + return status; + } else { + return rumor.pubkey == sender; + } + } + } + + // Fall back to basic sender verification rumor.pubkey == sender } @@ -1124,31 +1164,3 @@ impl AppState { Ok(Response::new(payload, root_device)) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::app_state; - - #[test] - fn verify_rumor_sender_accepts_matching_sender() { - let state = app_state(); - - let keys = Keys::generate(); - let public_key = keys.public_key(); - let rumor = EventBuilder::text_note("hello").build(public_key); - - assert!(state.verify_rumor_sender(public_key, &rumor)); - } - - #[test] - fn verify_rumor_sender_rejects_mismatched_sender() { - let state = app_state(); - - let sender_keys = Keys::generate(); - let rumor_keys = Keys::generate(); - let rumor = EventBuilder::text_note("spoof").build(rumor_keys.public_key()); - - assert!(!state.verify_rumor_sender(sender_keys.public_key(), &rumor)); - } -} diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 940e190f..d3dd4deb 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -27,6 +27,7 @@ pub enum IconName { Copy, Edit, Ellipsis, + Encryption, Eye, EyeOff, EmojiFill, @@ -90,6 +91,7 @@ impl IconName { Self::Edit => "icons/edit.svg", Self::Ellipsis => "icons/ellipsis.svg", Self::Eye => "icons/eye.svg", + Self::Encryption => "icons/encryption.svg", Self::EmojiFill => "icons/emoji-fill.svg", Self::EyeOff => "icons/eye-off.svg", Self::Info => "icons/info.svg", diff --git a/crates/ui/src/menu/popup_menu.rs b/crates/ui/src/menu/popup_menu.rs index 1fe68479..56639355 100644 --- a/crates/ui/src/menu/popup_menu.rs +++ b/crates/ui/src/menu/popup_menu.rs @@ -909,11 +909,12 @@ impl PopupMenu { window: &mut Window, cx: &mut Context, ) -> impl IntoElement { - let has_icon = self.has_icon; - let selected = self.selected_index == Some(ix); const EDGE_PADDING: Pixels = px(4.); const INNER_PADDING: Pixels = px(8.); + let has_icon = self.has_icon; + let selected = self.selected_index == Some(ix); + let is_submenu = matches!(item, PopupMenuItem::Submenu { .. }); let group_name = format!("popup-menu-item-{ix}"); @@ -954,9 +955,8 @@ impl PopupMenu { h_flex() .cursor_default() .items_center() - .gap_x_1() .font_semibold() - .children(Self::render_icon(has_icon, None, window, cx)) + .text_xs() .child(label.clone()), ), PopupMenuItem::ElementItem { diff --git a/locales/app.yml b/locales/app.yml index a285a344..1b1b1205 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -82,25 +82,25 @@ pending_encryption: label: en: "Wait for Approval" body_1: - en: "Please open %{c} and approve the request for sharing encryption keys. Without access to them, Coop cannot decrypt your messages that are encrypted with encryption keys." + en: "Please open %{c} and approve the request for sharing Encryption Key. Without access to them, Coop cannot decrypt your messages that are encrypted with Encryption Key." body_2: - en: "Or you can click the 'Reset' button to reset the encryption keys." + en: "Or you can click the 'Reset' button to reset the Encryption Key." body_3: - en: "By resetting the encryption keys, you will not be able to view your messages that were encrypted with the old encryption keys." + en: "By resetting the Encryption Key, you will not be able to view your messages that were encrypted with the old Encryption Key." request_encryption: label: - en: "Encryption Keys Request" + en: "Encryption Key Request" body: - en: "You've requested for the encryption keys from:" + en: "You've requested for the encryption Key from:" encryption: notice: - en: "Encryption keys are being generated" + en: "Encryption Key are being generated" success: - en: "Encryption keys have been successfully set up" + en: "Encryption Key have been successfully set up" reinit: - en: "Encryption keys are being reinitialized" + en: "Encryption Key are being reinitialized" auto_update: updating: