build NIP-59 seals manually for nip4e interop

This commit is contained in:
2026-10-04 13:27:43 +07:00
parent a8df0df0cd
commit 758ffaf7d9
6 changed files with 127 additions and 15 deletions
+1
View File
@@ -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
+9 -1
View File
@@ -874,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)?;
+103 -13
View File
@@ -6,14 +6,16 @@ use common::EventExt;
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 yet. Set one up in the settings to message them.";
/// The outcome of delivering an encrypted rumor to a single receiver.
#[derive(Debug, Clone)]
@@ -474,6 +476,9 @@ impl Room {
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
let config = self.config.clone();
let device = DeviceRegistry::global(cx);
let encryption_signer = device.read(cx).signer(cx);
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let user_signer = nostr.read(cx).signer();
@@ -495,14 +500,33 @@ impl Room {
let signer_kind = config.signer_kind();
// Deliver to every receiver concurrently
let sends = receivers
.into_iter()
.map(|receiver| deliver(&client, signer_kind, receiver, &rumor, &user_signer));
let sends = receivers.into_iter().map(|receiver| {
deliver(
&client,
signer_kind,
receiver,
&rumor,
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) {
reports.push(deliver(&client, signer_kind, sender, &rumor, &user_signer).await);
if config.backup() && minimum_success {
reports.push(
deliver(
&client,
signer_kind,
sender,
&rumor,
encryption_signer.as_ref(),
&user_signer,
)
.await,
);
}
reports
@@ -516,6 +540,7 @@ async fn deliver<S>(
signer_kind: &SignerKind,
receiver: Person,
rumor: &UnsignedEvent,
encryption_signer: Option<&UniversalSigner>,
user_signer: &S,
) -> SendReport
where
@@ -532,9 +557,26 @@ where
result: Err(NO_DEKEY.into()),
};
}
// Sender didn't set up a decoupled encryption key
if encryption_signer.is_none() {
return SendReport {
receiver: public_key,
result: Err(USER_NO_DEKEY.into()),
};
}
}
match send_gift_wrap(client, user_signer, &receiver, rumor, signer_kind).await {
match send_gift_wrap(
client,
&receiver,
rumor,
signer_kind,
encryption_signer,
user_signer,
)
.await
{
Ok(output) => SendReport {
receiver: public_key,
result: Ok(output),
@@ -549,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,
@@ -578,11 +621,50 @@ 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)
.await?;
// 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 seal: Event = match encryption_signer {
Some(encryption_signer) => {
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?
}
None => {
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
@@ -591,3 +673,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))
}