nip4e: fix message encryption and sync #57

Merged
reya merged 10 commits from fix-nip4e into master 2026-10-04 06:33:33 +00:00
17 changed files with 520 additions and 246 deletions
Generated
+25 -24
View File
@@ -1073,7 +1073,6 @@ version = "1.0.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
"device",
"flume 0.11.1", "flume 0.11.1",
"futures", "futures",
"fuzzy-matcher", "fuzzy-matcher",
@@ -1081,8 +1080,10 @@ dependencies = [
"instant", "instant",
"itertools 0.13.0", "itertools 0.13.0",
"log", "log",
"nip4e",
"nostr-sdk", "nostr-sdk",
"person", "person",
"rand 0.10.3",
"settings", "settings",
"smallvec", "smallvec",
"smol", "smol",
@@ -1413,7 +1414,6 @@ dependencies = [
"auto_update", "auto_update",
"chat", "chat",
"common", "common",
"device",
"gpui-pre", "gpui-pre",
"gpui-pre-linux", "gpui-pre-linux",
"gpui-pre-macos", "gpui-pre-macos",
@@ -1422,6 +1422,7 @@ dependencies = [
"gpui-pre-windows", "gpui-pre-windows",
"log", "log",
"nip29", "nip29",
"nip4e",
"nostr-sdk", "nostr-sdk",
"person", "person",
"settings", "settings",
@@ -1441,7 +1442,6 @@ dependencies = [
"common", "common",
"console_error_panic_hook", "console_error_panic_hook",
"console_log", "console_log",
"device",
"getrandom 0.2.17", "getrandom 0.2.17",
"getrandom 0.3.4", "getrandom 0.3.4",
"getrandom 0.4.3", "getrandom 0.4.3",
@@ -1451,6 +1451,7 @@ dependencies = [
"instant", "instant",
"log", "log",
"nip29", "nip29",
"nip4e",
"person", "person",
"settings", "settings",
"state", "state",
@@ -1823,26 +1824,6 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "device"
version = "1.0.2"
dependencies = [
"anyhow",
"common",
"flume 0.11.1",
"gpui-pre",
"instant",
"log",
"nostr-sdk",
"person",
"settings",
"smallvec",
"smol",
"state",
"theme",
"ui",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@@ -4577,6 +4558,26 @@ dependencies = [
"ui", "ui",
] ]
[[package]]
name = "nip4e"
version = "1.0.2"
dependencies = [
"anyhow",
"common",
"flume 0.11.1",
"gpui-pre",
"instant",
"log",
"nostr-sdk",
"person",
"settings",
"smallvec",
"smol",
"state",
"theme",
"ui",
]
[[package]] [[package]]
name = "no_std_io2" name = "no_std_io2"
version = "0.9.4" version = "0.9.4"
@@ -9438,12 +9439,12 @@ dependencies = [
"chat", "chat",
"chat_ui", "chat_ui",
"common", "common",
"device",
"gpui-pre", "gpui-pre",
"instant", "instant",
"log", "log",
"nip29", "nip29",
"nip29_ui", "nip29_ui",
"nip4e",
"nostr-connect", "nostr-connect",
"nostr-sdk", "nostr-sdk",
"person", "person",
+1 -1
View File
@@ -44,7 +44,7 @@ hkdf = "0.12"
chacha20 = "0.9" chacha20 = "0.9"
hmac = "0.12" hmac = "0.12"
# Pinned to the instance `nostr-sdk` already builds, so NIP-44 nonces share it # Pinned to the instance `nostr-sdk` already builds, so NIP-44 nonces share it
rand = { version = "0.10", default-features = false, features = [ "std", "sys_rng" ] } rand = { version = "0.10", default-features = false, features = [ "std", "sys_rng", "thread_rng" ] }
# Others # Others
anyhow = "1.0.44" anyhow = "1.0.44"
+2 -1
View File
@@ -7,7 +7,7 @@ publish.workspace = true
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
state = { path = "../state" } state = { path = "../state" }
device = { path = "../device" } nip4e = { path = "../nip4e" }
person = { path = "../person" } person = { path = "../person" }
settings = { path = "../settings" } settings = { path = "../settings" }
@@ -17,6 +17,7 @@ instant.workspace = true
anyhow.workspace = true anyhow.workspace = true
itertools.workspace = true itertools.workspace = true
rand.workspace = true
smallvec.workspace = true smallvec.workspace = true
log.workspace = true log.workspace = true
flume.workspace = true flume.workspace = true
+105 -10
View File
@@ -12,6 +12,7 @@ use gpui::{
WeakEntity, WeakEntity,
}; };
use instant::Duration; use instant::Duration;
use nip4e::{DeviceEvent, DeviceRegistry};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP}; use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP};
@@ -138,17 +139,24 @@ impl ChatRegistry {
let (tx, rx) = flume::unbounded::<Signal>(); let (tx, rx) = flume::unbounded::<Signal>();
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
subscriptions.push( let device = DeviceRegistry::global(cx);
// Subscribe to the signer event
cx.subscribe(&nostr, |this, _nostr, event, cx| { subscriptions.push(cx.subscribe(&nostr, |this, _nostr, event, cx| {
if event.signer_changed() { if event.signer_changed() {
this.reset(cx); this.reset(cx);
this.handle_notifications(cx); this.handle_notifications(cx);
this.get_metadata(cx); this.get_metadata(cx);
this.get_rooms(cx); this.get_rooms(cx);
}; };
}), }));
);
subscriptions.push(cx.subscribe(&device, |this, _device, event, cx| {
if matches!(event, DeviceEvent::Set) {
this.handle_notifications(cx);
this.get_messages(cx);
this.reprocess_trash(cx);
}
}));
cx.defer(move |cx| { cx.defer(move |cx| {
let _ = entity.update(cx, |this, cx| { let _ = entity.update(cx, |this, cx| {
@@ -183,6 +191,9 @@ impl ChatRegistry {
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let device = DeviceRegistry::global(cx);
let device_signer = device.read(cx).signer(cx);
let seen = self.seen.clone(); let seen = self.seen.clone();
let event_map = self.event_map.clone(); let event_map = self.event_map.clone();
let trash = self.trash.downgrade(); let trash = self.trash.downgrade();
@@ -249,7 +260,14 @@ impl ChatRegistry {
} }
// Extract the rumor from the gift wrap event // Extract the rumor from the gift wrap event
match extract_rumor(&client, &signer, event.as_ref()).await { match extract_rumor(
&client,
&signer,
device_signer.as_ref(),
event.as_ref(),
)
.await
{
Ok(rumor) => { Ok(rumor) => {
let Some(rumor_id) = rumor.id else { let Some(rumor_id) = rumor.id else {
log::error!("Rumor missing id after ensure_id"); log::error!("Rumor missing id after ensure_id");
@@ -385,6 +403,9 @@ impl ChatRegistry {
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let device = DeviceRegistry::global(cx);
let device_signer = device.read(cx).signer(cx);
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let public_key = signer.get_public_key_async().await?; let public_key = signer.get_public_key_async().await?;
@@ -417,6 +438,20 @@ impl ChatRegistry {
client.subscribe(target).with_id(id).await?; client.subscribe(target).with_id(id).await?;
// Also pick up gift wraps addressed to this account's decoupled encryption key
if let Some(device_signer) = device_signer.as_ref() {
let device_public_key = device_signer.get_public_key_async().await?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(device_public_key);
let id = SubscriptionId::new(DEVICE_GIFTWRAP);
let target: HashMap<RelayUrl, Filter> = nip17::extract_relay_list(&event)
.map(|relay| (relay, filter.clone()))
.collect();
client.subscribe(target).with_id(id).await?;
}
Ok(()) Ok(())
}); });
@@ -430,6 +465,49 @@ impl ChatRegistry {
})); }));
} }
/// Retry unwrapping trashed gift wraps with the newly available encryption key.
fn reprocess_trash(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let device_signer = DeviceRegistry::global(cx).read(cx).signer(cx);
let failed: Vec<FailedMessage> = self.trash.read(cx).iter().cloned().collect();
if failed.is_empty() {
return;
}
self.tasks.push(cx.spawn(async move |this, cx| {
for message in failed {
let Ok(event) = Event::from_json(message.raw_event.as_str()) else {
continue;
};
if extract_rumor(&client, &signer, device_signer.as_ref(), &event)
.await
.is_err()
{
continue;
}
let Ok(rumor) = get_rumor(&client, event.id).await else {
continue;
};
this.update(cx, |this, cx| {
this.trash.update(cx, |trash, cx| {
trash.remove(&message);
cx.notify();
});
this.new_message(NewMessage::new(event.id, rumor), cx);
})?;
}
Ok(())
}));
}
/// Reload the chat registry, fetching messages and contact list from relays. /// Reload the chat registry, fetching messages and contact list from relays.
pub fn reload(&mut self, cx: &mut Context<Self>) { pub fn reload(&mut self, cx: &mut Context<Self>) {
self.reset(cx); self.reset(cx);
@@ -740,21 +818,30 @@ impl ChatRegistry {
} }
/// Unwraps a gift-wrapped event and processes its contents. /// Unwraps a gift-wrapped event and processes its contents.
async fn extract_rumor<S>( async fn extract_rumor<S, D>(
client: &Client, client: &Client,
signer: &S, signer: &S,
device_signer: Option<&D>,
gift_wrap: &Event, gift_wrap: &Event,
) -> Result<UnsignedEvent, Error> ) -> Result<UnsignedEvent, Error>
where where
S: AsyncNip44 + ?Sized, S: AsyncNip44 + ?Sized,
D: AsyncNip44 + ?Sized,
{ {
// Try to get cached rumor first // Try to get cached rumor first
if let Ok(rumor) = get_rumor(client, gift_wrap.id).await { if let Ok(rumor) = get_rumor(client, gift_wrap.id).await {
return Ok(rumor); return Ok(rumor);
} }
// Try to unwrap with the available signer let unwrapped = match try_unwrap_with(signer, gift_wrap).await {
let unwrapped = try_unwrap_with(signer, gift_wrap).await?; Ok(unwrapped) => unwrapped,
Err(error) => {
let Some(device_signer) = device_signer else {
return Err(error);
};
try_unwrap_with(device_signer, gift_wrap).await?
}
};
let mut rumor = unwrapped.rumor; let mut rumor = unwrapped.rumor;
// Verify rumor author matches the seal sender (as per mobile implementation) // Verify rumor author matches the seal sender (as per mobile implementation)
@@ -787,9 +874,17 @@ where
let seal: Event = Event::from_json(seal)?; let seal: Event = Event::from_json(seal)?;
seal.verify()?; seal.verify()?;
let sender_encryption_key = seal
.tags
.iter()
.find(|tag| tag.kind() == "n")
.and_then(|tag| tag.content())
.and_then(|content| PublicKey::parse(content).ok())
.unwrap_or(seal.pubkey);
// Get the rumor event // Get the rumor event
let rumor = signer let rumor = signer
.nip44_decrypt_async(&seal.pubkey, &seal.content) .nip44_decrypt_async(&sender_encryption_key, &seal.content)
.await?; .await?;
let rumor = UnsignedEvent::from_json(rumor)?; let rumor = UnsignedEvent::from_json(rumor)?;
+70 -40
View File
@@ -3,19 +3,19 @@ use std::hash::{Hash, Hasher};
use anyhow::{Error, anyhow}; use anyhow::{Error, anyhow};
use common::EventExt; use common::EventExt;
use device::DeviceRegistry;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task}; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
use instant::Duration; use instant::Duration;
use itertools::Itertools; use itertools::Itertools;
use nip4e::DeviceRegistry;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry}; use person::{Person, PersonRegistry};
use settings::{RoomConfig, SignerKind}; use settings::{RoomConfig, SignerKind};
use state::{NostrRegistry, TIMEOUT}; use state::{NostrRegistry, TIMEOUT, UniversalSigner};
use crate::{FileAttachment, KIND_FILE_MESSAGE, NewMessage}; use crate::{FileAttachment, KIND_FILE_MESSAGE, NewMessage};
const NO_DEKEY: &str = "User hasn't set up a decoupled encryption key yet."; const NO_DEKEY: &str = "User hasn't set up a decoupled encryption key yet.";
const USER_NO_DEKEY: &str = "You haven't set up a decoupled encryption key or it's not available."; const USER_NO_DEKEY: &str = "You haven't set up a decoupled encryption key yet. Set one up in the settings to message them.";
/// The outcome of delivering an encrypted rumor to a single receiver. /// The outcome of delivering an encrypted rumor to a single receiver.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -472,28 +472,6 @@ impl Room {
tags tags
} }
/// Select the appropriate signer based on signer kind and available keys.
fn select_signer<'a, S>(
signer_kind: &SignerKind,
has_announcement: bool,
encryption_signer: &'a Option<S>,
user_signer: &'a S,
) -> &'a S {
match signer_kind {
SignerKind::Auto => {
if has_announcement {
encryption_signer.as_ref().unwrap_or(user_signer)
} else {
user_signer
}
}
SignerKind::Encryption => encryption_signer
.as_ref()
.expect("encryption signer must be set"),
SignerKind::User => user_signer,
}
}
/// Send the rumor to every member's messaging relays. /// Send the rumor to every member's messaging relays.
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> { pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
let config = self.config.clone(); let config = self.config.clone();
@@ -528,21 +506,23 @@ impl Room {
signer_kind, signer_kind,
receiver, receiver,
&rumor, &rumor,
&encryption_signer, encryption_signer.as_ref(),
&user_signer, &user_signer,
) )
}); });
let mut reports: Vec<SendReport> = futures::future::join_all(sends).await; let mut reports: Vec<SendReport> = futures::future::join_all(sends).await;
let minimum_success = reports.iter().any(SendReport::success);
// Back up the message to ourselves once at least one receiver got it // Back up the message to ourselves once at least one receiver got it
if config.backup() && reports.iter().any(SendReport::success) { if config.backup() && minimum_success {
reports.push( reports.push(
deliver( deliver(
&client, &client,
signer_kind, signer_kind,
sender, sender,
&rumor, &rumor,
&encryption_signer, encryption_signer.as_ref(),
&user_signer, &user_signer,
) )
.await, .await,
@@ -560,7 +540,7 @@ async fn deliver<S>(
signer_kind: &SignerKind, signer_kind: &SignerKind,
receiver: Person, receiver: Person,
rumor: &UnsignedEvent, rumor: &UnsignedEvent,
encryption_signer: &Option<S>, encryption_signer: Option<&UniversalSigner>,
user_signer: &S, user_signer: &S,
) -> SendReport ) -> SendReport
where where
@@ -587,15 +567,16 @@ where
} }
} }
// Determine the signer to use match send_gift_wrap(
let signer = Room::select_signer( client,
&receiver,
rumor,
signer_kind, signer_kind,
announcement.is_some(),
encryption_signer, encryption_signer,
user_signer, user_signer,
); )
.await
match send_gift_wrap(client, signer, &receiver, rumor, signer_kind).await { {
Ok(output) => SendReport { Ok(output) => SendReport {
receiver: public_key, receiver: public_key,
result: Ok(output), result: Ok(output),
@@ -610,10 +591,11 @@ where
/// Build the gift-wrapped event for a rumor and send it. /// Build the gift-wrapped event for a rumor and send it.
async fn send_gift_wrap<S>( async fn send_gift_wrap<S>(
client: &Client, client: &Client,
signer: &S,
receiver: &Person, receiver: &Person,
rumor: &UnsignedEvent, rumor: &UnsignedEvent,
config: &SignerKind, config: &SignerKind,
encryption_signer: Option<&UniversalSigner>,
user_signer: &S,
) -> Result<Output<EventId, EventSendStatus>, Error> ) -> Result<Output<EventId, EventSendStatus>, Error>
where where
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44, S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44,
@@ -639,12 +621,52 @@ where
SignerKind::User => receiver.public_key(), SignerKind::User => receiver.public_key(),
}; };
// Construct the gift wrap event // Make sure the rumor carries an event ID before it's serialized
let event = nip59::GiftWrapBuilder::new(receiver_key, rumor.clone()) let mut rumor = rumor.clone();
.extra_tags(extra_tags) rumor.ensure_id();
.finalize_async(signer)
let rumor_json = rumor.as_json();
let has_encryption_announcement = receiver.announcement().is_some();
let seal: Event = match (encryption_signer, has_encryption_announcement) {
(Some(encryption_signer), true) => {
let encryption_pubkey = encryption_signer.get_public_key_async().await?;
let content = encryption_signer
.nip44_encrypt_async(&receiver_key, &rumor_json)
.await?; .await?;
EventBuilder::new(Kind::Seal, content)
.tags(vec![Tag::custom("n", [encryption_pubkey.to_hex()])])
.custom_created_at(tweaked_timestamp())
.finalize_async(user_signer)
.await?
}
_ => {
let content = user_signer
.nip44_encrypt_async(&receiver_key, &rumor_json)
.await?;
EventBuilder::new(Kind::Seal, content)
.custom_created_at(tweaked_timestamp())
.finalize_async(user_signer)
.await?
}
};
let ephemeral_keys = Keys::generate();
let content = nip44::encrypt(
ephemeral_keys.secret_key(),
&receiver_key,
seal.as_json(),
nip44::Version::default(),
)?;
extra_tags.push(Tag::public_key(receiver_key));
let event = EventBuilder::new(Kind::GiftWrap, content)
.tags(extra_tags)
.custom_created_at(tweaked_timestamp())
.finalize(&ephemeral_keys)?;
// Send to the receiver's NIP-17 relays. // Send to the receiver's NIP-17 relays.
client client
.send_event(&event) .send_event(&event)
@@ -652,3 +674,11 @@ where
.await .await
.map_err(Into::into) .map_err(Into::into)
} }
/// A tweaked timestamp to thwart time-analysis attacks, per NIP-59
fn tweaked_timestamp() -> Timestamp {
const MAX_TWEAK_SECONDS: u64 = 2 * 24 * 60 * 60;
let seconds = rand::random_range(0..=MAX_TWEAK_SECONDS);
Timestamp::from_secs(Timestamp::now().as_secs().saturating_sub(seconds))
}
+37 -8
View File
@@ -126,6 +126,7 @@ impl GroupsRegistry {
if event.signer_changed() { if event.signer_changed() {
this.reset(cx); this.reset(cx);
this.handle_notifications(cx); this.handle_notifications(cx);
this.load_cached(cx);
this.load(cx); this.load(cx);
} }
})); }));
@@ -133,6 +134,7 @@ impl GroupsRegistry {
cx.defer(move |cx| { cx.defer(move |cx| {
if let Err(error) = this.update(cx, |this, cx| { if let Err(error) = this.update(cx, |this, cx| {
this.handle_notifications(cx); this.handle_notifications(cx);
this.load_cached(cx);
if nostr.read(cx).current_user().is_some() { if nostr.read(cx).current_user().is_some() {
this.load(cx); this.load(cx);
@@ -349,20 +351,21 @@ impl GroupsRegistry {
cx.notify(); cx.notify();
} }
fn load(&mut self, cx: &mut Context<Self>) { fn load_cached(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
if nostr.read(cx).current_user().is_none() {
return;
}
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
// Show the cached groups before the relays answer.
let task: Task<Result<GroupList, Error>> = cx.background_spawn(async move { let task: Task<Result<GroupList, Error>> = cx.background_spawn(async move {
let me = signer.get_public_key_async().await?; let me = signer.get_public_key_async().await?;
let filter = Filter::new().author(me).kind(Kind::SimpleGroups).limit(1); let filter = Filter::new().author(me).kind(Kind::SimpleGroups).limit(1);
client
.subscribe(ReqTarget::auto([filter.clone()]))
.with_id(list_subscription_id())
.await?;
match client.database().query(filter).await?.into_iter().next() { match client.database().query(filter).await?.into_iter().next() {
Some(event) => GroupList::parse(&event), Some(event) => GroupList::parse(&event),
None => Ok(GroupList::default()), None => Ok(GroupList::default()),
@@ -373,10 +376,36 @@ impl GroupsRegistry {
.push(cx.spawn(async move |this, cx| match task.await { .push(cx.spawn(async move |this, cx| match task.await {
Ok(list) => { Ok(list) => {
if let Err(error) = this.update(cx, |this, cx| this.merge(list, cx)) { if let Err(error) = this.update(cx, |this, cx| this.merge(list, cx)) {
log::warn!("nip29: applying the group list failed: {error}"); log::warn!("nip29: applying the cached group list failed: {error}");
} }
} }
Err(error) => log::warn!("nip29: loading the group list failed: {error}"), Err(error) => log::warn!("nip29: loading the cached group list failed: {error}"),
}));
}
fn load(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let updates: Task<Result<(), Error>> = cx.background_spawn(async move {
let me = signer.get_public_key_async().await?;
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let filter = Filter::new().author(me).kind(Kind::SimpleGroups).limit(1);
client
.subscribe(ReqTarget::auto([filter]))
.with_id(list_subscription_id())
.close_on(opts)
.await?;
Ok(())
});
self.tasks.push(cx.spawn(async move |_this, _cx| {
if let Err(error) = updates.await {
log::warn!("nip29: subscribing to the group list failed: {error}");
}
})); }));
self.watch_forks(cx); self.watch_forks(cx);
@@ -1,5 +1,5 @@
[package] [package]
name = "device" name = "nip4e"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
publish.workspace = true publish.workspace = true
@@ -10,7 +10,6 @@ use gpui::{
App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement, App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement,
SharedString, Styled, Subscription, Task, Window, div, relative, SharedString, Styled, Subscription, Task, Window, div, relative,
}; };
use instant::Duration;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use settings::AppSettings; use settings::AppSettings;
@@ -40,7 +39,7 @@ pub enum DeviceEvent {
/// User have not setup encryption key /// User have not setup encryption key
NotSet, NotSet,
/// The device is requesting an encryption key /// The device is requesting an encryption key
Requesting, Requesting(SharedString),
/// An error occurred /// An error occurred
Error(SharedString), Error(SharedString),
} }
@@ -59,19 +58,14 @@ impl DeviceEvent {
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
#[derive(Debug)] #[derive(Debug)]
pub struct DeviceRegistry { pub struct DeviceRegistry {
signer: Entity<Option<UniversalSigner>>,
/// Whether there is a pending request for encryption key approval /// Whether there is a pending request for encryption key approval
pub pending_request: bool, pub pending_request: bool,
/// Whether an announcement has been made for this device /// Whether an announcement has been made for this device
pub announcement_existed: Arc<AtomicBool>, pub announcement_existed: Arc<AtomicBool>,
/// Relay notification listener
/// Signer notification_listener: Option<Task<Result<(), Error>>>,
signer: Entity<Option<UniversalSigner>>,
/// Async tasks
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
/// Event subscription
_subscriptions: SmallVec<[Subscription; 2]>, _subscriptions: SmallVec<[Subscription; 2]>,
} }
@@ -110,6 +104,9 @@ impl DeviceRegistry {
// Observe the user signer // Observe the user signer
cx.subscribe(&nostr, move |this, _nostr, event, cx| { cx.subscribe(&nostr, move |this, _nostr, event, cx| {
if event.signer_changed() && settings.read(cx).is_nip4e_enabled(cx) { if event.signer_changed() && settings.read(cx).is_nip4e_enabled(cx) {
// The old account's state must not leak into the new one
this.announcement_existed.store(false, Ordering::Relaxed);
this.handle_notifications(cx);
this.get_announcement(cx); this.get_announcement(cx);
} }
}), }),
@@ -125,12 +122,15 @@ impl DeviceRegistry {
signer, signer,
pending_request: false, pending_request: false,
announcement_existed: Arc::new(AtomicBool::new(false)), announcement_existed: Arc::new(AtomicBool::new(false)),
notification_listener: None,
tasks: vec![], tasks: vec![],
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
fn handle_notifications(&mut self, cx: &mut Context<Self>) { fn handle_notifications(&mut self, cx: &mut Context<Self>) {
self.notification_listener = None;
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
@@ -138,7 +138,7 @@ impl DeviceRegistry {
let announcement_existed = self.announcement_existed.clone(); let announcement_existed = self.announcement_existed.clone();
let (tx, rx) = flume::bounded::<Event>(100); let (tx, rx) = flume::bounded::<Event>(100);
self.tasks.push(cx.background_spawn(async move { self.notification_listener = Some(cx.background_spawn(async move {
let mut notifications = client.notifications(); let mut notifications = client.notifications();
let mut processed_events = HashSet::new(); let mut processed_events = HashSet::new();
let current_user = signer.get_public_key_async().await.ok(); let current_user = signer.get_public_key_async().await.ok();
@@ -245,8 +245,12 @@ impl DeviceRegistry {
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
self.tasks.push(cx.background_spawn(async move { let announcement_existed = self.announcement_existed.clone();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
self.tasks.push(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let opts =
SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let current_user = signer.get_public_key_async().await?; let current_user = signer.get_public_key_async().await?;
// Construct the filter for the device announcement event // Construct the filter for the device announcement event
@@ -262,22 +266,20 @@ impl DeviceRegistry {
.await?; .await?;
Ok(()) Ok(())
})); });
let announcement_existed = self.announcement_existed.clone(); if let Err(e) = task.await {
this.update(cx, |_this, cx| {
self.tasks.push(cx.spawn(async move |this, cx| { cx.emit(DeviceEvent::error(e.to_string()));
// Wait for 5 seconds })?;
cx.background_executor().timer(Duration::from_secs(5)).await;
// Then check if the msg relays have been found
if announcement_existed.load(Ordering::Acquire) {
return Ok(()); return Ok(());
} }
if !announcement_existed.load(Ordering::Acquire) {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::NotSet); cx.emit(DeviceEvent::NotSet);
})?; })?;
}
Ok(()) Ok(())
})); }));
@@ -317,13 +319,13 @@ impl DeviceRegistry {
cx.background_spawn(async move { cx.background_spawn(async move {
// Construct an announcement event // Construct an announcement event
let event = EventBuilder::new(Kind::Custom(10044), "") let event = EventBuilder::new(Kind::Custom(10044), "")
.tags(vec![ .tag(Tag::custom("n", vec![n]))
Tag::custom("n", vec![n]),
Tag::custom("client", vec![CLIENT_NAME]),
])
.finalize_async(&signer) .finalize_async(&signer)
.await?; .await?;
// Persist the key locally before announcing it
set_keys(&client, &signer, &secret).await?;
// Publish announcement // Publish announcement
client client
.send_event(&event) .send_event(&event)
@@ -331,20 +333,21 @@ impl DeviceRegistry {
.ack_policy(AckPolicy::none()) .ack_policy(AckPolicy::none())
.await?; .await?;
// Save device keys to the database
set_keys(&client, &signer, &secret).await?;
Ok(keys) Ok(keys)
}) })
} }
/// Set encryption key from the announcement event /// Set encryption key from the announcement event
fn set_encryption(&mut self, event: &Event, cx: &mut Context<Self>) { fn set_encryption(&mut self, event: &Event, cx: &mut Context<Self>) {
let Some(announcement) = Announcement::from_event(event) else {
log::warn!("Ignoring malformed encryption announcement");
return;
};
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let announcement = Announcement::from(event);
let device_pubkey = announcement.public_key(); let device_pubkey = announcement.public_key();
// Get encryption key from the database and compare with the announcement // Get encryption key from the database and compare with the announcement
@@ -384,11 +387,8 @@ impl DeviceRegistry {
let public_key = signer.get_public_key_async().await?; let public_key = signer.get_public_key_async().await?;
let id = SubscriptionId::new("dekey-requests"); let id = SubscriptionId::new("dekey-requests");
// Construct a filter for encryption key requests // Construct a filter for encryption key requests.
let filter = Filter::new() let filter = Filter::new().kind(Kind::Custom(4454)).author(public_key);
.kind(Kind::Custom(4454))
.author(public_key)
.since(Timestamp::now());
// Subscribe to the device key requests on user's write relays // Subscribe to the device key requests on user's write relays
client.subscribe(vec![filter]).with_id(id).await?; client.subscribe(vec![filter]).with_id(id).await?;
@@ -417,17 +417,20 @@ impl DeviceRegistry {
.pubkey(app_pubkey) .pubkey(app_pubkey)
.limit(1); .limit(1);
match client.database().query(filter).await?.into_iter().next() { if let Some(event) = client.database().query(filter).await?.into_iter().next() {
// Found an approval event // Only reuse the stored approval if its key is still valid,
Some(event) => Ok(Some(event)), // otherwise it's stale and a fresh request is needed
// No approval event found, construct a request event if decrypt_approval(&client, &app_keys, public_key, &event)
None => { .await
// Construct an event for device key request .is_ok()
{
return Ok(Some(event));
}
}
// No usable approval event found, construct a request event
let event = EventBuilder::new(Kind::Custom(4454), "") let event = EventBuilder::new(Kind::Custom(4454), "")
.tags(vec![ .tags(vec![Tag::custom("P", vec![app_pubkey])])
Tag::custom("P", vec![app_pubkey]),
Tag::custom("client", vec![CLIENT_NAME]),
])
.finalize_async(&signer) .finalize_async(&signer)
.await?; .await?;
@@ -435,8 +438,6 @@ impl DeviceRegistry {
client.send_event(&event).to_nip65().await?; client.send_event(&event).to_nip65().await?;
Ok(None) Ok(None)
}
}
}); });
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
@@ -466,21 +467,46 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let app_keys_task = get_or_init_app_keys(cx);
cx.emit(DeviceEvent::Requesting); self.tasks.push(cx.spawn(async move |this, cx| {
let app_keys = match app_keys_task.await {
Ok(app_keys) => app_keys,
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::error(e.to_string()));
})?;
return Ok(());
}
};
self.tasks.push(cx.background_spawn(async move { this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::Requesting(authorization_code(
&app_keys.public_key(),
)));
})?;
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let public_key = signer.get_public_key_async().await?; let public_key = signer.get_public_key_async().await?;
// Construct a filter for device key requests // Only approvals addressed to this device's app key matter
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::Custom(4455)) .kind(Kind::Custom(4455))
.author(public_key) .author(public_key)
.since(Timestamp::now()); .pubkey(app_keys.public_key());
// Subscribe to the device key requests on user's write relays // Subscribe to the device key approvals on user's write relays
client.subscribe(filter).await?; client.subscribe(filter).await?;
Ok(())
});
if let Err(e) = task.await {
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::error(e.to_string()));
})?;
}
Ok(()) Ok(())
})); }));
} }
@@ -488,22 +514,16 @@ impl DeviceRegistry {
/// Parse the approval event to get encryption key then set it /// Parse the approval event to get encryption key then set it
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) { fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
let app_keys_task = get_or_init_app_keys(cx); let app_keys_task = get_or_init_app_keys(cx);
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let approval_id = event.id;
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move { let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
let app_keys = app_keys_task.await?; let app_keys = app_keys_task.await?;
let master = event let identity = signer.get_public_key_async().await?;
.tags let keys = decrypt_approval(&client, &app_keys, identity, &event).await?;
.iter()
.find(|tag| tag.kind() == "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_async(&master, payload).await?;
let secret = SecretKey::from_hex(&decrypted)?;
let keys = Keys::new(secret);
Ok(keys) Ok(keys)
}); });
@@ -513,6 +533,8 @@ impl DeviceRegistry {
Ok(keys) => { Ok(keys) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_signer(keys, cx); this.set_signer(keys, cx);
// Clean up our request and the approval event from relays
this.cleanup_sync_events(approval_id, cx);
})?; })?;
} }
Err(e) => { Err(e) => {
@@ -525,6 +547,47 @@ impl DeviceRegistry {
})); }));
} }
/// Delete our key request and the consumed approval event from relays
fn cleanup_sync_events(&mut self, approval_id: EventId, cx: &mut Context<Self>) {
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 result: Result<(), Error> = async {
let public_key = signer.get_public_key_async().await?;
// Collect our key request events
let filter = Filter::new().kind(Kind::Custom(4454)).author(public_key);
let mut ids: Vec<EventId> = client
.database()
.query(filter)
.await?
.into_iter()
.map(|event| event.id)
.collect();
ids.push(approval_id);
let deletion = EventBuilder::new(Kind::EventDeletion, "")
.tags(Tags::from_list(ids.into_iter().map(Tag::event).collect()))
.finalize_async(&signer)
.await?;
client.send_event(&deletion).to_nip65().await?;
Ok(())
}
.await;
if let Err(e) = result {
log::error!("Failed to delete sync events: {e}");
}
Ok(())
}));
}
/// Approve requests for device keys from other devices /// Approve requests for device keys from other devices
fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) { fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
@@ -570,9 +633,14 @@ impl DeviceRegistry {
Ok(()) Ok(())
}); });
self.tasks.push(cx.spawn_in(window, async move |_this, cx| { self.tasks.push(cx.spawn_in(window, async move |this, cx| {
match task.await { match task.await {
Ok(_) => { Ok(_) => {
this.update(cx, |this, cx| {
// Allow future requests to surface again
this.set_pending_request(false, cx);
})?;
cx.update(|window, cx| { cx.update(|window, cx| {
window.clear_notification_by_id::<DeviceNotification>(id, cx); window.clear_notification_by_id::<DeviceNotification>(id, cx);
}) })
@@ -601,11 +669,15 @@ impl DeviceRegistry {
} }
self.set_pending_request(true, cx); self.set_pending_request(true, cx);
// Show notification let notification = match self.notification(event, cx) {
let notification = self.notification(event, cx); Ok(notification) => notification,
Err(e) => {
log::warn!("Ignoring invalid encryption key request: {e}");
self.set_pending_request(false, cx);
return;
}
};
// The registry is global and not bound to a window, so surface the
// request in an open window.
if let Some(window) = cx.windows().first().copied() { if let Some(window) = cx.windows().first().copied() {
if let Err(error) = window.update(cx, |_view, window, cx| { if let Err(error) = window.update(cx, |_view, window, cx| {
window.push_notification(notification, cx); window.push_notification(notification, cx);
@@ -618,19 +690,30 @@ impl DeviceRegistry {
} }
/// Build a notification for the encryption request. /// Build a notification for the encryption request.
fn notification(&self, event: Event, cx: &Context<Self>) -> Notification { fn notification(&self, event: Event, cx: &Context<Self>) -> Result<Notification, Error> {
const MSG: &str = "You've requested an encryption key from another device. \ const MSG: &str = "You've requested an encryption key from another device. \
Approve to allow Coop to share with it."; Approve to allow Coop to share with it. Before approving, \
compare the code with the one shown on the requesting \
device.";
let request = Announcement::from(&event); let requester = event
.tags
.iter()
.find(|tag| tag.kind() == "P")
.and_then(|tag| tag.content())
.and_then(|content| PublicKey::parse(content).ok())
.context("Request has no valid requester key")?;
let code = authorization_code(&requester);
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&request.public_key(), cx); let profile = persons.read(cx).get(&requester, cx);
let code_label = code.clone();
let entity = cx.entity().downgrade(); let entity = cx.entity().downgrade();
let loading = Rc::new(Cell::new(false)); let loading = Rc::new(Cell::new(false));
let key = SharedString::from(event.id.to_hex()); let key = SharedString::from(event.id.to_hex());
Notification::new() Ok(Notification::new()
.type_id::<DeviceNotification>(key) .type_id::<DeviceNotification>(key)
.autohide(false) .autohide(false)
.with_kind(NotificationKind::Info) .with_kind(NotificationKind::Info)
@@ -687,7 +770,7 @@ impl DeviceRegistry {
.font_semibold() .font_semibold()
.text_xs() .text_xs()
.text_color(cx.theme().text_muted) .text_color(cx.theme().text_muted)
.child(SharedString::from("Client:")), .child(SharedString::from("Code:")),
) )
.child( .child(
div() div()
@@ -696,7 +779,7 @@ impl DeviceRegistry {
.px_2() .px_2()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background) .bg(cx.theme().elevated_surface_background)
.child(request.client_name()), .child(code_label.clone()),
), ),
), ),
) )
@@ -722,12 +805,19 @@ impl DeviceRegistry {
.ok(); .ok();
} }
}) })
}) }))
} }
} }
struct DeviceNotification; struct DeviceNotification;
/// The first eight hexadecimal characters of the client public key.
fn authorization_code(public_key: &PublicKey) -> SharedString {
let hex = public_key.to_hex();
let code = hex[..8].to_uppercase();
SharedString::from(format!("{} {}", &code[..4], &code[4..]))
}
/// Get or create new app keys (async, returns a task) /// Get or create new app keys (async, returns a task)
fn get_or_init_app_keys(cx: &App) -> Task<Result<Keys, Error>> { fn get_or_init_app_keys(cx: &App) -> Task<Result<Keys, Error>> {
let read = cx.read_credentials(CLIENT_NAME); let read = cx.read_credentials(CLIENT_NAME);
@@ -758,6 +848,51 @@ fn get_or_init_app_keys(cx: &App) -> Task<Result<Keys, Error>> {
}) })
} }
/// Decrypt and validate an approval event against the current announcement.
async fn decrypt_approval(
client: &Client,
app_keys: &Keys,
identity: PublicKey,
event: &Event,
) -> Result<Keys, Error> {
let master = event
.tags
.iter()
.find(|tag| tag.kind() == "P")
.and_then(|tag| tag.content())
.and_then(|content| PublicKey::parse(content).ok())
.context("Invalid event's tags")?;
let decrypted = app_keys
.nip44_decrypt_async(&master, event.content.as_str())
.await?;
let secret = SecretKey::from_hex(&decrypted)?;
let keys = Keys::new(secret);
// Reject approvals carrying a key that no longer matches the published announcement.
let filter = Filter::new()
.kind(Kind::Custom(10044))
.author(identity)
.limit(1);
if let Some(announcement) = client.database().query(filter).await?.into_iter().next() {
let announced = announcement
.tags
.iter()
.find(|tag| tag.kind() == "n")
.and_then(|tag| tag.content())
.and_then(|content| PublicKey::parse(content).ok())
.context("Announcement has no valid encryption key")?;
if keys.public_key() != announced {
return Err(anyhow!("Approval key doesn't match the announcement"));
}
}
Ok(keys)
}
/// Encrypt and store device keys in the local database. /// Encrypt and store device keys in the local database.
async fn set_keys<S>(client: &Client, signer: &S, secret: &str) -> Result<(), Error> async fn set_keys<S>(client: &Client, signer: &S, secret: &str) -> Result<(), Error>
where where
+4 -1
View File
@@ -216,7 +216,10 @@ impl PersonRegistry {
/// Set profile encryption keys announcement /// Set profile encryption keys announcement
fn set_announcement(&mut self, event: &Event, cx: &mut App) { fn set_announcement(&mut self, event: &Event, cx: &mut App) {
let announcement = Announcement::from(event); let Some(announcement) = Announcement::from_event(event) else {
log::warn!("Ignoring announcement with missing or invalid key tag");
return;
};
if let Some(person) = self.persons.get(&event.pubkey) { if let Some(person) = self.persons.get(&event.pubkey) {
person.update(cx, |person, cx| { person.update(cx, |person, cx| {
+10 -32
View File
@@ -1,4 +1,3 @@
use gpui::SharedString;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
/// Announcement /// Announcement
@@ -6,50 +5,29 @@ use nostr_sdk::prelude::*;
pub struct Announcement { pub struct Announcement {
/// The public key of the device that created this announcement. /// The public key of the device that created this announcement.
public_key: PublicKey, public_key: PublicKey,
/// The name of the device that created this announcement.
client_name: Option<String>,
} }
impl From<&Event> for Announcement { impl Announcement {
fn from(val: &Event) -> Self { /// Parse an announcement from an event, returning `None` when the `n`
let public_key = val /// tag is missing or malformed so callers never mistake the author's
/// identity key for an encryption key.
pub fn from_event(event: &Event) -> Option<Self> {
let public_key = event
.tags .tags
.iter() .iter()
.find(|tag| tag.kind() == "n") .find(|tag| tag.kind() == "n")
.and_then(|tag| tag.content()) .and_then(|tag| tag.content())
.and_then(|c| PublicKey::parse(c).ok()) .and_then(|c| PublicKey::parse(c).ok())?;
.unwrap_or(val.pubkey);
let client_name = val Some(Self { public_key })
.tags
.iter()
.find(|tag| tag.kind() == "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) -> Self {
pub fn new(public_key: PublicKey, client_name: Option<String>) -> Self { Self { public_key }
Self {
public_key,
client_name,
}
} }
/// Returns the public key of the device that created this announcement. /// Returns the public key of the device that created this announcement.
pub fn public_key(&self) -> PublicKey { pub fn public_key(&self) -> PublicKey {
self.public_key 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"))
}
} }
+1 -1
View File
@@ -9,7 +9,7 @@ ui = { path = "../ui" }
theme = { path = "../theme" } theme = { path = "../theme" }
common = { path = "../common" } common = { path = "../common" }
state = { path = "../state" } state = { path = "../state" }
device = { path = "../device" } nip4e = { path = "../nip4e" }
chat = { path = "../chat" } chat = { path = "../chat" }
chat_ui = { path = "../chat_ui" } chat_ui = { path = "../chat_ui" }
nip29 = { path = "../nip29" } nip29 = { path = "../nip29" }
+4 -4
View File
@@ -1,12 +1,11 @@
use instant::Duration;
use anyhow::Error; use anyhow::Error;
use device::DeviceRegistry;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, Window, div, Subscription, Task, Window, div,
}; };
use instant::Duration;
use nip4e::DeviceRegistry;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
@@ -52,8 +51,9 @@ impl RestoreEncryption {
let device = DeviceRegistry::global(cx); let device = DeviceRegistry::global(cx);
let content = self.key_input.read(cx).value(); let content = self.key_input.read(cx).value();
if !content.is_empty() { if content.is_empty() {
self.set_error("Secret Key cannot be empty.", cx); self.set_error("Secret Key cannot be empty.", cx);
return;
} }
let Ok(secret) = SecretKey::parse(&content) else { let Ok(secret) = SecretKey::parse(&content) else {
+10 -8
View File
@@ -5,11 +5,11 @@ use anyhow::Error;
use auto_update::AutoUpdater; use auto_update::AutoUpdater;
use chat::{ChatEvent, ChatRegistry}; use chat::{ChatEvent, ChatRegistry};
use common::download_dir; use common::download_dir;
use device::{DeviceEvent, DeviceRegistry};
use gpui::{ use gpui::{
Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, Styled, Subscription, Task, Window, div, px, Render, SharedString, Styled, Subscription, Task, Window, div, px,
}; };
use nip4e::{DeviceEvent, DeviceRegistry};
use nip29::{GroupId, GroupKey}; use nip29::{GroupId, GroupKey};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::Deserialize; use serde::Deserialize;
@@ -106,22 +106,24 @@ impl Workspace {
// Observe all events emitted by the device registry // Observe all events emitted by the device registry
cx.subscribe_in(&device, window, |_this, _device, event, window, cx| { cx.subscribe_in(&device, window, |_this, _device, event, window, cx| {
match event { match event {
DeviceEvent::Requesting => { DeviceEvent::Requesting(code) => {
const MSG: &str = const MSG: &str = "Please open another client and approve the request \
"Please open other client and approve the request for encryption key."; for the encryption key. Compare the code with the \
one shown there before approving.";
let note = Notification::new() let note = Notification::new()
.id::<DeviceNotifcation>() .id::<DeviceNotifcation>()
.autohide(false) .autohide(false)
.title("Wait for approval") .title(format!("Wait for approval · {code}"))
.message(MSG) .message(MSG)
.with_kind(NotificationKind::Info); .with_kind(NotificationKind::Info);
window.push_notification(note, cx); window.push_notification(note, cx);
} }
DeviceEvent::NotSet => { DeviceEvent::NotSet => {
const MSG: &str = const MSG: &str = "No encryption key announcement was found. It may \
"User're not setup encryption key yet. Do you want to create one?"; still be propagating, or you can restore one \
from the settings.";
let note = Notification::new() let note = Notification::new()
.id::<DeviceNotifcation>() .id::<DeviceNotifcation>()
@@ -132,7 +134,7 @@ impl Workspace {
move |_this, window, cx| { move |_this, window, cx| {
let device = DeviceRegistry::global(cx); let device = DeviceRegistry::global(cx);
device.update(cx, |this, cx| { device.update(cx, |this, cx| {
this.set_announcement(Keys::generate(), cx); this.get_announcement(cx);
}); });
window.clear_notification::<DeviceNotifcation>(cx); window.clear_notification::<DeviceNotifcation>(cx);
}, },
+1 -1
View File
@@ -33,7 +33,7 @@ ui = { path = "../crates/ui" }
theme = { path = "../crates/theme" } theme = { path = "../crates/theme" }
common = { path = "../crates/common" } common = { path = "../crates/common" }
state = { path = "../crates/state" } state = { path = "../crates/state" }
device = { path = "../crates/device" } nip4e = { path = "../crates/nip4e" }
chat = { path = "../crates/chat" } chat = { path = "../crates/chat" }
nip29 = { path = "../crates/nip29" } nip29 = { path = "../crates/nip29" }
settings = { path = "../crates/settings" } settings = { path = "../crates/settings" }
+1 -1
View File
@@ -73,7 +73,7 @@ fn main() {
// Initialize device signer // Initialize device signer
// //
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
device::init(cx); nip4e::init(cx);
// Initialize app registry // Initialize app registry
chat::init(cx); chat::init(cx);
+1 -1
View File
@@ -14,7 +14,7 @@ ui = { path = "../crates/ui" }
theme = { path = "../crates/theme" } theme = { path = "../crates/theme" }
common = { path = "../crates/common" } common = { path = "../crates/common" }
state = { path = "../crates/state" } state = { path = "../crates/state" }
device = { path = "../crates/device" } nip4e = { path = "../crates/nip4e" }
chat = { path = "../crates/chat" } chat = { path = "../crates/chat" }
nip29 = { path = "../crates/nip29" } nip29 = { path = "../crates/nip29" }
settings = { path = "../crates/settings" } settings = { path = "../crates/settings" }
+1 -1
View File
@@ -65,7 +65,7 @@ pub fn run() -> Result<(), JsValue> {
// Initialize device signer // Initialize device signer
// //
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
device::init(cx); nip4e::init(cx);
// Initialize app registry // Initialize app registry
chat::init(cx); chat::init(cx);