nip4e: fix message encryption and sync #57
Generated
+25
-24
@@ -1073,7 +1073,6 @@ version = "1.0.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"common",
|
||||
"device",
|
||||
"flume 0.11.1",
|
||||
"futures",
|
||||
"fuzzy-matcher",
|
||||
@@ -1081,8 +1080,10 @@ dependencies = [
|
||||
"instant",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"nip4e",
|
||||
"nostr-sdk",
|
||||
"person",
|
||||
"rand 0.10.3",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
@@ -1413,7 +1414,6 @@ dependencies = [
|
||||
"auto_update",
|
||||
"chat",
|
||||
"common",
|
||||
"device",
|
||||
"gpui-pre",
|
||||
"gpui-pre-linux",
|
||||
"gpui-pre-macos",
|
||||
@@ -1422,6 +1422,7 @@ dependencies = [
|
||||
"gpui-pre-windows",
|
||||
"log",
|
||||
"nip29",
|
||||
"nip4e",
|
||||
"nostr-sdk",
|
||||
"person",
|
||||
"settings",
|
||||
@@ -1441,7 +1442,6 @@ dependencies = [
|
||||
"common",
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"device",
|
||||
"getrandom 0.2.17",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
@@ -1451,6 +1451,7 @@ dependencies = [
|
||||
"instant",
|
||||
"log",
|
||||
"nip29",
|
||||
"nip4e",
|
||||
"person",
|
||||
"settings",
|
||||
"state",
|
||||
@@ -1823,26 +1824,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -4577,6 +4558,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "no_std_io2"
|
||||
version = "0.9.4"
|
||||
@@ -9438,12 +9439,12 @@ dependencies = [
|
||||
"chat",
|
||||
"chat_ui",
|
||||
"common",
|
||||
"device",
|
||||
"gpui-pre",
|
||||
"instant",
|
||||
"log",
|
||||
"nip29",
|
||||
"nip29_ui",
|
||||
"nip4e",
|
||||
"nostr-connect",
|
||||
"nostr-sdk",
|
||||
"person",
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ hkdf = "0.12"
|
||||
chacha20 = "0.9"
|
||||
hmac = "0.12"
|
||||
# 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
|
||||
anyhow = "1.0.44"
|
||||
|
||||
@@ -7,7 +7,7 @@ publish.workspace = true
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
state = { path = "../state" }
|
||||
device = { path = "../device" }
|
||||
nip4e = { path = "../nip4e" }
|
||||
person = { path = "../person" }
|
||||
settings = { path = "../settings" }
|
||||
|
||||
@@ -17,6 +17,7 @@ instant.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
itertools.workspace = true
|
||||
rand.workspace = true
|
||||
smallvec.workspace = true
|
||||
log.workspace = true
|
||||
flume.workspace = true
|
||||
|
||||
+105
-10
@@ -12,6 +12,7 @@ use gpui::{
|
||||
WeakEntity,
|
||||
};
|
||||
use instant::Duration;
|
||||
use nip4e::{DeviceEvent, DeviceRegistry};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP};
|
||||
@@ -138,17 +139,24 @@ impl ChatRegistry {
|
||||
let (tx, rx) = flume::unbounded::<Signal>();
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe to the signer event
|
||||
cx.subscribe(&nostr, |this, _nostr, event, cx| {
|
||||
let device = DeviceRegistry::global(cx);
|
||||
|
||||
subscriptions.push(cx.subscribe(&nostr, |this, _nostr, event, cx| {
|
||||
if event.signer_changed() {
|
||||
this.reset(cx);
|
||||
this.handle_notifications(cx);
|
||||
this.get_metadata(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| {
|
||||
let _ = entity.update(cx, |this, cx| {
|
||||
@@ -183,6 +191,9 @@ impl ChatRegistry {
|
||||
let client = nostr.read(cx).client();
|
||||
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 event_map = self.event_map.clone();
|
||||
let trash = self.trash.downgrade();
|
||||
@@ -249,7 +260,14 @@ impl ChatRegistry {
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
let Some(rumor_id) = rumor.id else {
|
||||
log::error!("Rumor missing id after ensure_id");
|
||||
@@ -385,6 +403,9 @@ impl ChatRegistry {
|
||||
let client = nostr.read(cx).client();
|
||||
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| {
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let public_key = signer.get_public_key_async().await?;
|
||||
@@ -417,6 +438,20 @@ impl ChatRegistry {
|
||||
|
||||
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(())
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
pub fn reload(&mut self, cx: &mut Context<Self>) {
|
||||
self.reset(cx);
|
||||
@@ -740,21 +818,30 @@ impl ChatRegistry {
|
||||
}
|
||||
|
||||
/// Unwraps a gift-wrapped event and processes its contents.
|
||||
async fn extract_rumor<S>(
|
||||
async fn extract_rumor<S, D>(
|
||||
client: &Client,
|
||||
signer: &S,
|
||||
device_signer: Option<&D>,
|
||||
gift_wrap: &Event,
|
||||
) -> Result<UnsignedEvent, Error>
|
||||
where
|
||||
S: AsyncNip44 + ?Sized,
|
||||
D: AsyncNip44 + ?Sized,
|
||||
{
|
||||
// Try to get cached rumor first
|
||||
if let Ok(rumor) = get_rumor(client, gift_wrap.id).await {
|
||||
return Ok(rumor);
|
||||
}
|
||||
|
||||
// Try to unwrap with the available signer
|
||||
let unwrapped = try_unwrap_with(signer, gift_wrap).await?;
|
||||
let unwrapped = match 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;
|
||||
|
||||
// Verify rumor author matches the seal sender (as per mobile implementation)
|
||||
@@ -787,9 +874,17 @@ where
|
||||
let seal: Event = Event::from_json(seal)?;
|
||||
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
|
||||
let rumor = signer
|
||||
.nip44_decrypt_async(&seal.pubkey, &seal.content)
|
||||
.nip44_decrypt_async(&sender_encryption_key, &seal.content)
|
||||
.await?;
|
||||
|
||||
let rumor = UnsignedEvent::from_json(rumor)?;
|
||||
|
||||
+70
-40
@@ -3,19 +3,19 @@ use std::hash::{Hash, Hasher};
|
||||
|
||||
use anyhow::{Error, anyhow};
|
||||
use common::EventExt;
|
||||
use device::DeviceRegistry;
|
||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
|
||||
use instant::Duration;
|
||||
use itertools::Itertools;
|
||||
use nip4e::DeviceRegistry;
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{Person, PersonRegistry};
|
||||
use settings::{RoomConfig, SignerKind};
|
||||
use state::{NostrRegistry, TIMEOUT};
|
||||
use state::{NostrRegistry, TIMEOUT, UniversalSigner};
|
||||
|
||||
use crate::{FileAttachment, KIND_FILE_MESSAGE, NewMessage};
|
||||
|
||||
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.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -472,28 +472,6 @@ impl Room {
|
||||
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.
|
||||
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
|
||||
let config = self.config.clone();
|
||||
@@ -528,21 +506,23 @@ impl Room {
|
||||
signer_kind,
|
||||
receiver,
|
||||
&rumor,
|
||||
&encryption_signer,
|
||||
encryption_signer.as_ref(),
|
||||
&user_signer,
|
||||
)
|
||||
});
|
||||
|
||||
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
|
||||
if config.backup() && reports.iter().any(SendReport::success) {
|
||||
if config.backup() && minimum_success {
|
||||
reports.push(
|
||||
deliver(
|
||||
&client,
|
||||
signer_kind,
|
||||
sender,
|
||||
&rumor,
|
||||
&encryption_signer,
|
||||
encryption_signer.as_ref(),
|
||||
&user_signer,
|
||||
)
|
||||
.await,
|
||||
@@ -560,7 +540,7 @@ async fn deliver<S>(
|
||||
signer_kind: &SignerKind,
|
||||
receiver: Person,
|
||||
rumor: &UnsignedEvent,
|
||||
encryption_signer: &Option<S>,
|
||||
encryption_signer: Option<&UniversalSigner>,
|
||||
user_signer: &S,
|
||||
) -> SendReport
|
||||
where
|
||||
@@ -587,15 +567,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the signer to use
|
||||
let signer = Room::select_signer(
|
||||
match send_gift_wrap(
|
||||
client,
|
||||
&receiver,
|
||||
rumor,
|
||||
signer_kind,
|
||||
announcement.is_some(),
|
||||
encryption_signer,
|
||||
user_signer,
|
||||
);
|
||||
|
||||
match send_gift_wrap(client, signer, &receiver, rumor, signer_kind).await {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(output) => SendReport {
|
||||
receiver: public_key,
|
||||
result: Ok(output),
|
||||
@@ -610,10 +591,11 @@ where
|
||||
/// Build the gift-wrapped event for a rumor and send it.
|
||||
async fn send_gift_wrap<S>(
|
||||
client: &Client,
|
||||
signer: &S,
|
||||
receiver: &Person,
|
||||
rumor: &UnsignedEvent,
|
||||
config: &SignerKind,
|
||||
encryption_signer: Option<&UniversalSigner>,
|
||||
user_signer: &S,
|
||||
) -> Result<Output<EventId, EventSendStatus>, Error>
|
||||
where
|
||||
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44,
|
||||
@@ -639,12 +621,52 @@ where
|
||||
SignerKind::User => receiver.public_key(),
|
||||
};
|
||||
|
||||
// Construct the gift wrap event
|
||||
let event = nip59::GiftWrapBuilder::new(receiver_key, rumor.clone())
|
||||
.extra_tags(extra_tags)
|
||||
.finalize_async(signer)
|
||||
// Make sure the rumor carries an event ID before it's serialized
|
||||
let mut rumor = rumor.clone();
|
||||
rumor.ensure_id();
|
||||
|
||||
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?;
|
||||
|
||||
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.
|
||||
client
|
||||
.send_event(&event)
|
||||
@@ -652,3 +674,11 @@ where
|
||||
.await
|
||||
.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
@@ -126,6 +126,7 @@ impl GroupsRegistry {
|
||||
if event.signer_changed() {
|
||||
this.reset(cx);
|
||||
this.handle_notifications(cx);
|
||||
this.load_cached(cx);
|
||||
this.load(cx);
|
||||
}
|
||||
}));
|
||||
@@ -133,6 +134,7 @@ impl GroupsRegistry {
|
||||
cx.defer(move |cx| {
|
||||
if let Err(error) = this.update(cx, |this, cx| {
|
||||
this.handle_notifications(cx);
|
||||
this.load_cached(cx);
|
||||
|
||||
if nostr.read(cx).current_user().is_some() {
|
||||
this.load(cx);
|
||||
@@ -349,20 +351,21 @@ impl GroupsRegistry {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn load(&mut self, cx: &mut Context<Self>) {
|
||||
fn load_cached(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
|
||||
if nostr.read(cx).current_user().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let client = nostr.read(cx).client();
|
||||
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 me = signer.get_public_key_async().await?;
|
||||
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() {
|
||||
Some(event) => GroupList::parse(&event),
|
||||
None => Ok(GroupList::default()),
|
||||
@@ -373,10 +376,36 @@ impl GroupsRegistry {
|
||||
.push(cx.spawn(async move |this, cx| match task.await {
|
||||
Ok(list) => {
|
||||
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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "device"
|
||||
name = "nip4e"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
@@ -10,7 +10,6 @@ use gpui::{
|
||||
App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement,
|
||||
SharedString, Styled, Subscription, Task, Window, div, relative,
|
||||
};
|
||||
use instant::Duration;
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use settings::AppSettings;
|
||||
@@ -40,7 +39,7 @@ pub enum DeviceEvent {
|
||||
/// User have not setup encryption key
|
||||
NotSet,
|
||||
/// The device is requesting an encryption key
|
||||
Requesting,
|
||||
Requesting(SharedString),
|
||||
/// An error occurred
|
||||
Error(SharedString),
|
||||
}
|
||||
@@ -59,19 +58,14 @@ impl DeviceEvent {
|
||||
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceRegistry {
|
||||
signer: Entity<Option<UniversalSigner>>,
|
||||
/// Whether there is a pending request for encryption key approval
|
||||
pub pending_request: bool,
|
||||
|
||||
/// Whether an announcement has been made for this device
|
||||
pub announcement_existed: Arc<AtomicBool>,
|
||||
|
||||
/// Signer
|
||||
signer: Entity<Option<UniversalSigner>>,
|
||||
|
||||
/// Async tasks
|
||||
/// Relay notification listener
|
||||
notification_listener: Option<Task<Result<(), Error>>>,
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
|
||||
/// Event subscription
|
||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
|
||||
@@ -110,6 +104,9 @@ impl DeviceRegistry {
|
||||
// Observe the user signer
|
||||
cx.subscribe(&nostr, move |this, _nostr, event, 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);
|
||||
}
|
||||
}),
|
||||
@@ -125,12 +122,15 @@ impl DeviceRegistry {
|
||||
signer,
|
||||
pending_request: false,
|
||||
announcement_existed: Arc::new(AtomicBool::new(false)),
|
||||
notification_listener: None,
|
||||
tasks: vec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
|
||||
self.notification_listener = None;
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
@@ -138,7 +138,7 @@ impl DeviceRegistry {
|
||||
let announcement_existed = self.announcement_existed.clone();
|
||||
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 processed_events = HashSet::new();
|
||||
let current_user = signer.get_public_key_async().await.ok();
|
||||
@@ -245,8 +245,12 @@ impl DeviceRegistry {
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let announcement_existed = self.announcement_existed.clone();
|
||||
|
||||
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?;
|
||||
|
||||
// Construct the filter for the device announcement event
|
||||
@@ -262,22 +266,20 @@ impl DeviceRegistry {
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
});
|
||||
|
||||
let announcement_existed = self.announcement_existed.clone();
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
// 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) {
|
||||
if let Err(e) = task.await {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(DeviceEvent::error(e.to_string()));
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !announcement_existed.load(Ordering::Acquire) {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(DeviceEvent::NotSet);
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
@@ -317,13 +319,13 @@ impl DeviceRegistry {
|
||||
cx.background_spawn(async move {
|
||||
// Construct an announcement event
|
||||
let event = EventBuilder::new(Kind::Custom(10044), "")
|
||||
.tags(vec![
|
||||
Tag::custom("n", vec![n]),
|
||||
Tag::custom("client", vec![CLIENT_NAME]),
|
||||
])
|
||||
.tag(Tag::custom("n", vec![n]))
|
||||
.finalize_async(&signer)
|
||||
.await?;
|
||||
|
||||
// Persist the key locally before announcing it
|
||||
set_keys(&client, &signer, &secret).await?;
|
||||
|
||||
// Publish announcement
|
||||
client
|
||||
.send_event(&event)
|
||||
@@ -331,20 +333,21 @@ impl DeviceRegistry {
|
||||
.ack_policy(AckPolicy::none())
|
||||
.await?;
|
||||
|
||||
// Save device keys to the database
|
||||
set_keys(&client, &signer, &secret).await?;
|
||||
|
||||
Ok(keys)
|
||||
})
|
||||
}
|
||||
|
||||
/// Set encryption key from the announcement event
|
||||
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 client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let announcement = Announcement::from(event);
|
||||
let device_pubkey = announcement.public_key();
|
||||
|
||||
// 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 id = SubscriptionId::new("dekey-requests");
|
||||
|
||||
// Construct a filter for encryption key requests
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(4454))
|
||||
.author(public_key)
|
||||
.since(Timestamp::now());
|
||||
// Construct a filter for encryption key requests.
|
||||
let filter = Filter::new().kind(Kind::Custom(4454)).author(public_key);
|
||||
|
||||
// Subscribe to the device key requests on user's write relays
|
||||
client.subscribe(vec![filter]).with_id(id).await?;
|
||||
@@ -417,17 +417,20 @@ impl DeviceRegistry {
|
||||
.pubkey(app_pubkey)
|
||||
.limit(1);
|
||||
|
||||
match client.database().query(filter).await?.into_iter().next() {
|
||||
// Found an approval event
|
||||
Some(event) => Ok(Some(event)),
|
||||
// No approval event found, construct a request event
|
||||
None => {
|
||||
// Construct an event for device key request
|
||||
if let Some(event) = client.database().query(filter).await?.into_iter().next() {
|
||||
// Only reuse the stored approval if its key is still valid,
|
||||
// otherwise it's stale and a fresh request is needed
|
||||
if decrypt_approval(&client, &app_keys, public_key, &event)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(Some(event));
|
||||
}
|
||||
}
|
||||
|
||||
// No usable approval event found, construct a request event
|
||||
let event = EventBuilder::new(Kind::Custom(4454), "")
|
||||
.tags(vec![
|
||||
Tag::custom("P", vec![app_pubkey]),
|
||||
Tag::custom("client", vec![CLIENT_NAME]),
|
||||
])
|
||||
.tags(vec![Tag::custom("P", vec![app_pubkey])])
|
||||
.finalize_async(&signer)
|
||||
.await?;
|
||||
|
||||
@@ -435,8 +438,6 @@ impl DeviceRegistry {
|
||||
client.send_event(&event).to_nip65().await?;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
@@ -466,21 +467,46 @@ impl DeviceRegistry {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
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?;
|
||||
|
||||
// Construct a filter for device key requests
|
||||
// Only approvals addressed to this device's app key matter
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(4455))
|
||||
.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?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
if let Err(e) = task.await {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(DeviceEvent::error(e.to_string()));
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
@@ -488,22 +514,16 @@ impl DeviceRegistry {
|
||||
/// Parse the approval event to get encryption key then set it
|
||||
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||
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 app_keys = app_keys_task.await?;
|
||||
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 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);
|
||||
let identity = signer.get_public_key_async().await?;
|
||||
let keys = decrypt_approval(&client, &app_keys, identity, &event).await?;
|
||||
|
||||
Ok(keys)
|
||||
});
|
||||
@@ -513,6 +533,8 @@ impl DeviceRegistry {
|
||||
Ok(keys) => {
|
||||
this.update(cx, |this, 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) => {
|
||||
@@ -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
|
||||
fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
@@ -570,9 +633,14 @@ impl DeviceRegistry {
|
||||
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 {
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
// Allow future requests to surface again
|
||||
this.set_pending_request(false, cx);
|
||||
})?;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
window.clear_notification_by_id::<DeviceNotification>(id, cx);
|
||||
})
|
||||
@@ -601,11 +669,15 @@ impl DeviceRegistry {
|
||||
}
|
||||
self.set_pending_request(true, cx);
|
||||
|
||||
// Show notification
|
||||
let notification = self.notification(event, cx);
|
||||
let notification = match 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 Err(error) = window.update(cx, |_view, window, cx| {
|
||||
window.push_notification(notification, cx);
|
||||
@@ -618,19 +690,30 @@ impl DeviceRegistry {
|
||||
}
|
||||
|
||||
/// 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. \
|
||||
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 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 loading = Rc::new(Cell::new(false));
|
||||
let key = SharedString::from(event.id.to_hex());
|
||||
|
||||
Notification::new()
|
||||
Ok(Notification::new()
|
||||
.type_id::<DeviceNotification>(key)
|
||||
.autohide(false)
|
||||
.with_kind(NotificationKind::Info)
|
||||
@@ -687,7 +770,7 @@ impl DeviceRegistry {
|
||||
.font_semibold()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::from("Client:")),
|
||||
.child(SharedString::from("Code:")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -696,7 +779,7 @@ impl DeviceRegistry {
|
||||
.px_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.child(request.client_name()),
|
||||
.child(code_label.clone()),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -722,12 +805,19 @@ impl DeviceRegistry {
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
fn get_or_init_app_keys(cx: &App) -> Task<Result<Keys, Error>> {
|
||||
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.
|
||||
async fn set_keys<S>(client: &Client, signer: &S, secret: &str) -> Result<(), Error>
|
||||
where
|
||||
@@ -216,7 +216,10 @@ impl PersonRegistry {
|
||||
|
||||
/// Set profile encryption keys announcement
|
||||
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) {
|
||||
person.update(cx, |person, cx| {
|
||||
|
||||
+10
-32
@@ -1,4 +1,3 @@
|
||||
use gpui::SharedString;
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
/// Announcement
|
||||
@@ -6,50 +5,29 @@ use nostr_sdk::prelude::*;
|
||||
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<String>,
|
||||
}
|
||||
|
||||
impl From<&Event> for Announcement {
|
||||
fn from(val: &Event) -> Self {
|
||||
let public_key = val
|
||||
impl Announcement {
|
||||
/// Parse an announcement from an event, returning `None` when the `n`
|
||||
/// 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
|
||||
.iter()
|
||||
.find(|tag| tag.kind() == "n")
|
||||
.and_then(|tag| tag.content())
|
||||
.and_then(|c| PublicKey::parse(c).ok())
|
||||
.unwrap_or(val.pubkey);
|
||||
.and_then(|c| PublicKey::parse(c).ok())?;
|
||||
|
||||
let client_name = val
|
||||
.tags
|
||||
.iter()
|
||||
.find(|tag| tag.kind() == "client")
|
||||
.and_then(|tag| tag.content())
|
||||
.map(|c| c.to_string());
|
||||
|
||||
Self::new(public_key, client_name)
|
||||
}
|
||||
Some(Self { public_key })
|
||||
}
|
||||
|
||||
impl Announcement {
|
||||
pub fn new(public_key: PublicKey, client_name: Option<String>) -> Self {
|
||||
Self {
|
||||
public_key,
|
||||
client_name,
|
||||
}
|
||||
pub fn new(public_key: PublicKey) -> Self {
|
||||
Self { public_key }
|
||||
}
|
||||
|
||||
/// 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"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ ui = { path = "../ui" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
state = { path = "../state" }
|
||||
device = { path = "../device" }
|
||||
nip4e = { path = "../nip4e" }
|
||||
chat = { path = "../chat" }
|
||||
chat_ui = { path = "../chat_ui" }
|
||||
nip29 = { path = "../nip29" }
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use instant::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use device::DeviceRegistry;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Task, Window, div,
|
||||
};
|
||||
use instant::Duration;
|
||||
use nip4e::DeviceRegistry;
|
||||
use nostr_sdk::prelude::*;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
@@ -52,8 +51,9 @@ impl RestoreEncryption {
|
||||
let device = DeviceRegistry::global(cx);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(secret) = SecretKey::parse(&content) else {
|
||||
|
||||
@@ -5,11 +5,11 @@ use anyhow::Error;
|
||||
use auto_update::AutoUpdater;
|
||||
use chat::{ChatEvent, ChatRegistry};
|
||||
use common::download_dir;
|
||||
use device::{DeviceEvent, DeviceRegistry};
|
||||
use gpui::{
|
||||
Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, Styled, Subscription, Task, Window, div, px,
|
||||
};
|
||||
use nip4e::{DeviceEvent, DeviceRegistry};
|
||||
use nip29::{GroupId, GroupKey};
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
@@ -106,22 +106,24 @@ impl Workspace {
|
||||
// Observe all events emitted by the device registry
|
||||
cx.subscribe_in(&device, window, |_this, _device, event, window, cx| {
|
||||
match event {
|
||||
DeviceEvent::Requesting => {
|
||||
const MSG: &str =
|
||||
"Please open other client and approve the request for encryption key.";
|
||||
DeviceEvent::Requesting(code) => {
|
||||
const MSG: &str = "Please open another client and approve the request \
|
||||
for the encryption key. Compare the code with the \
|
||||
one shown there before approving.";
|
||||
|
||||
let note = Notification::new()
|
||||
.id::<DeviceNotifcation>()
|
||||
.autohide(false)
|
||||
.title("Wait for approval")
|
||||
.title(format!("Wait for approval · {code}"))
|
||||
.message(MSG)
|
||||
.with_kind(NotificationKind::Info);
|
||||
|
||||
window.push_notification(note, cx);
|
||||
}
|
||||
DeviceEvent::NotSet => {
|
||||
const MSG: &str =
|
||||
"User're not setup encryption key yet. Do you want to create one?";
|
||||
const MSG: &str = "No encryption key announcement was found. It may \
|
||||
still be propagating, or you can restore one \
|
||||
from the settings.";
|
||||
|
||||
let note = Notification::new()
|
||||
.id::<DeviceNotifcation>()
|
||||
@@ -132,7 +134,7 @@ impl Workspace {
|
||||
move |_this, window, cx| {
|
||||
let device = DeviceRegistry::global(cx);
|
||||
device.update(cx, |this, cx| {
|
||||
this.set_announcement(Keys::generate(), cx);
|
||||
this.get_announcement(cx);
|
||||
});
|
||||
window.clear_notification::<DeviceNotifcation>(cx);
|
||||
},
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ ui = { path = "../crates/ui" }
|
||||
theme = { path = "../crates/theme" }
|
||||
common = { path = "../crates/common" }
|
||||
state = { path = "../crates/state" }
|
||||
device = { path = "../crates/device" }
|
||||
nip4e = { path = "../crates/nip4e" }
|
||||
chat = { path = "../crates/chat" }
|
||||
nip29 = { path = "../crates/nip29" }
|
||||
settings = { path = "../crates/settings" }
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ fn main() {
|
||||
// Initialize device signer
|
||||
//
|
||||
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||
device::init(cx);
|
||||
nip4e::init(cx);
|
||||
|
||||
// Initialize app registry
|
||||
chat::init(cx);
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ ui = { path = "../crates/ui" }
|
||||
theme = { path = "../crates/theme" }
|
||||
common = { path = "../crates/common" }
|
||||
state = { path = "../crates/state" }
|
||||
device = { path = "../crates/device" }
|
||||
nip4e = { path = "../crates/nip4e" }
|
||||
chat = { path = "../crates/chat" }
|
||||
nip29 = { path = "../crates/nip29" }
|
||||
settings = { path = "../crates/settings" }
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ pub fn run() -> Result<(), JsValue> {
|
||||
// Initialize device signer
|
||||
//
|
||||
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||
device::init(cx);
|
||||
nip4e::init(cx);
|
||||
|
||||
// Initialize app registry
|
||||
chat::init(cx);
|
||||
|
||||
Reference in New Issue
Block a user