client.rs (8395B)
1 #![forbid(unsafe_code)] 2 3 use std::time::Duration; 4 5 use crate::core::nip46::session::Nip46Session; 6 use crate::host_nostr::{ 7 Filter, Kind, RelayPoolNotification, SubscriptionId, Timestamp, with_filter_tag, 8 }; 9 use crate::transport::jsonrpc::{RpcError, params::DEFAULT_TIMEOUT_SECS}; 10 use crate::transport::nostr::protocol::sign_nip46_message; 11 use nostr::JsonUtil; 12 use nostr::UnsignedEvent; 13 use nostr::nips::{ 14 nip44, 15 nip46::{NostrConnectMessage, NostrConnectMethod, NostrConnectRequest, ResponseResult}, 16 }; 17 use tokio::sync::broadcast; 18 use tokio::time::sleep; 19 20 pub async fn sign_event( 21 session: &Nip46Session, 22 mut unsigned: UnsignedEvent, 23 label: &str, 24 ) -> Result<nostr::Event, RpcError> { 25 unsigned.verify_id().map_err(|_| { 26 RpcError::InvalidParams(format!("nip46 {label} unsigned event ID mismatch")) 27 })?; 28 let expected_public_key = unsigned.pubkey; 29 let expected_event_id = unsigned.id(); 30 let req = NostrConnectRequest::SignEvent(unsigned); 31 let response = request(session, req, label).await?; 32 let response = response 33 .to_response(NostrConnectMethod::SignEvent) 34 .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?; 35 36 if let Some(error) = response.error { 37 return Err(RpcError::Other(format!("nip46 {label} error: {error}"))); 38 } 39 40 let event = match response.result { 41 Some(ResponseResult::SignEvent(event)) => *event, 42 Some(_) => { 43 return Err(RpcError::Other(format!( 44 "nip46 {label} unexpected response" 45 ))); 46 } 47 None => return Err(RpcError::Other(format!("nip46 {label} missing response"))), 48 }; 49 50 validate_signed_event_response(expected_public_key, expected_event_id, event, label) 51 } 52 53 pub async fn request( 54 session: &Nip46Session, 55 request: NostrConnectRequest, 56 label: &str, 57 ) -> Result<NostrConnectMessage, RpcError> { 58 session.client.connect().await; 59 session 60 .client 61 .wait_for_connection(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 62 .await; 63 64 let message = NostrConnectMessage::request(&request); 65 let request_id = message.id().to_string(); 66 let filter = response_filter(session, Timestamp::now(), label)?; 67 let notifications = session.client.clone().into_inner().notifications(); 68 let subscription = session 69 .client 70 .subscribe(filter, None) 71 .await 72 .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?; 73 let event = sign_nip46_message(&session.client_keys, session.remote_signer_pubkey, message) 74 .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?; 75 76 if let Err(error) = session 77 .client 78 .send_event(&event) 79 .await 80 .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}"))) 81 { 82 session.client.unsubscribe(&subscription.val).await; 83 return Err(error); 84 } 85 86 wait_for_response( 87 session, 88 &request_id, 89 label, 90 notifications, 91 &subscription.val, 92 ) 93 .await 94 } 95 96 fn validate_signed_event_response( 97 expected_public_key: nostr::PublicKey, 98 expected_event_id: nostr::EventId, 99 event: nostr::Event, 100 label: &str, 101 ) -> Result<nostr::Event, RpcError> { 102 if event.pubkey != expected_public_key { 103 return Err(RpcError::Other(format!( 104 "nip46 {label} response author mismatch" 105 ))); 106 } 107 if event.id != expected_event_id { 108 return Err(RpcError::Other(format!( 109 "nip46 {label} response event ID mismatch" 110 ))); 111 } 112 event 113 .verify() 114 .map_err(|_| RpcError::Other(format!("nip46 {label} response event is invalid")))?; 115 Ok(event) 116 } 117 118 fn response_filter( 119 session: &Nip46Session, 120 since: Timestamp, 121 label: &str, 122 ) -> Result<Filter, RpcError> { 123 let filter = Filter::new() 124 .kind(Kind::NostrConnect) 125 .author(session.remote_signer_pubkey) 126 .since(since); 127 with_filter_tag(filter, "p", vec![session.client_pubkey.to_hex()]) 128 .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}"))) 129 } 130 131 async fn wait_for_response( 132 session: &Nip46Session, 133 request_id: &str, 134 label: &str, 135 mut notifications: broadcast::Receiver<RelayPoolNotification>, 136 subscription_id: &SubscriptionId, 137 ) -> Result<NostrConnectMessage, RpcError> { 138 let timeout = sleep(Duration::from_secs(DEFAULT_TIMEOUT_SECS)); 139 tokio::pin!(timeout); 140 141 loop { 142 tokio::select! { 143 _ = &mut timeout => { 144 session.client.unsubscribe(subscription_id).await; 145 return Err(RpcError::Other(format!("nip46 {label} response not found"))); 146 } 147 msg = notifications.recv() => { 148 let notification = match msg { 149 Ok(notification) => notification, 150 Err(broadcast::error::RecvError::Lagged(_)) => continue, 151 Err(broadcast::error::RecvError::Closed) => { 152 session.client.unsubscribe(subscription_id).await; 153 return Err(RpcError::Other(format!("nip46 {label} notification closed"))); 154 } 155 }; 156 let RelayPoolNotification::Event { event, .. } = notification else { 157 continue; 158 }; 159 let event = (*event).clone(); 160 if event.kind != Kind::NostrConnect 161 || event.pubkey != session.remote_signer_pubkey 162 { 163 continue; 164 } 165 let decrypted = nip44::decrypt( 166 session.client_keys.secret_key(), 167 &session.remote_signer_pubkey, 168 &event.content, 169 ) 170 .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?; 171 let message = NostrConnectMessage::from_json(&decrypted) 172 .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?; 173 if message.is_response() && message.id() == request_id { 174 session.client.unsubscribe(subscription_id).await; 175 return Ok(message); 176 } 177 } 178 } 179 } 180 } 181 182 #[cfg(test)] 183 mod tests { 184 use crate::host_nostr::Keys; 185 use nostr::{EventBuilder, EventId, Kind, Timestamp}; 186 187 use super::validate_signed_event_response; 188 189 fn signed_fixture() -> (nostr::UnsignedEvent, nostr::Event) { 190 let keys = Keys::generate(); 191 let unsigned = EventBuilder::new(Kind::Custom(30_001), "checked") 192 .custom_created_at(Timestamp::from_secs(1_784_347_200)) 193 .build(keys.public_key()); 194 let event = unsigned 195 .clone() 196 .sign_with_keys(&keys) 197 .expect("signed fixture"); 198 (unsigned, event) 199 } 200 201 #[test] 202 fn sign_event_response_accepts_only_the_exact_valid_event() { 203 let (mut unsigned, event) = signed_fixture(); 204 let expected_public_key = unsigned.pubkey; 205 let expected_event_id = unsigned.id(); 206 assert!( 207 validate_signed_event_response( 208 expected_public_key, 209 expected_event_id, 210 event.clone(), 211 "test", 212 ) 213 .is_ok() 214 ); 215 216 let (_, wrong_author) = signed_fixture(); 217 let error = validate_signed_event_response( 218 expected_public_key, 219 expected_event_id, 220 wrong_author, 221 "test", 222 ) 223 .expect_err("wrong author"); 224 assert!(error.to_string().contains("author mismatch")); 225 226 let mut wrong_id = event.clone(); 227 wrong_id.id = EventId::all_zeros(); 228 let error = validate_signed_event_response( 229 expected_public_key, 230 expected_event_id, 231 wrong_id, 232 "test", 233 ) 234 .expect_err("wrong event ID"); 235 assert!(error.to_string().contains("event ID mismatch")); 236 237 let mut wrong_signature = event; 238 wrong_signature.content.push('!'); 239 let error = validate_signed_event_response( 240 expected_public_key, 241 expected_event_id, 242 wrong_signature, 243 "test", 244 ) 245 .expect_err("invalid event"); 246 assert!(error.to_string().contains("event is invalid")); 247 } 248 }