wip: dekey

This commit is contained in:
2026-01-08 17:49:58 +07:00
parent bf586580ab
commit 68015ead1c
7 changed files with 311 additions and 94 deletions

View File

@@ -39,7 +39,6 @@ use crate::text::RenderedText;
mod actions;
mod emoji;
mod subject;
mod text;
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
@@ -601,7 +600,6 @@ impl ChatPanel {
text: AnyElement,
cx: &Context<Self>,
) -> AnyElement {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let hide_avatar = AppSettings::get_hide_user_avatars(cx);
let id = message.id;
@@ -1132,7 +1130,6 @@ impl Panel for ChatPanel {
fn title(&self, cx: &App) -> AnyElement {
self.room
.read_with(cx, |this, cx| {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let label = this.display_name(cx);
let url = this.display_image(cx);

View File

@@ -541,7 +541,7 @@ impl ChatSpace {
}),
)
})
.when_some(identity.read(cx).option_public_key(), |this, public_key| {
.when_some(identity.read(cx).public_key, |this, public_key| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);

View File

@@ -9,7 +9,7 @@ use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_sdk::prelude::*;
pub use person::*;
use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, TIMEOUT};
use state::{Announcement, NostrRegistry, TIMEOUT};
mod person;
@@ -21,6 +21,12 @@ struct GlobalPersonRegistry(Entity<PersonRegistry>);
impl Global for GlobalPersonRegistry {}
#[derive(Debug, Clone)]
enum Dispatch {
Person(Box<Person>),
Announcement(Box<Event>),
}
/// Person Registry
#[derive(Debug)]
pub struct PersonRegistry {
@@ -54,7 +60,7 @@ impl PersonRegistry {
let client = nostr.read(cx).client();
// Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Person>(100);
let (tx, rx) = flume::bounded::<Dispatch>(100);
let (mta_tx, mta_rx) = flume::bounded::<PublicKey>(100);
let mut tasks = smallvec![];
@@ -84,9 +90,16 @@ impl PersonRegistry {
tasks.push(
// Update GPUI state
cx.spawn(async move |this, cx| {
while let Ok(person) = rx.recv_async().await {
while let Ok(event) = rx.recv_async().await {
this.update(cx, |this, cx| {
this.insert(person, cx);
match event {
Dispatch::Person(person) => {
this.insert(*person, cx);
}
Dispatch::Announcement(event) => {
this.set_announcement(&event, cx);
}
};
})
.ok();
}
@@ -124,7 +137,7 @@ impl PersonRegistry {
}
/// Handle nostr notifications
async fn handle_notifications(client: &Client, tx: &flume::Sender<Person>) {
async fn handle_notifications(client: &Client, tx: &flume::Sender<Dispatch>) {
let mut notifications = client.notifications();
let mut processed_events = HashSet::new();
@@ -144,12 +157,21 @@ impl PersonRegistry {
Kind::Metadata => {
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
let person = Person::new(event.pubkey, metadata);
let val = Box::new(person);
tx.send_async(person).await.ok();
// Send
tx.send_async(Dispatch::Person(val)).await.ok();
}
Kind::Custom(10044) => {
let val = Box::new(event.into_owned());
// Send
tx.send_async(Dispatch::Announcement(val)).await.ok();
}
Kind::ContactList => {
let public_keys = event.extract_public_keys();
// Get metadata for all public keys
Self::get_metadata(client, public_keys).await.ok();
}
_ => {}
@@ -232,6 +254,18 @@ impl PersonRegistry {
Ok(persons)
}
/// Set profile encryption keys announcement
fn set_announcement(&mut self, event: &Event, cx: &mut App) {
if let Some(person) = self.persons.get(&event.pubkey) {
let announcement = Announcement::from(event);
person.update(cx, |person, cx| {
person.set_announcement(announcement);
cx.notify();
});
}
}
/// Insert batch of persons
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
for person in persons.into_iter() {

View File

@@ -8,8 +8,13 @@ use state::Announcement;
/// Person
#[derive(Debug, Clone)]
pub struct Person {
/// Public Key
public_key: PublicKey,
/// Metadata (profile)
metadata: Metadata,
/// Dekey (NIP-4e) announcement
announcement: Option<Announcement>,
}
@@ -69,6 +74,12 @@ impl Person {
self.announcement.clone()
}
/// Set profile encryption keys announcement
pub fn set_announcement(&mut self, announcement: Announcement) {
self.announcement = Some(announcement);
log::info!("Updated announcement for: {}", self.public_key());
}
/// Get profile avatar
pub fn avatar(&self) -> SharedString {
self.metadata()

View File

@@ -1,10 +1,21 @@
use gpui::SharedString;
use nostr_sdk::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum DeviceState {
#[default]
Initial,
Requesting,
Set,
}
/// Announcement
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Announcement {
id: EventId,
/// 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>,
}
@@ -24,27 +35,24 @@ impl From<&Event> for Announcement {
.and_then(|tag| tag.content())
.map(|c| c.to_string());
Self::new(val.id, client_name, public_key)
Self::new(public_key, client_name)
}
}
impl Announcement {
pub fn new(id: EventId, client_name: Option<String>, public_key: PublicKey) -> Self {
pub fn new(public_key: PublicKey, client_name: Option<String>) -> Self {
Self {
id,
client_name,
public_key,
client_name,
}
}
pub fn id(&self) -> EventId {
self.id
}
/// 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()

View File

@@ -1,6 +1,6 @@
use nostr_sdk::prelude::*;
use std::sync::Arc;
use crate::Announcement;
use nostr_sdk::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RelayState {
@@ -16,13 +16,15 @@ impl RelayState {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[derive(Debug, Clone, Default)]
pub struct Identity {
/// The public key of the account
public_key: Option<PublicKey>,
pub public_key: Option<PublicKey>,
/// Encryption key announcement
announcement: Option<Announcement>,
/// Decoupled encryption key
///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
dekey: Option<Arc<dyn NostrSigner>>,
/// Status of the current user NIP-65 relays
relay_list: RelayState,
@@ -41,7 +43,7 @@ impl Identity {
pub fn new() -> Self {
Self {
public_key: None,
announcement: None,
dekey: None,
relay_list: RelayState::default(),
messaging_relays: RelayState::default(),
}
@@ -57,6 +59,7 @@ impl Identity {
self.relay_list
}
/// Sets the state of the NIP-17 relays.
pub fn set_messaging_relays_state(&mut self, state: RelayState) {
self.messaging_relays = state;
}
@@ -66,6 +69,26 @@ impl Identity {
self.messaging_relays
}
/// Returns the decoupled encryption key.
pub fn dekey(&self) -> Option<Arc<dyn NostrSigner>> {
self.dekey.clone()
}
/// Sets the decoupled encryption key.
pub fn set_dekey<S>(&mut self, dekey: S)
where
S: NostrSigner + 'static,
{
self.dekey = Some(Arc::new(dekey));
}
/// Force getting the public key of the identity.
///
/// Panics if the public key is not set.
pub fn public_key(&self) -> PublicKey {
self.public_key.unwrap()
}
/// Returns true if the identity has a public key.
pub fn has_public_key(&self) -> bool {
self.public_key.is_some()
@@ -80,15 +103,4 @@ impl Identity {
pub fn unset_public_key(&mut self) {
self.public_key = None;
}
/// Returns the public key of the identity.
pub fn option_public_key(&self) -> Option<PublicKey> {
self.public_key
}
/// Returns the public key of the identity.
pub fn public_key(&self) -> PublicKey {
// This method is safe to unwrap because the public key is always called when the identity is created.
self.public_key.unwrap()
}
}

View File

@@ -1,8 +1,8 @@
use std::collections::HashSet;
use std::time::Duration;
use anyhow::Error;
use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use anyhow::{anyhow, Context as AnyhowContext, Error};
use common::{app_name, config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use nostr_lmdb::NostrLmdb;
use nostr_sdk::prelude::*;
@@ -52,6 +52,11 @@ pub struct NostrRegistry {
/// Gossip implementation
gossip: Entity<Gossip>,
/// Device state
///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
device_state: Entity<DeviceState>,
/// Tasks for asynchronous operations
tasks: Vec<Task<Result<(), Error>>>,
@@ -108,6 +113,7 @@ impl NostrRegistry {
// Construct the identity entity
let identity = cx.new(|_| Identity::default());
let device_state = cx.new(|_| DeviceState::default());
// Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Event>(2048);
@@ -123,16 +129,18 @@ impl NostrRegistry {
RelayState::Initial => {
this.get_relay_list(cx);
}
RelayState::Set => match state.read(cx).messaging_relays_state() {
RelayState::Initial => {
this.get_profile(cx);
this.get_messaging_relays(cx);
}
RelayState::Set => {
this.get_messages(cx);
}
_ => {}
},
RelayState::Set => {
match state.read(cx).messaging_relays_state() {
RelayState::Initial => {
this.get_profile(cx);
this.get_messaging_relays(cx);
}
RelayState::Set => {
this.get_messages(state.read(cx).dekey(), cx);
}
_ => {}
};
}
_ => {}
}
}
@@ -150,7 +158,7 @@ impl NostrRegistry {
tasks.push(
// Update GPUI states
cx.spawn(async move |_this, cx| {
cx.spawn(async move |this, cx| {
while let Ok(event) = rx.recv_async().await {
match event.kind {
Kind::RelayList => {
@@ -165,6 +173,11 @@ impl NostrRegistry {
cx.notify();
})?;
}
Kind::Custom(10044) => {
this.update(cx, |this, cx| {
this.init_dekey(&event, cx);
})?;
}
_ => {}
}
}
@@ -176,6 +189,7 @@ impl NostrRegistry {
Self {
client,
identity,
device_state,
gossip,
app_keys,
_subscriptions: subscriptions,
@@ -183,7 +197,7 @@ impl NostrRegistry {
}
}
// Handle nostr notifications
/// Handle nostr notifications
async fn handle_notifications(client: &Client, tx: &flume::Sender<Event>) -> Result<(), Error> {
// Add bootstrap relay to the relay pool
for url in BOOTSTRAP_RELAYS.into_iter() {
@@ -198,6 +212,7 @@ impl NostrRegistry {
// Connect to all added relays
client.connect().await;
// Handle nostr notifications
let mut notifications = client.notifications();
let mut processed_events = HashSet::new();
@@ -217,7 +232,7 @@ impl NostrRegistry {
Kind::RelayList => {
// Automatically get messaging relays for each member when the user opens a room
if subscription_id.as_str().starts_with("room-") {
Self::get_messaging_relays_by(client, event.as_ref()).await?;
Self::get_adv_events_by(client, event.as_ref()).await?;
}
tx.send_async(event.into_owned()).await?;
@@ -225,6 +240,16 @@ impl NostrRegistry {
Kind::InboxRelays => {
tx.send_async(event.into_owned()).await?;
}
Kind::Custom(10044) => {
if let Ok(signer) = client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
// Only send if the event is from the current user
if public_key == event.pubkey {
tx.send_async(event.into_owned()).await?;
}
}
}
}
_ => {}
}
}
@@ -251,8 +276,8 @@ impl NostrRegistry {
Ok(())
}
/// Automatically get messaging relays from a received relay list
async fn get_messaging_relays_by(client: &Client, event: &Event) -> Result<(), Error> {
/// Automatically get messaging relays and encryption announcement from a received relay list
async fn get_adv_events_by(client: &Client, event: &Event) -> Result<(), Error> {
// Subscription options
let opts = SubscribeAutoCloseOptions::default()
.timeout(Some(Duration::from_secs(TIMEOUT)))
@@ -276,16 +301,20 @@ impl NostrRegistry {
}
// Construct filter for inbox relays
let filter = Filter::new()
let inbox = Filter::new()
.kind(Kind::InboxRelays)
.author(event.pubkey)
.limit(1);
client
.subscribe_to(write_relays, vec![filter], Some(opts))
.await?;
// Construct filter for encryption announcement
let announcement = Filter::new()
.kind(Kind::Custom(10044))
.author(event.pubkey)
.limit(1);
log::info!("Getting inbox relays for: {}", event.pubkey);
client
.subscribe_to(write_relays, vec![inbox, announcement], Some(opts))
.await?;
Ok(())
}
@@ -528,18 +557,8 @@ impl NostrRegistry {
.limit(1)
.author(public_key);
// Filter for encryption keys announcement
let encryption_keys = Filter::new()
.kind(Kind::Custom(10044))
.limit(1)
.author(public_key);
client
.subscribe_to(
urls,
vec![metadata, contact_list, encryption_keys],
Some(opts),
)
.subscribe_to(urls, vec![metadata, contact_list], Some(opts))
.await?;
Ok(())
@@ -604,7 +623,10 @@ impl NostrRegistry {
}
/// Continuously get gift wrap events for the current user in their messaging relays
fn get_messages(&mut self, cx: &mut Context<Self>) {
fn get_messages<T>(&mut self, dekey: Option<T>, cx: &mut Context<Self>)
where
T: NostrSigner + 'static,
{
let client = self.client();
let public_key = self.identity().read(cx).public_key();
let messaging_relays = self.messaging_relays(&public_key, cx);
@@ -612,43 +634,176 @@ impl NostrRegistry {
cx.background_spawn(async move {
let urls = messaging_relays.await;
let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let mut filters = vec![];
if let Err(e) = client
.subscribe_with_id_to(urls, id, vec![filter], None)
.await
{
// Construct a filter to get user messages
filters.push(Filter::new().kind(Kind::GiftWrap).pubkey(public_key));
// Construct a filter to get dekey messages if available
if let Some(signer) = dekey {
if let Ok(pubkey) = signer.get_public_key().await {
filters.push(Filter::new().kind(Kind::GiftWrap).pubkey(pubkey));
}
}
if let Err(e) = client.subscribe_with_id_to(urls, id, filters, None).await {
log::error!("Failed to subscribe to gift wrap events: {e}");
}
})
.detach();
}
/// Subscribe to event kinds to author's write relays
pub fn subscribe<I>(&self, kinds: I, author: PublicKey, cx: &App)
/// Set the decoupled encryption key for the current user
fn set_dekey<T>(&mut self, dekey: T, cx: &mut Context<Self>)
where
I: Into<Vec<Kind>>,
T: NostrSigner + 'static,
{
self.identity.update(cx, |this, cx| {
this.set_dekey(dekey);
cx.notify();
});
self.device_state.update(cx, |this, cx| {
*this = DeviceState::Set;
cx.notify();
});
}
/// Initialize dekey (decoupled encryption key) for the current user
fn init_dekey(&mut self, event: &Event, cx: &mut Context<Self>) {
let client = self.client();
let write_relays = self.write_relays(&author, cx);
let announcement = Announcement::from(event);
let dekey = announcement.public_key();
// Construct filters based on event kinds
let filters: Vec<Filter> = kinds
.into()
.into_iter()
.map(|kind| Filter::new().kind(kind).author(author).limit(1))
.collect();
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
cx.background_spawn(async move {
let urls = write_relays.await;
let filter = Filter::new()
.identifier("coop:device")
.kind(Kind::ApplicationSpecificData)
.author(public_key)
.limit(1);
// Construct subscription options
let opts = SubscribeAutoCloseOptions::default()
.timeout(Some(Duration::from_secs(TIMEOUT)))
.exit_policy(ReqExitPolicy::ExitOnEOSE);
if let Some(event) = client.database().query(filter).await?.first() {
let content = signer.nip44_decrypt(&public_key, &event.content).await?;
let secret = SecretKey::parse(&content)?;
let keys = Keys::new(secret);
if let Err(e) = client.subscribe_to(urls, filters, Some(opts)).await {
log::error!("Failed to create a subscription: {e}");
if keys.public_key() == dekey {
Ok(keys)
} else {
Err(anyhow!("Key mismatch"))
}
} else {
Err(anyhow!("Key not found"))
}
});
cx.spawn(async move |this, cx| {
match task.await {
Ok(keys) => {
this.update(cx, |this, cx| {
this.set_dekey(keys, cx);
})
.ok();
}
Err(e) => {
log::warn!("Failed to initialize dekey: {e}");
this.update(cx, |this, cx| {
this.request_dekey(cx);
})
.ok();
}
};
})
.detach();
}
/// Request dekey from other device
fn request_dekey(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let device_state = self.device_state.downgrade();
let public_key = self.identity().read(cx).public_key();
let write_relays = self.write_relays(&public_key, cx);
let app_keys = self.app_keys().clone();
let app_pubkey = app_keys.public_key();
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::Custom(4455))
.author(public_key)
.pubkey(app_pubkey)
.limit(1);
match client.database().query(filter).await?.first_owned() {
Some(event) => {
let root_device = event
.tags
.find(TagKind::custom("P"))
.and_then(|tag| tag.content())
.and_then(|content| PublicKey::parse(content).ok())
.context("Invalid event's tags")?;
let payload = event.content.as_str();
let decrypted = app_keys.nip44_decrypt(&root_device, payload).await?;
let secret = SecretKey::from_hex(&decrypted)?;
let keys = Keys::new(secret);
Ok(Some(keys))
}
None => {
let urls = write_relays.await;
// Construct an event for device key request
let event = EventBuilder::new(Kind::Custom(4454), "")
.tags(vec![
Tag::client(app_name()),
Tag::custom(TagKind::custom("P"), vec![app_pubkey]),
])
.sign(&signer)
.await?;
// Send the event to write relays
client.send_event_to(&urls, &event).await?;
// Construct a filter to get the approval response event
let filter = Filter::new()
.kind(Kind::Custom(4455))
.author(public_key)
.since(Timestamp::now());
// Subscribe to the approval response event
client.subscribe_to(&urls, vec![filter], None).await?;
Ok(None)
}
}
});
cx.spawn(async move |this, cx| {
match task.await {
Ok(Some(keys)) => {
this.update(cx, |this, cx| {
this.set_dekey(keys, cx);
})
.ok();
}
Ok(None) => {
device_state
.update(cx, |this, cx| {
*this = DeviceState::Requesting;
cx.notify();
})
.ok();
}
Err(e) => {
log::error!("Failed to request the encryption key: {e}");
}
};
})
.detach();