From 81c0026ca23ba302c224c39c01da596190665d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Calder=C3=B3n?= Date: Mon, 23 Sep 2024 17:50:16 -0300 Subject: [PATCH] Final code on sending messages All messages to mostro are being sent using nip59 Messages between parties are simple nostr messages kind 14 encrypted with nip44 between two parties, users should use ephemeral keys to send this messages. User can get the conversation key of this messages and delivered to an admin in case of a dispute, this way the admin can see the history of the chat between parties --- src/cli.rs | 15 +++--- src/cli/add_invoice.rs | 2 +- src/cli/conversation_key.rs | 39 ++++----------- src/cli/get_dm.rs | 9 +++- src/cli/new_order.rs | 2 +- src/cli/rate_user.rs | 2 +- src/cli/send_dm.rs | 2 +- src/cli/send_msg.rs | 2 +- src/cli/take_buy.rs | 2 +- src/cli/take_dispute.rs | 2 +- src/cli/take_sell.rs | 2 +- src/util.rs | 96 +++++++++++++++++++++++++++---------- 12 files changed, 104 insertions(+), 71 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 17ff099..414bcdd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -149,6 +149,9 @@ pub enum Commands { #[arg(short, long)] #[clap(default_value_t = 30)] since: i64, + /// If true, get messages from counterparty, otherwise from Mostro + #[arg(short)] + from_user: bool, }, /// Send direct message to a user SendDm { @@ -223,9 +226,6 @@ pub enum Commands { /// Pubkey of the counterpart #[arg(short, long)] pubkey: String, - /// Event id of the message to derive the key - #[arg(short, long)] - event_id: String, }, } @@ -305,9 +305,8 @@ pub async fn run() -> Result<()> { if let Some(cmd) = cli.command { match &cmd { - Commands::ConversationKey { event_id, pubkey } => { - execute_conversation_key(&my_key, PublicKey::from_str(pubkey)?, &client, event_id) - .await? + Commands::ConversationKey { pubkey } => { + execute_conversation_key(&my_key, PublicKey::from_str(pubkey)?).await? } Commands::ListOrders { status, @@ -327,7 +326,9 @@ pub async fn run() -> Result<()> { Commands::AddInvoice { order_id, invoice } => { execute_add_invoice(order_id, invoice, &my_key, mostro_key, &client).await? } - Commands::GetDm { since } => execute_get_dm(since, &my_key, &client).await?, + Commands::GetDm { since, from_user } => { + execute_get_dm(since, &my_key, &client, *from_user).await? + } Commands::FiatSent { order_id } | Commands::Release { order_id } | Commands::Dispute { order_id } diff --git a/src/cli/add_invoice.rs b/src/cli/add_invoice.rs index e82854a..6f383be 100644 --- a/src/cli/add_invoice.rs +++ b/src/cli/add_invoice.rs @@ -34,7 +34,7 @@ pub async fn execute_add_invoice( .as_json() .unwrap(); - send_order_id_cmd(client, my_key, mostro_key, add_invoice_message, true).await?; + send_order_id_cmd(client, my_key, mostro_key, add_invoice_message, true, true).await?; Ok(()) } diff --git a/src/cli/conversation_key.rs b/src/cli/conversation_key.rs index 35d5675..989fc11 100644 --- a/src/cli/conversation_key.rs +++ b/src/cli/conversation_key.rs @@ -1,38 +1,17 @@ use anyhow::Result; use nip44::v2::ConversationKey; use nostr_sdk::prelude::*; -use std::str::FromStr; -use crate::util::send_relays_requests; - -pub async fn execute_conversation_key( - my_key: &Keys, - receiver: PublicKey, - client: &Client, - event_id: &str, -) -> Result<()> { - let id = EventId::from_str(event_id)?; - let filter = Filter::new().id(id).limit(1); - let mostro_req = send_relays_requests(client, filter).await; - let event = mostro_req.first().unwrap().first().unwrap(); - // Derive gift wrap conversation key - let gw_ck = ConversationKey::derive(my_key.secret_key()?, &event.pubkey); - let gw_key = gw_ck.as_bytes(); - let mut gw_ck_hex = vec![]; - for i in gw_key { - gw_ck_hex.push(format!("{:02x}", i)); - } - let gw_ck_hex = gw_ck_hex.join(""); - // Derive seal conversation key - let seal_ck = ConversationKey::derive(my_key.secret_key()?, &receiver); - let seal_key = seal_ck.as_bytes(); - let mut seal_ck_hex = vec![]; - for i in seal_key { - seal_ck_hex.push(format!("{:02x}", i)); +pub async fn execute_conversation_key(my_key: &Keys, receiver: PublicKey) -> Result<()> { + // Derive conversation key + let ck = ConversationKey::derive(my_key.secret_key()?, &receiver); + let key = ck.as_bytes(); + let mut ck_hex = vec![]; + for i in key { + ck_hex.push(format!("{:02x}", i)); } - let seal_ck_hex = seal_ck_hex.join(""); - println!("Gift wrap Conversation key: {:?}", gw_ck_hex); - println!("Seal Conversation key: {:?}", seal_ck_hex); + let ck_hex = ck_hex.join(""); + println!("Conversation key: {:?}", ck_hex); Ok(()) } diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index ba83837..528e98b 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -4,8 +4,13 @@ use nostr_sdk::prelude::*; use crate::util::get_direct_messages; -pub async fn execute_get_dm(since: &i64, my_key: &Keys, client: &Client) -> Result<()> { - let dm = get_direct_messages(client, my_key, *since).await; +pub async fn execute_get_dm( + since: &i64, + my_key: &Keys, + client: &Client, + from_user: bool, +) -> Result<()> { + let dm = get_direct_messages(client, my_key, *since, from_user).await; if dm.is_empty() { println!(); println!("No new messages"); diff --git a/src/cli/new_order.rs b/src/cli/new_order.rs index 91a77ae..76ace62 100644 --- a/src/cli/new_order.rs +++ b/src/cli/new_order.rs @@ -113,6 +113,6 @@ pub async fn execute_new_order( .as_json() .unwrap(); - send_order_id_cmd(client, my_key, mostro_key, message, false).await?; + send_order_id_cmd(client, my_key, mostro_key, message, false, true).await?; Ok(()) } diff --git a/src/cli/rate_user.rs b/src/cli/rate_user.rs index 498d951..8e45a3a 100644 --- a/src/cli/rate_user.rs +++ b/src/cli/rate_user.rs @@ -28,7 +28,7 @@ pub async fn execute_rate_user( .as_json() .unwrap(); - send_order_id_cmd(client, my_key, mostro_key, rate_message, true).await?; + send_order_id_cmd(client, my_key, mostro_key, rate_message, true, true).await?; std::process::exit(0); } diff --git a/src/cli/send_dm.rs b/src/cli/send_dm.rs index ce0dcc6..041bd68 100644 --- a/src/cli/send_dm.rs +++ b/src/cli/send_dm.rs @@ -8,7 +8,7 @@ pub async fn execute_send_dm( client: &Client, message: &str, ) -> Result<()> { - send_order_id_cmd(client, my_key, receiver, message.to_string(), true).await?; + send_order_id_cmd(client, my_key, receiver, message.to_string(), true, true).await?; Ok(()) } diff --git a/src/cli/send_msg.rs b/src/cli/send_msg.rs index 1623685..bc385ea 100644 --- a/src/cli/send_msg.rs +++ b/src/cli/send_msg.rs @@ -47,7 +47,7 @@ pub async fn execute_send_msg( .as_json() .unwrap(); info!("Sending message: {:#?}", message); - send_order_id_cmd(client, my_key, mostro_key, message, false).await?; + send_order_id_cmd(client, my_key, mostro_key, message, false, true).await?; Ok(()) } diff --git a/src/cli/take_buy.rs b/src/cli/take_buy.rs index b1e4127..e41eef2 100644 --- a/src/cli/take_buy.rs +++ b/src/cli/take_buy.rs @@ -26,7 +26,7 @@ pub async fn execute_take_buy( .as_json() .unwrap(); - send_order_id_cmd(client, my_key, mostro_key, take_buy_message, true).await?; + send_order_id_cmd(client, my_key, mostro_key, take_buy_message, true, true).await?; Ok(()) } diff --git a/src/cli/take_dispute.rs b/src/cli/take_dispute.rs index 19d2d78..c8d055e 100644 --- a/src/cli/take_dispute.rs +++ b/src/cli/take_dispute.rs @@ -22,7 +22,7 @@ pub async fn execute_take_dispute( .as_json() .unwrap(); - send_order_id_cmd(client, my_key, mostro_key, take_dispute_message, true).await?; + send_order_id_cmd(client, my_key, mostro_key, take_dispute_message, true, true).await?; Ok(()) } diff --git a/src/cli/take_sell.rs b/src/cli/take_sell.rs index 89e2f04..d8cb259 100644 --- a/src/cli/take_sell.rs +++ b/src/cli/take_sell.rs @@ -50,6 +50,6 @@ pub async fn execute_take_sell( .as_json() .unwrap(); - send_order_id_cmd(client, my_key, mostro_key, take_sell_message, true).await?; + send_order_id_cmd(client, my_key, mostro_key, take_sell_message, true, true).await?; Ok(()) } diff --git a/src/util.rs b/src/util.rs index 5c366f6..d91d09c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -2,6 +2,8 @@ use crate::nip33::{dispute_from_tags, order_from_tags}; use crate::nip59::{gift_wrap, unwrap_gift_wrap}; use anyhow::{Error, Result}; +use base64::engine::general_purpose; +use base64::Engine; use chrono::DateTime; use dotenvy::var; use log::{error, info}; @@ -14,6 +16,7 @@ use nostr_sdk::prelude::*; use std::time::Duration; use tokio::time::timeout; use uuid::Uuid; +use v2::{decrypt_to_bytes, encrypt_to_bytes, ConversationKey}; pub fn get_keys() -> Result { // nostr private key @@ -28,9 +31,26 @@ pub async fn send_dm( sender_keys: &Keys, receiver_pubkey: &PublicKey, content: String, + to_user: bool, ) -> Result<()> { let pow: u8 = var("POW").unwrap_or('0'.to_string()).parse().unwrap(); - let event = gift_wrap(sender_keys, *receiver_pubkey, content, None, pow)?; + let event = if to_user { + // Derive conversation key + let ck = ConversationKey::derive(sender_keys.secret_key()?, receiver_pubkey); + // Encrypt content + let encrypted_content = encrypt_to_bytes(&ck, content)?; + // Encode with base64 + let b64decoded_content = general_purpose::STANDARD.encode(encrypted_content); + // Compose builder + EventBuilder::new( + Kind::PrivateDirectMessage, + b64decoded_content, + [Tag::public_key(*receiver_pubkey)], + ) + .to_pow_event(sender_keys, pow)? + } else { + gift_wrap(sender_keys, *receiver_pubkey, content, None, pow)? + }; info!("Sending event: {event:#?}"); println!("Sending event Id: {}", event.id()); @@ -62,18 +82,20 @@ pub async fn connect_nostr() -> Result { pub async fn send_order_id_cmd( client: &Client, my_key: &Keys, - mostro_pubkey: PublicKey, + receiver_pubkey: PublicKey, message: String, wait_for_dm_ans: bool, + to_user: bool, ) -> Result<()> { - // Send dm to mostro pub id - send_dm(client, my_key, &mostro_pubkey, message).await?; + println!("to_user: {to_user}"); + // Send dm to receiver pubkey + send_dm(client, my_key, &receiver_pubkey, message, to_user).await?; let mut notifications = client.notifications(); while let Ok(notification) = notifications.recv().await { if wait_for_dm_ans { - let dm = get_direct_messages(client, my_key, 1).await; + let dm = get_direct_messages(client, my_key, 1, to_user).await; for el in dm.iter() { match Message::from_json(&el.0) { @@ -223,6 +245,7 @@ pub async fn get_direct_messages( client: &Client, my_key: &Keys, since: i64, + from_user: bool, ) -> Vec<(String, String, u64)> { // We use a fake timestamp to thwart time-analysis attacks let fake_since = 2880; @@ -232,10 +255,22 @@ pub async fn get_direct_messages( .timestamp() as u64; let fake_timestamp = Timestamp::from(fake_since_time); - let filters = Filter::new() - .kind(Kind::GiftWrap) - .pubkey(my_key.public_key()) - .since(fake_timestamp); + let filters = if from_user { + let since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(since)) + .unwrap() + .timestamp() as u64; + let timestamp = Timestamp::from(since_time); + Filter::new() + .kind(Kind::PrivateDirectMessage) + .pubkey(my_key.public_key()) + .since(timestamp) + } else { + Filter::new() + .kind(Kind::GiftWrap) + .pubkey(my_key.public_key()) + .since(fake_timestamp) + }; info!("Request events with event kind : {:?} ", filters.kinds); @@ -252,30 +287,43 @@ pub async fn get_direct_messages( for dm in dms { if !id_list.contains(&dm.id()) { id_list.push(dm.id()); - let unwrapped_gift = match unwrap_gift_wrap(Some(my_key), None, None, dm) { - Ok(u) => u, - Err(_) => { - continue; - } - }; + let created_at: Timestamp; + let content: String; + if from_user { + let ck = ConversationKey::derive(my_key.secret_key().unwrap(), &dm.pubkey); + let b64decoded_content = + match general_purpose::STANDARD.decode(dm.content.as_bytes()) { + Ok(b64decoded_content) => b64decoded_content, + Err(_) => { + continue; + } + }; + // Decrypt + let unencrypted_content = decrypt_to_bytes(&ck, b64decoded_content).unwrap(); + content = String::from_utf8(unencrypted_content).expect("Found invalid UTF-8"); + created_at = dm.created_at; + } else { + let unwrapped_gift = match unwrap_gift_wrap(Some(my_key), None, None, dm) { + Ok(u) => u, + Err(_) => { + continue; + } + }; + content = unwrapped_gift.rumor.content; + created_at = unwrapped_gift.rumor.created_at; + } // Here we discard messages older than the real since parameter let since_time = chrono::Utc::now() .checked_sub_signed(chrono::Duration::minutes(since)) .unwrap() .timestamp() as u64; - if unwrapped_gift.rumor.created_at.as_u64() < since_time { + if created_at.as_u64() < since_time { continue; } - let date = - DateTime::from_timestamp(unwrapped_gift.rumor.created_at.as_u64() as i64, 0); + let date = DateTime::from_timestamp(created_at.as_u64() as i64, 0); let human_date = date.unwrap().format("%H:%M:%S date - %d/%m/%Y").to_string(); - - direct_messages.push(( - unwrapped_gift.rumor.content, - human_date, - unwrapped_gift.rumor.created_at.as_u64(), - )); + direct_messages.push((content, human_date, created_at.as_u64())); } } }