nip4e: fix message encryption and sync #57
Generated
+25
-24
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+111
-16
@@ -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)?;
|
||||||
|
|||||||
+71
-41
@@ -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,11 +621,51 @@ 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)
|
|
||||||
.await?;
|
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.
|
// Send to the receiver's NIP-17 relays.
|
||||||
client
|
client
|
||||||
@@ -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
@@ -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,11 +376,37 @@ 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,39 +245,41 @@ 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 opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
|
||||||
let current_user = signer.get_public_key_async().await?;
|
|
||||||
|
|
||||||
// Construct the filter for the device announcement event
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::Custom(10044))
|
|
||||||
.author(current_user)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
client
|
|
||||||
.subscribe(filter)
|
|
||||||
.close_on(opts)
|
|
||||||
.with_id(SubscriptionId::new("nip4e"))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
|
|
||||||
let announcement_existed = self.announcement_existed.clone();
|
let announcement_existed = self.announcement_existed.clone();
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
// Wait for 5 seconds
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
cx.background_executor().timer(Duration::from_secs(5)).await;
|
let opts =
|
||||||
|
SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
let current_user = signer.get_public_key_async().await?;
|
||||||
|
|
||||||
// Then check if the msg relays have been found
|
// Construct the filter for the device announcement event
|
||||||
if announcement_existed.load(Ordering::Acquire) {
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::Custom(10044))
|
||||||
|
.author(current_user)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
client
|
||||||
|
.subscribe(filter)
|
||||||
|
.close_on(opts)
|
||||||
|
.with_id(SubscriptionId::new("nip4e"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(e) = task.await {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(DeviceEvent::error(e.to_string()));
|
||||||
|
})?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update(cx, |_this, cx| {
|
if !announcement_existed.load(Ordering::Acquire) {
|
||||||
cx.emit(DeviceEvent::NotSet);
|
this.update(cx, |_this, cx| {
|
||||||
})?;
|
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,26 +417,27 @@ 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()
|
||||||
let event = EventBuilder::new(Kind::Custom(4454), "")
|
{
|
||||||
.tags(vec![
|
return Ok(Some(event));
|
||||||
Tag::custom("P", vec![app_pubkey]),
|
|
||||||
Tag::custom("client", vec![CLIENT_NAME]),
|
|
||||||
])
|
|
||||||
.finalize_async(&signer)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Send the event to write relays
|
|
||||||
client.send_event(&event).to_nip65().await?;
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No usable approval event found, construct a request event
|
||||||
|
let event = EventBuilder::new(Kind::Custom(4454), "")
|
||||||
|
.tags(vec![Tag::custom("P", vec![app_pubkey])])
|
||||||
|
.finalize_async(&signer)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Send the event to write relays
|
||||||
|
client.send_event(&event).to_nip65().await?;
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
});
|
});
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
@@ -466,20 +467,45 @@ 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| {
|
||||||
let public_key = signer.get_public_key_async().await?;
|
cx.emit(DeviceEvent::Requesting(authorization_code(
|
||||||
|
&app_keys.public_key(),
|
||||||
|
)));
|
||||||
|
})?;
|
||||||
|
|
||||||
// Construct a filter for device key requests
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let filter = Filter::new()
|
let public_key = signer.get_public_key_async().await?;
|
||||||
.kind(Kind::Custom(4455))
|
|
||||||
.author(public_key)
|
|
||||||
.since(Timestamp::now());
|
|
||||||
|
|
||||||
// Subscribe to the device key requests on user's write relays
|
// Only approvals addressed to this device's app key matter
|
||||||
client.subscribe(filter).await?;
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::Custom(4455))
|
||||||
|
.author(public_key)
|
||||||
|
.pubkey(app_keys.public_key());
|
||||||
|
|
||||||
|
// 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(())
|
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
|
||||||
@@ -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
@@ -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"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user