From fa99828e1b291255c9cd66991ecb029fb2f80aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Calder=C3=B3n?= Date: Wed, 11 Sep 2024 18:08:19 -0300 Subject: [PATCH] Implement gift wrap --- Cargo.lock | 2 ++ src/main.rs | 1 + src/nip59.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/util.rs | 5 ++- 4 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 src/nip59.rs diff --git a/Cargo.lock b/Cargo.lock index 1c5d8a66..40c9f174 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1862,6 +1862,8 @@ dependencies = [ [[package]] name = "mostro-core" version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5044810603714db3c64fb351c555a3aa6222f26203a0f65c1e5aecfed87a24" dependencies = [ "anyhow", "chrono", diff --git a/src/main.rs b/src/main.rs index 6d4cbbbd..d522a397 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ pub mod lnurl; pub mod messages; pub mod models; pub mod nip33; +pub mod nip59; pub mod scheduler; pub mod util; diff --git a/src/nip59.rs b/src/nip59.rs new file mode 100644 index 00000000..9e1ce115 --- /dev/null +++ b/src/nip59.rs @@ -0,0 +1,96 @@ +use base64::engine::{general_purpose, Engine}; +use nip44::v2::{decrypt_to_bytes, encrypt_to_bytes, ConversationKey}; +use nostr_sdk::event::builder::Error as BuilderError; +use nostr_sdk::prelude::*; + +/// Creates a new nip59 event +/// +/// # Arguments +/// +/// * `sender_keys` - The keys of the sender +/// * `receiver` - The public key of the receiver +/// * `rumor` - A regular nostr event, but is not signed. +/// * `expiration` - Time of the expiration of the event +/// +/// # Returns +/// Returns a gift wrap event +/// +pub fn gift_wrap( + sender_keys: &Keys, + receiver: PublicKey, + content: String, + expiration: Option, +) -> Result { + let rumor: UnsignedEvent = EventBuilder::text_note(content, []).to_unsigned_event(receiver); + let seal: Event = seal(sender_keys, &receiver, rumor)?.to_event(sender_keys)?; + + gift_wrap_from_seal(sender_keys, &receiver, &seal, expiration) +} + +pub fn seal( + sender_keys: &Keys, + receiver_pubkey: &PublicKey, + rumor: UnsignedEvent, +) -> Result { + let sender_private_key = sender_keys.secret_key()?; + + // Derive conversation key + let ck = ConversationKey::derive(sender_private_key, receiver_pubkey); + // Encrypt content + let encrypted_content = encrypt_to_bytes(&ck, rumor.as_json()).unwrap(); + // Encode with base64 + let b64decoded_content = general_purpose::STANDARD.encode(encrypted_content); + // Compose builder + Ok(EventBuilder::new(Kind::Seal, b64decoded_content, []) + .custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK))) +} + +pub fn gift_wrap_from_seal( + sender_keys: &Keys, + receiver: &PublicKey, + seal: &Event, + expiration: Option, +) -> Result { + let ephemeral_keys: Keys = Keys::generate(); + // Derive conversation key + let ck = ConversationKey::derive(sender_keys.secret_key()?, receiver); + // Encrypt content + let encrypted_content = encrypt_to_bytes(&ck, seal.as_json()).unwrap(); + + let mut tags: Vec = Vec::with_capacity(1 + usize::from(expiration.is_some())); + tags.push(Tag::public_key(*receiver)); + + if let Some(timestamp) = expiration { + tags.push(Tag::expiration(timestamp)); + } + // Encode with base64 + let b64decoded_content = general_purpose::STANDARD.encode(encrypted_content); + EventBuilder::new(Kind::GiftWrap, b64decoded_content, tags) + .custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK)) + .to_event(&ephemeral_keys) +} + +pub fn unwrap_gift_wrap(keys: &Keys, gift_wrap: &Event) -> Result { + let ck = ConversationKey::derive(keys.secret_key()?, &gift_wrap.pubkey); + let b64decoded_content = general_purpose::STANDARD + .decode(gift_wrap.content.as_bytes()) + .unwrap(); + // Decrypt and verify seal + let seal = decrypt_to_bytes(&ck, b64decoded_content)?; + let seal = String::from_utf8(seal).expect("Found invalid UTF-8"); + let seal: Event = Event::from_json(seal).unwrap(); + seal.verify().unwrap(); + + let ck = ConversationKey::derive(keys.secret_key()?, &seal.pubkey); + let b64decoded_content = general_purpose::STANDARD + .decode(seal.content.as_bytes()) + .unwrap(); + // Decrypt rumor + let rumor = decrypt_to_bytes(&ck, b64decoded_content)?; + let rumor = String::from_utf8(rumor).expect("Found invalid UTF-8"); + + Ok(UnwrappedGift { + sender: seal.pubkey, + rumor: UnsignedEvent::from_json(rumor)?, + }) +} diff --git a/src/util.rs b/src/util.rs index b2dd4e2e..b29c488f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -9,6 +9,7 @@ use crate::lightning::LndConnector; use crate::messages; use crate::models::Yadio; use crate::nip33::{new_event, order_to_tags}; +use crate::nip59::gift_wrap; use crate::NOSTR_CLIENT; use anyhow::{Context, Error, Result}; @@ -243,9 +244,7 @@ pub async fn send_dm(receiver_pubkey: &PublicKey, content: String) -> Result<()> info!("DM content: {content:#?}"); // Get mostro keys let sender_keys = crate::util::get_keys().unwrap(); - - let event = EventBuilder::encrypted_direct_msg(&sender_keys, *receiver_pubkey, content, None)? - .to_event(&sender_keys)?; + let event = gift_wrap(&sender_keys, *receiver_pubkey, content, None)?; info!("Sending event: {event:#?}"); NOSTR_CLIENT.get().unwrap().send_event(event).await?;