diff --git a/Cargo.lock b/Cargo.lock index 8880fd6..74613e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1005,6 +1005,7 @@ version = "0.3.0" dependencies = [ "anyhow", "common", + "device", "flume", "futures", "fuzzy-matcher", @@ -1288,6 +1289,7 @@ dependencies = [ "chat", "chat_ui", "common", + "device", "futures", "gpui", "gpui_tokio", @@ -1607,6 +1609,24 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "device" +version = "0.3.0" +dependencies = [ + "anyhow", + "common", + "flume", + "gpui", + "itertools 0.13.0", + "log", + "nostr-sdk", + "serde", + "serde_json", + "smallvec", + "smol", + "state", +] + [[package]] name = "digest" version = "0.10.7" diff --git a/crates/chat/Cargo.toml b/crates/chat/Cargo.toml index 0c2a465..a37256b 100644 --- a/crates/chat/Cargo.toml +++ b/crates/chat/Cargo.toml @@ -7,6 +7,7 @@ publish.workspace = true [dependencies] common = { path = "../common" } state = { path = "../state" } +device = { path = "../device" } person = { path = "../person" } settings = { path = "../settings" } diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 8d6d031..70a5306 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -7,6 +7,7 @@ use std::time::Duration; use anyhow::{anyhow, Context as AnyhowContext, Error}; use common::EventUtils; +use device::DeviceRegistry; use flume::Sender; use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; @@ -96,7 +97,9 @@ impl ChatRegistry { fn new(cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); let identity = nostr.read(cx).identity(); - let device_signer = nostr.read(cx).device_signer(); + + let device = DeviceRegistry::global(cx); + let device_signer = device.read(cx).device_signer.clone(); // A flag to indicate if the registry is loading let tracking_flag = Arc::new(AtomicBool::new(true)); @@ -172,7 +175,9 @@ impl ChatRegistry { fn handle_notifications(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let device_signer = nostr.read(cx).device_signer().read(cx).clone(); + + let device = DeviceRegistry::global(cx); + let device_signer = device.read(cx).signer(cx); let status = self.tracking_flag.clone(); let tx = self.sender.clone(); diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index df8e9ba..a437497 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -33,6 +33,7 @@ title_bar = { path = "../title_bar" } theme = { path = "../theme" } common = { path = "../common" } state = { path = "../state" } +device = { path = "../device" } key_store = { path = "../key_store" } chat = { path = "../chat" } chat_ui = { path = "../chat_ui" } diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 9277d49..a1fb4de 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -89,6 +89,11 @@ fn main() { // Initialize the nostr client state::init(cx); + // Initialize device signer + // + // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + device::init(cx); + // Initialize settings settings::init(cx); diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml new file mode 100644 index 0000000..e45b973 --- /dev/null +++ b/crates/device/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "device" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +common = { path = "../common" } +state = { path = "../state" } + +gpui.workspace = true +nostr-sdk.workspace = true + +anyhow.workspace = true +itertools.workspace = true +smallvec.workspace = true +smol.workspace = true +log.workspace = true +flume.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/crates/device/src/device.rs b/crates/device/src/device.rs new file mode 100644 index 0000000..f809c38 --- /dev/null +++ b/crates/device/src/device.rs @@ -0,0 +1,62 @@ +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 { + /// 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, +} + +impl From<&Event> for Announcement { + fn from(val: &Event) -> Self { + let public_key = val + .tags + .iter() + .find(|tag| tag.kind().as_str() == "n" || tag.kind().as_str() == "P") + .and_then(|tag| tag.content()) + .and_then(|c| PublicKey::parse(c).ok()) + .unwrap_or(val.pubkey); + + let client_name = val + .tags + .find(TagKind::Client) + .and_then(|tag| tag.content()) + .map(|c| c.to_string()); + + Self::new(public_key, client_name) + } +} + +impl Announcement { + pub fn new(public_key: PublicKey, client_name: Option) -> Self { + Self { + public_key, + client_name, + } + } + + /// 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() + .map(SharedString::from) + .unwrap_or(SharedString::from("Unknown")) + } +} diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs new file mode 100644 index 0000000..6b4d1e1 --- /dev/null +++ b/crates/device/src/lib.rs @@ -0,0 +1,512 @@ +use std::collections::HashSet; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{anyhow, Context as AnyhowContext, Error}; +use common::app_name; +pub use device::*; +use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; +use nostr_sdk::prelude::*; +use smallvec::{smallvec, SmallVec}; +use state::{NostrRegistry, RelayState, GIFTWRAP_SUBSCRIPTION, TIMEOUT}; + +mod device; + +pub fn init(cx: &mut App) { + DeviceRegistry::set_global(cx.new(DeviceRegistry::new), cx); +} + +struct GlobalDeviceRegistry(Entity); + +impl Global for GlobalDeviceRegistry {} + +/// Device Registry +#[derive(Debug)] +pub struct DeviceRegistry { + /// Device signer + /// + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + pub device_signer: Entity>>, + + /// Device state + /// + /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md + state: Entity, + + /// Async tasks + tasks: Vec>>, + + /// Subscriptions + _subscriptions: SmallVec<[Subscription; 1]>, +} + +impl DeviceRegistry { + /// Retrieve the global device registry state + pub fn global(cx: &App) -> Entity { + cx.global::().0.clone() + } + + /// Set the global device registry instance + fn set_global(state: Entity, cx: &mut App) { + cx.set_global(GlobalDeviceRegistry(state)); + } + + /// Create a new device registry instance + fn new(cx: &mut Context) -> Self { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + let identity = nostr.read(cx).identity(); + + let device_signer = cx.new(|_| None); + let state = cx.new(|_| DeviceState::default()); + + // Channel for communication between nostr and gpui + let (tx, rx) = flume::bounded::(100); + + let mut subscriptions = smallvec![]; + let mut tasks = vec![]; + + subscriptions.push( + // Observe the identity entity + cx.observe(&identity, |this, state, cx| { + if state.read(cx).has_public_key() { + if state.read(cx).relay_list_state() == RelayState::Set { + this.get_announcement(cx); + } + if state.read(cx).messaging_relays_state() == RelayState::Set { + this.get_messages(cx); + } + } + }), + ); + + tasks.push( + // Handle nostr notifications + cx.background_spawn(async move { Self::handle_notifications(&client, &tx).await }), + ); + + tasks.push( + // Update GPUI states + cx.spawn(async move |this, cx| { + while let Ok(event) = rx.recv_async().await { + match event.kind { + Kind::Custom(4454) => { + // + } + Kind::Custom(4455) => { + // + } + _ => {} + } + } + + Ok(()) + }), + ); + + Self { + device_signer, + state, + tasks, + _subscriptions: subscriptions, + } + } + + /// Returns the device signer entity + pub fn signer(&self, cx: &App) -> Option> { + self.device_signer.read(cx).clone() + } + + /// Set the decoupled encryption key for the current user + fn set_device_signer(&mut self, signer: S, cx: &mut Context) + where + S: NostrSigner + 'static, + { + self.device_signer.update(cx, |this, cx| { + *this = Some(Arc::new(signer)); + cx.notify(); + }); + self.state.update(cx, |this, cx| { + *this = DeviceState::Set; + cx.notify(); + }); + } + + /// Handle nostr notifications + async fn handle_notifications(client: &Client, tx: &flume::Sender) -> Result<(), Error> { + let mut notifications = client.notifications(); + let mut processed_events = HashSet::new(); + + while let Ok(notification) = notifications.recv().await { + if let RelayPoolNotification::Message { + message: RelayMessage::Event { event, .. }, + .. + } = notification + { + if !processed_events.insert(event.id) { + // Skip if the event has already been processed + continue; + } + + match event.kind { + Kind::Custom(4454) => { + if Self::verify_author(client, event.as_ref()).await { + tx.send_async(event.into_owned()).await.ok(); + } + } + Kind::Custom(4455) => { + if Self::verify_author(client, event.as_ref()).await { + tx.send_async(event.into_owned()).await.ok(); + } + } + _ => {} + } + } + } + + Ok(()) + } + + /// Verify the author of an event + async fn verify_author(client: &Client, event: &Event) -> bool { + if let Ok(signer) = client.signer().await { + if let Ok(public_key) = signer.get_public_key().await { + return public_key == event.pubkey; + } + } + false + } + + /// Continuously get gift wrap events for the current user in their messaging relays + fn get_messages(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + let device_signer = self.device_signer.read(cx).clone(); + + let public_key = nostr.read(cx).identity().read(cx).public_key(); + let messaging_relays = nostr.read(cx).messaging_relays(&public_key, cx); + + cx.background_spawn(async move { + let urls = messaging_relays.await; + let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION); + let mut filters = vec![]; + + // 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) = device_signer.as_ref() { + 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(); + } + + /// Get device announcement for current user + fn get_announcement(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + let public_key = nostr.read(cx).identity().read(cx).public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + + let task: Task> = cx.background_spawn(async move { + let urls = write_relays.await; + + // Construct the filter for the device announcement event + let filter = Filter::new() + .kind(Kind::Custom(10044)) + .author(public_key) + .limit(1); + + let mut stream = client + .stream_events_from(&urls, vec![filter], Duration::from_secs(TIMEOUT)) + .await?; + + while let Some((_url, res)) = stream.next().await { + match res { + Ok(event) => { + log::info!("Received device announcement event: {event:?}"); + return Ok(event); + } + Err(e) => { + log::error!("Failed to receive device announcement event: {e}"); + } + } + } + + Err(anyhow!("Device announcement not found")) + }); + + self.tasks.push(cx.spawn(async move |this, cx| { + match task.await { + Ok(event) => { + this.update(cx, |this, cx| { + this.init_device_signer(&event, cx); + })?; + } + Err(_) => { + this.update(cx, |this, cx| { + this.announce_device(cx); + })?; + } + } + + Ok(()) + })); + } + + /// Create a new device signer and announce it + fn announce_device(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + let public_key = nostr.read(cx).identity().read(cx).public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + + let keys = Keys::generate(); + let secret = keys.secret_key().to_secret_hex(); + let n = keys.public_key(); + + let task: Task> = cx.background_spawn(async move { + let signer = client.signer().await?; + let urls = write_relays.await; + + // Construct an announcement event + let event = EventBuilder::new(Kind::Custom(10044), "") + .tags(vec![ + Tag::custom(TagKind::custom("n"), vec![n]), + Tag::client(app_name()), + ]) + .sign(&signer) + .await?; + + // Publish announcement + client.send_event_to(&urls, &event).await?; + + // Encrypt the secret key + let encrypted = signer.nip44_encrypt(&public_key, &secret).await?; + + // Construct a storage event + let event = EventBuilder::new(Kind::ApplicationSpecificData, encrypted) + .tag(Tag::identifier("coop:device")) + .sign(&signer) + .await?; + + // Save storage event to database + // + // Note: never publish to any relays + client.database().save_event(&event).await?; + + Ok(()) + }); + + cx.spawn(async move |this, cx| { + if task.await.is_ok() { + this.update(cx, |this, cx| { + this.set_device_signer(keys, cx); + this.listen_device_request(cx); + }) + .ok(); + } + }) + .detach(); + } + + /// Initialize device signer (decoupled encryption key) for the current user + fn init_device_signer(&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(); + + let task: Task> = cx.background_spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + let filter = Filter::new() + .identifier("coop:device") + .kind(Kind::ApplicationSpecificData) + .author(public_key) + .limit(1); + + 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 keys.public_key() != device_pubkey { + return Err(anyhow!("Key mismatch")); + }; + + Ok(keys) + } else { + Err(anyhow!("Key not found")) + } + }); + + cx.spawn(async move |this, cx| { + match task.await { + Ok(keys) => { + this.update(cx, |this, cx| { + this.set_device_signer(keys, cx); + this.listen_device_request(cx); + }) + .ok(); + } + Err(e) => { + this.update(cx, |this, cx| { + this.request_device_keys(cx); + this.listen_device_approval(cx); + }) + .ok(); + + log::warn!("Failed to initialize device signer: {e}"); + } + }; + }) + .detach(); + } + + /// Listen for device key requests on user's write relays + fn listen_device_request(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + let public_key = nostr.read(cx).identity().read(cx).public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + + let task: Task> = cx.background_spawn(async move { + let urls = write_relays.await; + + // Construct a filter for device key requests + let filter = 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_to(&urls, vec![filter], None).await?; + + Ok(()) + }); + + task.detach(); + } + + /// Listen for device key approvals on user's write relays + fn listen_device_approval(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + let public_key = nostr.read(cx).identity().read(cx).public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + + let task: Task> = cx.background_spawn(async move { + let urls = write_relays.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_to(&urls, vec![filter], None).await?; + + Ok(()) + }); + + task.detach(); + } + + /// Request encryption keys from other device + fn request_device_keys(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + let public_key = nostr.read(cx).identity().read(cx).public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + + let app_keys = nostr.read(cx).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?; + + Ok(None) + } + } + }); + + cx.spawn(async move |this, cx| { + match task.await { + Ok(Some(keys)) => { + this.update(cx, |this, cx| { + this.set_device_signer(keys, cx); + }) + .ok(); + } + Ok(None) => { + this.update(cx, |this, cx| { + this.state.update(cx, |this, cx| { + *this = DeviceState::Requesting; + cx.notify(); + }); + }) + .ok(); + } + Err(e) => { + log::error!("Failed to request the encryption key: {e}"); + } + }; + }) + .detach(); + } +} diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 66e882a..2b1699e 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,9 +1,8 @@ use std::collections::HashSet; -use std::sync::Arc; use std::time::Duration; -use anyhow::{anyhow, Context as AnyhowContext, Error}; -use common::{app_name, config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; +use anyhow::Error; +use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_lmdb::NostrLmdb; use nostr_sdk::prelude::*; @@ -53,16 +52,6 @@ pub struct NostrRegistry { /// Gossip implementation gossip: Entity, - /// Device signer - /// - /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - device_signer: 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>>, @@ -120,10 +109,6 @@ impl NostrRegistry { // Construct the identity entity let identity = cx.new(|_| Identity::default()); - // Construct the device signer entity - let device_signer = cx.new(|_| None); - let device_state = cx.new(|_| DeviceState::default()); - // Channel for communication between nostr and gpui let (tx, rx) = flume::bounded::(2048); @@ -139,16 +124,9 @@ impl NostrRegistry { this.get_relay_list(cx); } RelayState::Set => { - match state.read(cx).messaging_relays_state() { - RelayState::Initial => { - this.get_profile(cx); - this.get_announcement(cx); - this.get_messaging_relays(cx); - } - RelayState::Set => { - this.get_messages(cx); - } - _ => {} + if state.read(cx).messaging_relays_state() == RelayState::Initial { + this.get_profile(cx); + this.get_messaging_relays(cx); }; } _ => {} @@ -196,8 +174,6 @@ impl NostrRegistry { app_keys, identity, gossip, - device_signer, - device_state, _subscriptions: subscriptions, tasks, } @@ -352,11 +328,6 @@ impl NostrRegistry { self.identity.clone() } - /// Get current device signer - pub fn device_signer(&self) -> Entity>> { - self.device_signer.clone() - } - /// Get a relay hint (messaging relay) for a given public key pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Option { self.gossip @@ -568,58 +539,6 @@ impl NostrRegistry { task.detach(); } - /// Get device announcement for current user - fn get_announcement(&mut self, cx: &mut Context) { - let client = self.client(); - let public_key = self.identity().read(cx).public_key(); - let write_relays = self.write_relays(&public_key, cx); - - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; - - // Construct the filter for the device announcement event - let filter = Filter::new() - .kind(Kind::Custom(10044)) - .author(public_key) - .limit(1); - - let mut stream = client - .stream_events_from(&urls, vec![filter], Duration::from_secs(TIMEOUT)) - .await?; - - while let Some((_url, res)) = stream.next().await { - match res { - Ok(event) => { - log::info!("Received device announcement event: {event:?}"); - return Ok(event); - } - Err(e) => { - log::error!("Failed to receive device announcement event: {e}"); - } - } - } - - Err(anyhow!("Device announcement not found")) - }); - - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(event) => { - this.update(cx, |this, cx| { - this.init_device_signer(&event, cx); - })?; - } - Err(_) => { - this.update(cx, |this, cx| { - this.announce_device(cx); - })?; - } - } - - Ok(()) - })); - } - /// Get messaging relays for current user fn get_messaging_relays(&mut self, cx: &mut Context) { let client = self.client(); @@ -674,285 +593,4 @@ impl NostrRegistry { Ok(()) })); } - - /// Continuously get gift wrap events for the current user in their messaging relays - fn get_messages(&mut self, cx: &mut Context) { - let client = self.client(); - let device_signer = self.device_signer().read(cx).clone(); - let public_key = self.identity().read(cx).public_key(); - let messaging_relays = self.messaging_relays(&public_key, cx); - - cx.background_spawn(async move { - let urls = messaging_relays.await; - let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION); - let mut filters = vec![]; - - // 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) = device_signer.as_ref() { - 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(); - } - - /// Set the decoupled encryption key for the current user - fn set_device_signer(&mut self, signer: S, cx: &mut Context) - where - S: NostrSigner + 'static, - { - self.device_signer.update(cx, |this, cx| { - *this = Some(Arc::new(signer)); - cx.notify(); - }); - self.device_state.update(cx, |this, cx| { - *this = DeviceState::Set; - cx.notify(); - }); - } - - /// Create a new device signer and announce it - fn announce_device(&mut self, cx: &mut Context) { - let client = self.client(); - let public_key = self.identity().read(cx).public_key(); - let write_relays = self.write_relays(&public_key, cx); - - let keys = Keys::generate(); - let secret = keys.secret_key().to_secret_hex(); - let n = keys.public_key(); - - let task: Task> = cx.background_spawn(async move { - let signer = client.signer().await?; - let urls = write_relays.await; - - // Construct an announcement event - let event = EventBuilder::new(Kind::Custom(10044), "") - .tags(vec![ - Tag::custom(TagKind::custom("n"), vec![n]), - Tag::client(app_name()), - ]) - .sign(&signer) - .await?; - - // Publish announcement - client.send_event_to(&urls, &event).await?; - - // Encrypt the secret key - let encrypted = signer.nip44_encrypt(&public_key, &secret).await?; - - // Construct a storage event - let event = EventBuilder::new(Kind::ApplicationSpecificData, encrypted) - .tag(Tag::identifier("coop:device")) - .sign(&signer) - .await?; - - // Save storage event to database - // - // Note: never publish to any relays - client.database().save_event(&event).await?; - - Ok(()) - }); - - cx.spawn(async move |this, cx| { - if task.await.is_ok() { - this.update(cx, |this, cx| { - this.set_device_signer(keys, cx); - this.listen_device_request(cx); - }) - .ok(); - } - }) - .detach(); - } - - /// Initialize device signer (decoupled encryption key) for the current user - fn init_device_signer(&mut self, event: &Event, cx: &mut Context) { - let client = self.client(); - let announcement = Announcement::from(event); - let device_pubkey = announcement.public_key(); - - let task: Task> = cx.background_spawn(async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - let filter = Filter::new() - .identifier("coop:device") - .kind(Kind::ApplicationSpecificData) - .author(public_key) - .limit(1); - - 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 keys.public_key() != device_pubkey { - return Err(anyhow!("Key mismatch")); - }; - - Ok(keys) - } else { - Err(anyhow!("Key not found")) - } - }); - - cx.spawn(async move |this, cx| { - match task.await { - Ok(keys) => { - this.update(cx, |this, cx| { - this.set_device_signer(keys, cx); - this.listen_device_request(cx); - }) - .ok(); - } - Err(e) => { - log::warn!("Failed to initialize dekey: {e}"); - this.update(cx, |this, cx| { - this.request_device_keys(cx); - }) - .ok(); - } - }; - }) - .detach(); - } - - /// Listen for device key requests on user's write relays - fn listen_device_request(&mut self, cx: &mut Context) { - let client = self.client(); - let public_key = self.identity().read(cx).public_key(); - let write_relays = self.write_relays(&public_key, cx); - - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; - - // Construct a filter for device key requests - let filter = 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_to(&urls, vec![filter], None).await?; - - Ok(()) - }); - - task.detach(); - } - - /// Listen for device key approvals on user's write relays - fn listen_device_approval(&mut self, cx: &mut Context) { - let client = self.client(); - let public_key = self.identity().read(cx).public_key(); - let write_relays = self.write_relays(&public_key, cx); - - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.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_to(&urls, vec![filter], None).await?; - - Ok(()) - }); - - task.detach(); - } - - /// Request encryption keys from other device - fn request_device_keys(&mut self, cx: &mut Context) { - let client = self.client(); - 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?; - - Ok(None) - } - } - }); - - cx.spawn(async move |this, cx| { - match task.await { - Ok(Some(keys)) => { - this.update(cx, |this, cx| { - this.set_device_signer(keys, cx); - }) - .ok(); - } - Ok(None) => { - this.update(cx, |this, cx| { - this.device_state.update(cx, |this, cx| { - *this = DeviceState::Requesting; - cx.notify(); - }); - this.listen_device_approval(cx); - }) - .ok(); - } - Err(e) => { - log::error!("Failed to request the encryption key: {e}"); - } - }; - }) - .detach(); - } }