nip46.rs (67720B)
1 use std::future::Future; 2 use std::sync::Arc; 3 4 use radroots_nostr::prelude::{ 5 RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrPublicKey, 6 RadrootsNostrRelayPoolNotification, RadrootsNostrRelayUrl, 7 }; 8 use radroots_nostr_connect::prelude::{ 9 RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectRequestMessage, 10 RadrootsNostrConnectResponse, 11 }; 12 use radroots_nostr_signer::prelude::{ 13 RadrootsNostrSignerConnectionId, RadrootsNostrSignerHandledRequestOutcome, 14 RadrootsNostrSignerNip46Handler, RadrootsNostrSignerNip46Signer, 15 RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerWorkflowId, 16 }; 17 use tokio::sync::broadcast; 18 19 #[cfg(test)] 20 use radroots_nostr_signer::prelude::RadrootsNostrSignerHandledRequest; 21 22 use crate::app::MycSignerContext; 23 use crate::app::backend::MycSignerBackend; 24 use crate::audit::{MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord}; 25 use crate::error::MycError; 26 use crate::outbox::{MycDeliveryOutboxKind, MycDeliveryOutboxRecord, MycDeliveryOutboxStore}; 27 use crate::transport::MycNostrTransport; 28 29 type MycNip46CoreHandler = RadrootsNostrSignerNip46Handler< 30 MycSignerBackend, 31 crate::policy::MycPolicyContext, 32 MycNip46Signer, 33 >; 34 35 #[derive(Clone)] 36 pub struct MycNip46Handler { 37 signer: MycSignerContext, 38 handler: MycNip46CoreHandler, 39 } 40 41 pub struct MycNip46Service { 42 handler: MycNip46Handler, 43 transport: MycNostrTransport, 44 delivery_outbox_store: Arc<dyn MycDeliveryOutboxStore>, 45 } 46 47 type MycNip46HandledOutcome = RadrootsNostrSignerHandledRequestOutcome; 48 49 #[derive(Clone)] 50 struct MycNip46Signer { 51 signer: MycSignerContext, 52 } 53 54 impl RadrootsNostrSignerNip46Signer for MycNip46Signer { 55 fn signer_public_key_hex(&self) -> String { 56 self.signer.signer_public_identity().public_key_hex 57 } 58 59 fn decrypt_request( 60 &self, 61 client_public_key: &RadrootsNostrPublicKey, 62 ciphertext: &str, 63 ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> { 64 self.signer 65 .signer_identity() 66 .nip44_decrypt(client_public_key, ciphertext) 67 .map_err(|error| { 68 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string()) 69 }) 70 } 71 72 fn encrypt_response( 73 &self, 74 client_public_key: &RadrootsNostrPublicKey, 75 payload: &str, 76 ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> { 77 self.signer 78 .signer_identity() 79 .nip44_encrypt(client_public_key, payload.to_owned()) 80 .map_err(|error| { 81 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string()) 82 }) 83 } 84 85 fn user_identity(&self) -> radroots_identity::RadrootsIdentityPublic { 86 self.signer.user_public_identity() 87 } 88 89 fn sign_user_event( 90 &self, 91 unsigned_event: nostr::UnsignedEvent, 92 ) -> Result<RadrootsNostrEvent, radroots_nostr_signer::prelude::RadrootsNostrSignerError> { 93 self.signer 94 .user_identity() 95 .sign_unsigned_event(unsigned_event, "managed user sign_event") 96 .map_err(|error| { 97 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string()) 98 }) 99 } 100 101 fn nip04_encrypt( 102 &self, 103 public_key: &RadrootsNostrPublicKey, 104 plaintext: &str, 105 ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> { 106 self.signer 107 .user_identity() 108 .nip04_encrypt(public_key, plaintext.to_owned()) 109 .map_err(|error| { 110 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string()) 111 }) 112 } 113 114 fn nip04_decrypt( 115 &self, 116 public_key: &RadrootsNostrPublicKey, 117 ciphertext: &str, 118 ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> { 119 self.signer 120 .user_identity() 121 .nip04_decrypt(public_key, ciphertext.to_owned()) 122 .map_err(|error| { 123 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string()) 124 }) 125 } 126 127 fn nip44_encrypt( 128 &self, 129 public_key: &RadrootsNostrPublicKey, 130 plaintext: &str, 131 ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> { 132 self.signer 133 .user_identity() 134 .nip44_encrypt(public_key, plaintext.to_owned()) 135 .map_err(|error| { 136 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string()) 137 }) 138 } 139 140 fn nip44_decrypt( 141 &self, 142 public_key: &RadrootsNostrPublicKey, 143 ciphertext: &str, 144 ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> { 145 self.signer 146 .user_identity() 147 .nip44_decrypt(public_key, ciphertext.to_owned()) 148 .map_err(|error| { 149 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string()) 150 }) 151 } 152 } 153 154 impl MycNip46Handler { 155 pub fn new(signer: MycSignerContext, relays: Vec<RadrootsNostrRelayUrl>) -> Self { 156 let handler = RadrootsNostrSignerNip46Handler::new( 157 MycSignerBackend::new(signer.clone()), 158 signer.policy().clone(), 159 relays, 160 MycNip46Signer { 161 signer: signer.clone(), 162 }, 163 ); 164 Self { signer, handler } 165 } 166 167 pub fn filter(&self) -> Result<RadrootsNostrFilter, MycError> { 168 self.handler.filter().map_err(Into::into) 169 } 170 171 pub fn parse_request_event( 172 &self, 173 event: &RadrootsNostrEvent, 174 ) -> Result<RadrootsNostrConnectRequestMessage, MycError> { 175 self.handler.parse_request_event(event).map_err(Into::into) 176 } 177 178 pub fn build_response_event( 179 &self, 180 client_public_key: RadrootsNostrPublicKey, 181 request_id: impl Into<String>, 182 response: RadrootsNostrConnectResponse, 183 ) -> Result<radroots_nostr::prelude::RadrootsNostrEventBuilder, MycError> { 184 self.handler 185 .build_response_event(client_public_key, request_id, response) 186 .map_err(Into::into) 187 } 188 189 pub(crate) fn handle_request( 190 &self, 191 client_public_key: RadrootsNostrPublicKey, 192 request_message: RadrootsNostrConnectRequestMessage, 193 ) -> Result<MycNip46HandledOutcome, MycError> { 194 self.handler 195 .handle_request(client_public_key, request_message) 196 .map_err(Into::into) 197 } 198 199 #[cfg(test)] 200 fn handle_request_response( 201 &self, 202 client_public_key: RadrootsNostrPublicKey, 203 request_message: RadrootsNostrConnectRequestMessage, 204 ) -> Result<RadrootsNostrConnectResponse, MycError> { 205 match self.handle_request(client_public_key, request_message)? { 206 MycNip46HandledOutcome { 207 handled_request: RadrootsNostrSignerHandledRequest::Respond { response, .. }, 208 .. 209 } => Ok(response), 210 MycNip46HandledOutcome { 211 handled_request: RadrootsNostrSignerHandledRequest::Ignore, 212 .. 213 } => Err(MycError::InvalidOperation( 214 "request was ignored without a response".to_owned(), 215 )), 216 } 217 } 218 219 pub(crate) fn handle_authorized_request_evaluation( 220 &self, 221 request_message: RadrootsNostrConnectRequestMessage, 222 evaluation: RadrootsNostrSignerRequestEvaluation, 223 ) -> Result<MycNip46HandledOutcome, MycError> { 224 self.handler 225 .handle_authorized_request_evaluation(request_message, evaluation) 226 .map_err(Into::into) 227 } 228 } 229 230 impl MycNip46Service { 231 pub fn new( 232 signer: MycSignerContext, 233 transport: MycNostrTransport, 234 delivery_outbox_store: Arc<dyn MycDeliveryOutboxStore>, 235 ) -> Self { 236 let handler = MycNip46Handler::new(signer, transport.relays().to_vec()); 237 Self { 238 handler, 239 transport, 240 delivery_outbox_store, 241 } 242 } 243 244 pub async fn run(&self) -> Result<(), MycError> { 245 self.run_until(std::future::pending()).await 246 } 247 248 pub async fn run_until<F>(&self, shutdown: F) -> Result<(), MycError> 249 where 250 F: Future<Output = ()>, 251 { 252 tokio::pin!(shutdown); 253 self.transport.connect().await?; 254 255 let filter = self.handler.filter()?; 256 let mut notifications = self.transport.client().notifications(); 257 let subscription = self.transport.client().subscribe(filter, None).await?; 258 tracing::info!( 259 subscription_id = %subscription.val, 260 relay_count = self.transport.relays().len(), 261 "myc NIP-46 listener subscribed" 262 ); 263 264 loop { 265 let notification = tokio::select! { 266 _ = &mut shutdown => return Ok(()), 267 notification = notifications.recv() => { 268 match notification { 269 Ok(notification) => notification, 270 Err(broadcast::error::RecvError::Lagged(_)) => continue, 271 Err(broadcast::error::RecvError::Closed) => { 272 return Err(MycError::Nip46ListenerClosed); 273 } 274 } 275 } 276 }; 277 let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else { 278 continue; 279 }; 280 let event = *event; 281 if event.kind != RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) { 282 continue; 283 } 284 285 let request_message = match self.handler.parse_request_event(&event) { 286 Ok(message) => message, 287 Err(error) => { 288 tracing::warn!(error = %error, "discarding invalid NIP-46 request event"); 289 continue; 290 } 291 }; 292 293 let request_id = request_message.id.clone(); 294 let handled_outcome = match self.handler.handle_request(event.pubkey, request_message) { 295 Ok(handled_outcome) => handled_outcome, 296 Err(error) => { 297 tracing::warn!(error = %error, "failed to handle NIP-46 request"); 298 MycNip46HandledOutcome::respond(RadrootsNostrConnectResponse::Error { 299 result: None, 300 error: error.to_string(), 301 }) 302 } 303 }; 304 if let Some(audit) = handled_outcome.audit.as_ref() { 305 self.handler.signer.record_signer_request_audit(audit); 306 } 307 let Some((response, connection_id, consume_connect_secret_for)) = 308 handled_outcome.handled_request.into_publish_parts() 309 else { 310 tracing::debug!( 311 request_id = %request_id, 312 client_public_key = %event.pubkey, 313 "ignoring NIP-46 request without response" 314 ); 315 continue; 316 }; 317 318 let response_event = 319 self.handler 320 .build_response_event(event.pubkey, request_id.as_str(), response)?; 321 let response_event = match self 322 .handler 323 .signer 324 .signer_identity() 325 .sign_event_builder(response_event, "NIP-46 response") 326 { 327 Ok(event) => event, 328 Err(error) => { 329 self.record_listener_publish_local_rejection( 330 connection_id.as_ref(), 331 request_id.as_str(), 332 format!("failed to sign NIP-46 response event: {error}"), 333 ); 334 continue; 335 } 336 }; 337 338 let mut workflow_id = None; 339 if let Some(connect_connection_id) = consume_connect_secret_for.as_ref() { 340 let manager = match self.handler.signer.load_signer_manager() { 341 Ok(manager) => manager, 342 Err(error) => { 343 self.record_listener_publish_local_rejection( 344 connection_id.as_ref(), 345 request_id.as_str(), 346 error.to_string(), 347 ); 348 continue; 349 } 350 }; 351 match manager.begin_connect_secret_publish_finalization(connect_connection_id) { 352 Ok(workflow) => workflow_id = Some(workflow.workflow_id), 353 Err(error) => { 354 self.record_listener_publish_local_rejection( 355 connection_id.as_ref(), 356 request_id.as_str(), 357 format!( 358 "failed to begin connect-secret publish finalization workflow: {error}" 359 ), 360 ); 361 continue; 362 } 363 } 364 } 365 366 let outbox_record = match self.build_listener_outbox_record( 367 response_event.clone(), 368 connection_id.as_ref(), 369 request_id.as_str(), 370 workflow_id.as_ref(), 371 ) { 372 Ok(record) => record, 373 Err(error) => { 374 let error = self 375 .cancel_listener_publish_workflow_if_needed(workflow_id.as_ref(), error); 376 self.record_listener_publish_local_rejection( 377 connection_id.as_ref(), 378 request_id.as_str(), 379 error.to_string(), 380 ); 381 continue; 382 } 383 }; 384 if let Err(error) = self.delivery_outbox_store.enqueue(&outbox_record) { 385 let error = 386 self.cancel_listener_publish_workflow_if_needed(workflow_id.as_ref(), error); 387 self.record_listener_publish_local_rejection( 388 connection_id.as_ref(), 389 request_id.as_str(), 390 error.to_string(), 391 ); 392 continue; 393 } 394 let publish_outcome = match self 395 .transport 396 .publish_event("NIP-46 response publish", &response_event) 397 .await 398 { 399 Ok(publish_outcome) => publish_outcome, 400 Err(error) => { 401 let mut error = self.record_listener_outbox_failure(&outbox_record, error); 402 error = self 403 .cancel_listener_publish_workflow_if_needed(workflow_id.as_ref(), error); 404 self.record_listener_publish_error( 405 connection_id.as_ref(), 406 request_id.as_str(), 407 &error, 408 ); 409 continue; 410 } 411 }; 412 if let Some(workflow_id) = workflow_id.as_ref() { 413 let manager = match self.handler.signer.load_signer_manager() { 414 Ok(manager) => manager, 415 Err(error) => { 416 self.record_listener_publish_post_publish_failure( 417 connection_id.as_ref(), 418 request_id.as_str(), 419 &publish_outcome, 420 format!( 421 "failed to load signer manager for publish finalization: {error}" 422 ), 423 ); 424 continue; 425 } 426 }; 427 if let Err(error) = manager.mark_publish_workflow_published(workflow_id) { 428 self.record_listener_publish_post_publish_failure( 429 connection_id.as_ref(), 430 request_id.as_str(), 431 &publish_outcome, 432 format!("failed to mark signer publish workflow as published: {error}"), 433 ); 434 continue; 435 } 436 } 437 if let Err(error) = self.delivery_outbox_store.mark_published_pending_finalize( 438 &outbox_record.job_id, 439 publish_outcome.attempt_count, 440 ) { 441 self.record_listener_publish_post_publish_failure( 442 connection_id.as_ref(), 443 request_id.as_str(), 444 &publish_outcome, 445 format!("failed to persist delivery outbox published state: {error}"), 446 ); 447 continue; 448 } 449 if let Some(workflow_id) = workflow_id.as_ref() { 450 let manager = match self.handler.signer.load_signer_manager() { 451 Ok(manager) => manager, 452 Err(error) => { 453 self.record_listener_publish_post_publish_failure( 454 connection_id.as_ref(), 455 request_id.as_str(), 456 &publish_outcome, 457 format!("failed to load signer manager for publish workflow finalization: {error}"), 458 ); 459 continue; 460 } 461 }; 462 if let Err(error) = manager.finalize_publish_workflow(workflow_id) { 463 self.record_listener_publish_post_publish_failure( 464 connection_id.as_ref(), 465 request_id.as_str(), 466 &publish_outcome, 467 format!("failed to finalize signer publish workflow: {error}"), 468 ); 469 continue; 470 } 471 } 472 if let Err(error) = self 473 .delivery_outbox_store 474 .mark_finalized(&outbox_record.job_id) 475 { 476 self.record_listener_publish_post_publish_failure( 477 connection_id.as_ref(), 478 request_id.as_str(), 479 &publish_outcome, 480 format!("failed to finalize delivery outbox job: {error}"), 481 ); 482 continue; 483 } 484 self.record_listener_publish_success( 485 connection_id.as_ref(), 486 request_id.as_str(), 487 &publish_outcome, 488 ); 489 } 490 } 491 492 fn build_listener_outbox_record( 493 &self, 494 response_event: RadrootsNostrEvent, 495 connection_id: Option<&RadrootsNostrSignerConnectionId>, 496 request_id: &str, 497 workflow_id: Option<&RadrootsNostrSignerWorkflowId>, 498 ) -> Result<MycDeliveryOutboxRecord, MycError> { 499 let mut record = MycDeliveryOutboxRecord::new( 500 MycDeliveryOutboxKind::ListenerResponsePublish, 501 response_event, 502 self.transport.relays().to_vec(), 503 )? 504 .with_request_id(request_id.to_owned()); 505 if let Some(connection_id) = connection_id { 506 record = record.with_connection_id(connection_id); 507 } 508 if let Some(workflow_id) = workflow_id { 509 record = record.with_signer_publish_workflow_id(workflow_id); 510 } 511 Ok(record) 512 } 513 514 fn cancel_listener_publish_workflow_if_needed( 515 &self, 516 workflow_id: Option<&RadrootsNostrSignerWorkflowId>, 517 error: MycError, 518 ) -> MycError { 519 let Some(workflow_id) = workflow_id else { 520 return error; 521 }; 522 match self 523 .handler 524 .signer 525 .load_signer_manager() 526 .and_then(|manager| { 527 manager 528 .cancel_publish_workflow(workflow_id) 529 .map(|_| ()) 530 .map_err(Into::into) 531 }) { 532 Ok(()) => error, 533 Err(cancel_error) => MycError::InvalidOperation(format!( 534 "{error}; additionally failed to cancel listener publish workflow: {cancel_error}" 535 )), 536 } 537 } 538 539 fn record_listener_outbox_failure( 540 &self, 541 outbox_record: &MycDeliveryOutboxRecord, 542 error: MycError, 543 ) -> MycError { 544 let publish_attempt_count = error.publish_attempt_count().unwrap_or_default(); 545 let failure_summary = error 546 .publish_rejection_details() 547 .map(ToOwned::to_owned) 548 .unwrap_or_else(|| error.to_string()); 549 match self.delivery_outbox_store.mark_failed( 550 &outbox_record.job_id, 551 publish_attempt_count, 552 &failure_summary, 553 ) { 554 Ok(_) => error, 555 Err(outbox_error) => MycError::InvalidOperation(format!( 556 "{error}; additionally failed to persist listener publish failure to the outbox: {outbox_error}" 557 )), 558 } 559 } 560 561 fn record_listener_publish_local_rejection( 562 &self, 563 connection_id: Option<&RadrootsNostrSignerConnectionId>, 564 request_id: &str, 565 summary: impl Into<String>, 566 ) { 567 self.handler 568 .signer 569 .record_operation_audit(&MycOperationAuditRecord::new( 570 MycOperationAuditKind::ListenerResponsePublish, 571 MycOperationAuditOutcome::Rejected, 572 connection_id, 573 Some(request_id), 574 self.transport.relays().len(), 575 0, 576 summary.into(), 577 )); 578 } 579 580 fn record_listener_publish_error( 581 &self, 582 connection_id: Option<&RadrootsNostrSignerConnectionId>, 583 request_id: &str, 584 error: &MycError, 585 ) { 586 let mut record = MycOperationAuditRecord::new( 587 MycOperationAuditKind::ListenerResponsePublish, 588 MycOperationAuditOutcome::Rejected, 589 connection_id, 590 Some(request_id), 591 error 592 .publish_rejection_counts() 593 .map(|(relay_count, _)| relay_count) 594 .unwrap_or(self.transport.relays().len()), 595 error 596 .publish_rejection_counts() 597 .map(|(_, acknowledged)| acknowledged) 598 .unwrap_or_default(), 599 error 600 .publish_rejection_details() 601 .map(ToOwned::to_owned) 602 .unwrap_or_else(|| error.to_string()), 603 ); 604 if let ( 605 Some(delivery_policy), 606 Some(required_acknowledged_relay_count), 607 Some(attempt_count), 608 ) = ( 609 error.publish_delivery_policy(), 610 error.publish_required_acknowledged_relay_count(), 611 error.publish_attempt_count(), 612 ) { 613 record = record.with_delivery_details( 614 delivery_policy, 615 required_acknowledged_relay_count, 616 attempt_count, 617 ); 618 } 619 self.handler.signer.record_operation_audit(&record); 620 } 621 622 fn record_listener_publish_post_publish_failure( 623 &self, 624 connection_id: Option<&RadrootsNostrSignerConnectionId>, 625 request_id: &str, 626 publish_outcome: &crate::transport::MycPublishOutcome, 627 summary: impl Into<String>, 628 ) { 629 self.handler.signer.record_operation_audit( 630 &MycOperationAuditRecord::new( 631 MycOperationAuditKind::ListenerResponsePublish, 632 MycOperationAuditOutcome::Rejected, 633 connection_id, 634 Some(request_id), 635 publish_outcome.relay_count, 636 publish_outcome.acknowledged_relay_count, 637 summary.into(), 638 ) 639 .with_delivery_details( 640 publish_outcome.delivery_policy, 641 publish_outcome.required_acknowledged_relay_count, 642 publish_outcome.attempt_count, 643 ), 644 ); 645 } 646 647 fn record_listener_publish_success( 648 &self, 649 connection_id: Option<&RadrootsNostrSignerConnectionId>, 650 request_id: &str, 651 publish_outcome: &crate::transport::MycPublishOutcome, 652 ) { 653 self.handler.signer.record_operation_audit( 654 &MycOperationAuditRecord::new( 655 MycOperationAuditKind::ListenerResponsePublish, 656 MycOperationAuditOutcome::Succeeded, 657 connection_id, 658 Some(request_id), 659 publish_outcome.relay_count, 660 publish_outcome.acknowledged_relay_count, 661 publish_outcome.relay_outcome_summary.clone(), 662 ) 663 .with_delivery_details( 664 publish_outcome.delivery_policy, 665 publish_outcome.required_acknowledged_relay_count, 666 publish_outcome.attempt_count, 667 ), 668 ); 669 } 670 } 671 672 #[cfg(test)] 673 mod tests { 674 use nostr::nips::nip04; 675 use nostr::nips::nip44; 676 use nostr::nips::nip44::Version; 677 use nostr::{EventBuilder, Keys, PublicKey, SecretKey, Timestamp, UnsignedEvent}; 678 use radroots_nostr::prelude::{RadrootsNostrTag, radroots_nostr_kind}; 679 use radroots_nostr_connect::prelude::{ 680 RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectMethod, 681 RadrootsNostrConnectPermission, RadrootsNostrConnectRequest, 682 RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, 683 RadrootsNostrConnectResponseEnvelope, 684 }; 685 use radroots_nostr_signer::prelude::{ 686 RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerHandledRequest, 687 }; 688 use serde_json::json; 689 690 use crate::app::MycRuntime; 691 use crate::config::{MycConfig, MycConnectionApproval}; 692 693 use super::MycNip46Handler; 694 695 fn write_identity(path: &std::path::Path, secret_key: &str) { 696 let identity = 697 radroots_identity::RadrootsIdentity::from_secret_key_str(secret_key).expect("identity"); 698 crate::identity_files::store_encrypted_identity(path, &identity).expect("save identity"); 699 } 700 701 fn runtime() -> MycRuntime { 702 runtime_with_config(MycConnectionApproval::NotRequired, |_| {}) 703 } 704 705 fn runtime_with_config<F>(approval: MycConnectionApproval, configure: F) -> MycRuntime 706 where 707 F: FnOnce(&mut MycConfig), 708 { 709 let temp = tempfile::tempdir().expect("tempdir").keep(); 710 let mut config = MycConfig::default(); 711 config.paths.state_dir = temp.join("state"); 712 config.paths.signer_identity_path = temp.join("signer.json"); 713 config.paths.user_identity_path = temp.join("user.json"); 714 config.policy.connection_approval = approval; 715 config.transport.enabled = true; 716 config.transport.connect_timeout_secs = 15; 717 config.transport.relays = vec!["wss://relay.example.com".to_owned()]; 718 configure(&mut config); 719 write_identity( 720 &config.paths.signer_identity_path, 721 "1111111111111111111111111111111111111111111111111111111111111111", 722 ); 723 write_identity( 724 &config.paths.user_identity_path, 725 "2222222222222222222222222222222222222222222222222222222222222222", 726 ); 727 MycRuntime::bootstrap(config).expect("runtime") 728 } 729 730 fn runtime_with_explicit_approval() -> MycRuntime { 731 runtime_with_config(MycConnectionApproval::ExplicitUser, |_| {}) 732 } 733 734 fn handler(runtime: &MycRuntime) -> MycNip46Handler { 735 MycNip46Handler::new( 736 runtime.signer_context(), 737 runtime.transport().expect("transport").relays().to_vec(), 738 ) 739 } 740 741 fn client_keys() -> Keys { 742 client_keys_from_hex("3333333333333333333333333333333333333333333333333333333333333333") 743 } 744 745 fn client_keys_from_hex(secret_key: &str) -> Keys { 746 let secret = SecretKey::from_hex(secret_key).expect("secret"); 747 Keys::new(secret) 748 } 749 750 fn request_event( 751 handler: &MycNip46Handler, 752 request: RadrootsNostrConnectRequestMessage, 753 ) -> nostr::Event { 754 request_event_with_client_keys(handler, request, &client_keys()) 755 } 756 757 fn request_event_with_client_keys( 758 handler: &MycNip46Handler, 759 request: RadrootsNostrConnectRequestMessage, 760 client_keys: &Keys, 761 ) -> nostr::Event { 762 let payload = serde_json::to_string(&request).expect("serialize request"); 763 let ciphertext = nip44::encrypt( 764 client_keys.secret_key(), 765 &PublicKey::parse( 766 handler 767 .signer 768 .signer_public_identity() 769 .public_key_hex 770 .as_str(), 771 ) 772 .expect("signer pubkey"), 773 payload, 774 Version::V2, 775 ) 776 .expect("encrypt"); 777 EventBuilder::new( 778 radroots_nostr_kind(RADROOTS_NOSTR_CONNECT_RPC_KIND), 779 ciphertext, 780 ) 781 .tags(vec![RadrootsNostrTag::public_key( 782 handler.signer.signer_identity().public_key(), 783 )]) 784 .sign_with_keys(client_keys) 785 .expect("sign request") 786 } 787 788 fn sign_event_permission(kind: u16) -> RadrootsNostrConnectPermission { 789 RadrootsNostrConnectPermission::with_parameter( 790 RadrootsNostrConnectMethod::SignEvent, 791 format!("kind:{kind}"), 792 ) 793 } 794 795 fn unsigned_event(pubkey: PublicKey, kind: u16, content: &str) -> UnsignedEvent { 796 serde_json::from_value(json!({ 797 "pubkey": pubkey.to_hex(), 798 "created_at": Timestamp::from(1).as_secs(), 799 "kind": kind, 800 "tags": [], 801 "content": content 802 })) 803 .expect("unsigned event") 804 } 805 806 fn connect_with_permissions( 807 handler: &MycNip46Handler, 808 runtime: &MycRuntime, 809 requested_permissions: Vec<RadrootsNostrConnectPermission>, 810 ) { 811 handler 812 .handle_request_response( 813 client_keys().public_key(), 814 RadrootsNostrConnectRequestMessage::new( 815 "req-connect", 816 RadrootsNostrConnectRequest::Connect { 817 remote_signer_public_key: runtime.signer_identity().public_key(), 818 secret: None, 819 requested_permissions: requested_permissions.into(), 820 }, 821 ), 822 ) 823 .expect("connect"); 824 } 825 826 fn connection_for( 827 runtime: &MycRuntime, 828 client_public_key: PublicKey, 829 ) -> RadrootsNostrSignerConnectionRecord { 830 runtime 831 .signer_manager() 832 .expect("manager") 833 .find_connections_by_client_public_key(&client_public_key) 834 .expect("connections") 835 .into_iter() 836 .next() 837 .expect("connection") 838 } 839 840 #[test] 841 fn parse_and_build_nip46_envelopes_roundtrip() { 842 let runtime = runtime(); 843 let handler = handler(&runtime); 844 let request = 845 RadrootsNostrConnectRequestMessage::new("req-1", RadrootsNostrConnectRequest::Ping); 846 let event = request_event(&handler, request.clone()); 847 848 let parsed = handler.parse_request_event(&event).expect("parse request"); 849 assert_eq!(parsed, request); 850 851 let response_builder = handler 852 .build_response_event(event.pubkey, "req-1", RadrootsNostrConnectResponse::Pong) 853 .expect("response builder"); 854 let response_event = runtime 855 .signer_identity() 856 .sign_event_builder(response_builder, "test response") 857 .expect("sign response"); 858 let decrypted = nip44::decrypt( 859 client_keys().secret_key(), 860 &runtime.signer_identity().public_key(), 861 &response_event.content, 862 ) 863 .expect("decrypt response"); 864 let envelope: RadrootsNostrConnectResponseEnvelope = 865 serde_json::from_str(&decrypted).expect("parse envelope"); 866 let parsed = RadrootsNostrConnectResponse::from_envelope( 867 &RadrootsNostrConnectRequest::Ping.method(), 868 envelope, 869 ) 870 .expect("parse response"); 871 assert_eq!(parsed, RadrootsNostrConnectResponse::Pong); 872 } 873 874 #[test] 875 fn connect_registers_client_and_echoes_secret() { 876 let runtime = runtime(); 877 let handler = handler(&runtime); 878 let response = handler 879 .handle_request_response( 880 client_keys().public_key(), 881 RadrootsNostrConnectRequestMessage::new( 882 "req-connect", 883 RadrootsNostrConnectRequest::Connect { 884 remote_signer_public_key: runtime.signer_identity().public_key(), 885 secret: Some("s3cr3t".to_owned()), 886 requested_permissions: Default::default(), 887 }, 888 ), 889 ) 890 .expect("connect response"); 891 892 assert_eq!( 893 response, 894 RadrootsNostrConnectResponse::ConnectSecretEcho("s3cr3t".to_owned()) 895 ); 896 let connections = runtime 897 .signer_manager() 898 .expect("manager") 899 .list_connections() 900 .expect("connections"); 901 assert_eq!(connections.len(), 1); 902 assert_eq!( 903 connections[0].user_identity.id.to_string(), 904 runtime.user_public_identity().id.to_string() 905 ); 906 assert_eq!(connections[0].relays.len(), 1); 907 } 908 909 #[test] 910 fn denied_clients_are_rejected_without_registration() { 911 let denied_client_keys = client_keys_from_hex( 912 "4444444444444444444444444444444444444444444444444444444444444444", 913 ); 914 let runtime = runtime_with_config(MycConnectionApproval::ExplicitUser, |config| { 915 config.policy.denied_client_pubkeys = vec![denied_client_keys.public_key().to_hex()]; 916 }); 917 let handler = handler(&runtime); 918 919 let response = handler 920 .handle_request_response( 921 denied_client_keys.public_key(), 922 RadrootsNostrConnectRequestMessage::new( 923 "req-connect", 924 RadrootsNostrConnectRequest::Connect { 925 remote_signer_public_key: runtime.signer_identity().public_key(), 926 secret: None, 927 requested_permissions: Default::default(), 928 }, 929 ), 930 ) 931 .expect("connect response"); 932 933 assert_eq!( 934 response, 935 RadrootsNostrConnectResponse::Error { 936 result: None, 937 error: "client public key denied by policy".to_owned(), 938 } 939 ); 940 assert!( 941 runtime 942 .signer_manager() 943 .expect("manager") 944 .list_connections() 945 .expect("connections") 946 .is_empty() 947 ); 948 } 949 950 #[test] 951 fn existing_unconsumed_connect_secret_can_still_retry_after_failed_publish() { 952 let runtime = runtime(); 953 let handler = handler(&runtime); 954 955 let first = handler 956 .handle_request_response( 957 client_keys().public_key(), 958 RadrootsNostrConnectRequestMessage::new( 959 "req-connect-1", 960 RadrootsNostrConnectRequest::Connect { 961 remote_signer_public_key: runtime.signer_identity().public_key(), 962 secret: Some("s3cr3t".to_owned()), 963 requested_permissions: Default::default(), 964 }, 965 ), 966 ) 967 .expect("first connect response"); 968 let second = handler 969 .handle_request_response( 970 client_keys().public_key(), 971 RadrootsNostrConnectRequestMessage::new( 972 "req-connect-2", 973 RadrootsNostrConnectRequest::Connect { 974 remote_signer_public_key: runtime.signer_identity().public_key(), 975 secret: Some("s3cr3t".to_owned()), 976 requested_permissions: Default::default(), 977 }, 978 ), 979 ) 980 .expect("second connect response"); 981 982 assert_eq!( 983 first, 984 RadrootsNostrConnectResponse::ConnectSecretEcho("s3cr3t".to_owned()) 985 ); 986 assert_eq!(second, first); 987 } 988 989 #[test] 990 fn consumed_connect_secret_is_ignored_on_reuse() { 991 let runtime = runtime(); 992 let handler = handler(&runtime); 993 let response = handler 994 .handle_request_response( 995 client_keys().public_key(), 996 RadrootsNostrConnectRequestMessage::new( 997 "req-connect", 998 RadrootsNostrConnectRequest::Connect { 999 remote_signer_public_key: runtime.signer_identity().public_key(), 1000 secret: Some("s3cr3t".to_owned()), 1001 requested_permissions: Default::default(), 1002 }, 1003 ), 1004 ) 1005 .expect("connect response"); 1006 assert_eq!( 1007 response, 1008 RadrootsNostrConnectResponse::ConnectSecretEcho("s3cr3t".to_owned()) 1009 ); 1010 1011 let connection = runtime 1012 .signer_manager() 1013 .expect("manager") 1014 .list_connections() 1015 .expect("connections") 1016 .into_iter() 1017 .next() 1018 .expect("connection"); 1019 runtime 1020 .signer_manager() 1021 .expect("manager") 1022 .mark_connect_secret_consumed(&connection.connection_id) 1023 .expect("consume connect secret"); 1024 1025 let ignored = handler 1026 .handle_request( 1027 client_keys().public_key(), 1028 RadrootsNostrConnectRequestMessage::new( 1029 "req-connect-reused", 1030 RadrootsNostrConnectRequest::Connect { 1031 remote_signer_public_key: runtime.signer_identity().public_key(), 1032 secret: Some("s3cr3t".to_owned()), 1033 requested_permissions: Default::default(), 1034 }, 1035 ), 1036 ) 1037 .expect("ignored response"); 1038 1039 assert_eq!( 1040 ignored.handled_request, 1041 RadrootsNostrSignerHandledRequest::Ignore 1042 ); 1043 let connections = runtime 1044 .signer_manager() 1045 .expect("manager") 1046 .list_connections() 1047 .expect("connections"); 1048 assert_eq!(connections.len(), 1); 1049 assert!(connections[0].connect_secret_is_consumed()); 1050 } 1051 1052 #[test] 1053 fn connect_requests_are_throttled_after_configured_limit() { 1054 let runtime = runtime_with_config(MycConnectionApproval::NotRequired, |config| { 1055 config.policy.connect_rate_limit_window_secs = Some(1); 1056 config.policy.connect_rate_limit_max_attempts = Some(1); 1057 }); 1058 let handler = handler(&runtime); 1059 1060 let first = handler 1061 .handle_request_response( 1062 client_keys().public_key(), 1063 RadrootsNostrConnectRequestMessage::new( 1064 "req-connect-1", 1065 RadrootsNostrConnectRequest::Connect { 1066 remote_signer_public_key: runtime.signer_identity().public_key(), 1067 secret: None, 1068 requested_permissions: Default::default(), 1069 }, 1070 ), 1071 ) 1072 .expect("first connect response"); 1073 assert_eq!(first, RadrootsNostrConnectResponse::ConnectAcknowledged); 1074 1075 let second = handler 1076 .handle_request_response( 1077 client_keys().public_key(), 1078 RadrootsNostrConnectRequestMessage::new( 1079 "req-connect-2", 1080 RadrootsNostrConnectRequest::Connect { 1081 remote_signer_public_key: runtime.signer_identity().public_key(), 1082 secret: None, 1083 requested_permissions: Default::default(), 1084 }, 1085 ), 1086 ) 1087 .expect("second connect response"); 1088 assert!(matches!( 1089 second, 1090 RadrootsNostrConnectResponse::Error { error, .. } 1091 if error.contains("connect attempts throttled by policy") 1092 )); 1093 1094 let connection = connection_for(&runtime, client_keys().public_key()); 1095 runtime 1096 .signer_manager() 1097 .expect("manager") 1098 .revoke_connection(&connection.connection_id, Some("test reset".to_owned())) 1099 .expect("revoke connection"); 1100 1101 std::thread::sleep(std::time::Duration::from_secs(2)); 1102 1103 let third = handler 1104 .handle_request_response( 1105 client_keys().public_key(), 1106 RadrootsNostrConnectRequestMessage::new( 1107 "req-connect-3", 1108 RadrootsNostrConnectRequest::Connect { 1109 remote_signer_public_key: runtime.signer_identity().public_key(), 1110 secret: None, 1111 requested_permissions: Default::default(), 1112 }, 1113 ), 1114 ) 1115 .expect("third connect response"); 1116 assert_eq!(third, RadrootsNostrConnectResponse::ConnectAcknowledged); 1117 } 1118 1119 #[test] 1120 fn connect_preserves_pending_status_when_explicit_approval_is_required() { 1121 let runtime = runtime_with_explicit_approval(); 1122 let handler = handler(&runtime); 1123 1124 let response = handler 1125 .handle_request_response( 1126 client_keys().public_key(), 1127 RadrootsNostrConnectRequestMessage::new( 1128 "req-connect", 1129 RadrootsNostrConnectRequest::Connect { 1130 remote_signer_public_key: runtime.signer_identity().public_key(), 1131 secret: None, 1132 requested_permissions: vec![sign_event_permission(1)].into(), 1133 }, 1134 ), 1135 ) 1136 .expect("connect response"); 1137 1138 assert_eq!(response, RadrootsNostrConnectResponse::ConnectAcknowledged); 1139 let connection = runtime 1140 .signer_manager() 1141 .expect("manager") 1142 .list_connections() 1143 .expect("connections") 1144 .into_iter() 1145 .next() 1146 .expect("connection"); 1147 assert_eq!( 1148 connection.status, 1149 radroots_nostr_signer::prelude::RadrootsNostrSignerConnectionStatus::Pending 1150 ); 1151 assert_eq!( 1152 connection.approval_state, 1153 radroots_nostr_signer::prelude::RadrootsNostrSignerApprovalState::Pending 1154 ); 1155 assert!(connection.granted_permissions().as_slice().is_empty()); 1156 } 1157 1158 #[test] 1159 fn trusted_clients_auto_grant_only_policy_allowed_permissions() { 1160 let trusted_client_keys = client_keys_from_hex( 1161 "4545454545454545454545454545454545454545454545454545454545454545", 1162 ); 1163 let runtime = runtime_with_config(MycConnectionApproval::ExplicitUser, |config| { 1164 config.policy.trusted_client_pubkeys = vec![trusted_client_keys.public_key().to_hex()]; 1165 config.policy.permission_ceiling = vec![ 1166 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Encrypt), 1167 sign_event_permission(1), 1168 ] 1169 .into(); 1170 config.policy.allowed_sign_event_kinds = vec![1]; 1171 }); 1172 let handler = handler(&runtime); 1173 1174 let response = handler 1175 .handle_request_response( 1176 trusted_client_keys.public_key(), 1177 RadrootsNostrConnectRequestMessage::new( 1178 "req-connect", 1179 RadrootsNostrConnectRequest::Connect { 1180 remote_signer_public_key: runtime.signer_identity().public_key(), 1181 secret: None, 1182 requested_permissions: vec![ 1183 RadrootsNostrConnectPermission::new( 1184 RadrootsNostrConnectMethod::Nip04Encrypt, 1185 ), 1186 RadrootsNostrConnectPermission::new( 1187 RadrootsNostrConnectMethod::SignEvent, 1188 ), 1189 sign_event_permission(7), 1190 ] 1191 .into(), 1192 }, 1193 ), 1194 ) 1195 .expect("connect response"); 1196 1197 assert_eq!(response, RadrootsNostrConnectResponse::ConnectAcknowledged); 1198 let connection = connection_for(&runtime, trusted_client_keys.public_key()); 1199 assert_eq!( 1200 connection.granted_permissions().to_string(), 1201 "sign_event:kind:1,nip04_encrypt" 1202 ); 1203 assert_eq!( 1204 connection.requested_permissions.to_string(), 1205 "sign_event:kind:1,nip04_encrypt" 1206 ); 1207 } 1208 1209 #[test] 1210 fn trusted_client_requires_auth_again_after_authorized_ttl() { 1211 let trusted_client_keys = client_keys_from_hex( 1212 "5656565656565656565656565656565656565656565656565656565656565656", 1213 ); 1214 let runtime = runtime_with_config(MycConnectionApproval::ExplicitUser, |config| { 1215 config.policy.trusted_client_pubkeys = vec![trusted_client_keys.public_key().to_hex()]; 1216 config.policy.permission_ceiling = vec![sign_event_permission(1)].into(); 1217 config.policy.allowed_sign_event_kinds = vec![1]; 1218 config.policy.auth_url = Some("https://auth.example/challenge".to_owned()); 1219 config.policy.auth_authorized_ttl_secs = Some(1); 1220 }); 1221 let handler = handler(&runtime); 1222 1223 let _ = handler 1224 .handle_request_response( 1225 trusted_client_keys.public_key(), 1226 RadrootsNostrConnectRequestMessage::new( 1227 "req-connect", 1228 RadrootsNostrConnectRequest::Connect { 1229 remote_signer_public_key: runtime.signer_identity().public_key(), 1230 secret: None, 1231 requested_permissions: vec![sign_event_permission(1)].into(), 1232 }, 1233 ), 1234 ) 1235 .expect("connect"); 1236 1237 let first = handler 1238 .handle_request_response( 1239 trusted_client_keys.public_key(), 1240 RadrootsNostrConnectRequestMessage::new( 1241 "req-sign-1", 1242 RadrootsNostrConnectRequest::SignEvent(unsigned_event( 1243 runtime.user_identity().public_key(), 1244 1, 1245 "first", 1246 )), 1247 ), 1248 ) 1249 .expect("first sign request"); 1250 assert_eq!( 1251 first, 1252 RadrootsNostrConnectResponse::AuthUrl("https://auth.example/challenge".to_owned()) 1253 ); 1254 1255 let connection = connection_for(&runtime, trusted_client_keys.public_key()); 1256 runtime 1257 .signer_manager() 1258 .expect("manager") 1259 .authorize_auth_challenge(&connection.connection_id) 1260 .expect("authorize auth challenge"); 1261 1262 let second = handler 1263 .handle_request_response( 1264 trusted_client_keys.public_key(), 1265 RadrootsNostrConnectRequestMessage::new( 1266 "req-sign-2", 1267 RadrootsNostrConnectRequest::SignEvent(unsigned_event( 1268 runtime.user_identity().public_key(), 1269 1, 1270 "second", 1271 )), 1272 ), 1273 ) 1274 .expect("second sign request"); 1275 assert!(matches!( 1276 second, 1277 RadrootsNostrConnectResponse::SignedEvent(_) 1278 )); 1279 1280 std::thread::sleep(std::time::Duration::from_secs(2)); 1281 1282 let third = handler 1283 .handle_request_response( 1284 trusted_client_keys.public_key(), 1285 RadrootsNostrConnectRequestMessage::new( 1286 "req-sign-3", 1287 RadrootsNostrConnectRequest::SignEvent(unsigned_event( 1288 runtime.user_identity().public_key(), 1289 1, 1290 "third", 1291 )), 1292 ), 1293 ) 1294 .expect("third sign request"); 1295 assert_eq!( 1296 third, 1297 RadrootsNostrConnectResponse::AuthUrl("https://auth.example/challenge".to_owned()) 1298 ); 1299 } 1300 1301 #[test] 1302 fn trusted_client_requires_auth_again_after_inactivity() { 1303 let trusted_client_keys = client_keys_from_hex( 1304 "5757575757575757575757575757575757575757575757575757575757575757", 1305 ); 1306 let runtime = runtime_with_config(MycConnectionApproval::ExplicitUser, |config| { 1307 config.policy.trusted_client_pubkeys = vec![trusted_client_keys.public_key().to_hex()]; 1308 config.policy.permission_ceiling = vec![sign_event_permission(1)].into(); 1309 config.policy.allowed_sign_event_kinds = vec![1]; 1310 config.policy.auth_url = Some("https://auth.example/challenge".to_owned()); 1311 config.policy.reauth_after_inactivity_secs = Some(1); 1312 }); 1313 let handler = handler(&runtime); 1314 1315 let _ = handler 1316 .handle_request_response( 1317 trusted_client_keys.public_key(), 1318 RadrootsNostrConnectRequestMessage::new( 1319 "req-connect", 1320 RadrootsNostrConnectRequest::Connect { 1321 remote_signer_public_key: runtime.signer_identity().public_key(), 1322 secret: None, 1323 requested_permissions: vec![sign_event_permission(1)].into(), 1324 }, 1325 ), 1326 ) 1327 .expect("connect"); 1328 1329 let first = handler 1330 .handle_request_response( 1331 trusted_client_keys.public_key(), 1332 RadrootsNostrConnectRequestMessage::new( 1333 "req-sign-1", 1334 RadrootsNostrConnectRequest::SignEvent(unsigned_event( 1335 runtime.user_identity().public_key(), 1336 1, 1337 "first", 1338 )), 1339 ), 1340 ) 1341 .expect("first sign request"); 1342 assert_eq!( 1343 first, 1344 RadrootsNostrConnectResponse::AuthUrl("https://auth.example/challenge".to_owned()) 1345 ); 1346 1347 let connection = connection_for(&runtime, trusted_client_keys.public_key()); 1348 runtime 1349 .signer_manager() 1350 .expect("manager") 1351 .authorize_auth_challenge(&connection.connection_id) 1352 .expect("authorize auth challenge"); 1353 1354 std::thread::sleep(std::time::Duration::from_secs(2)); 1355 1356 let second = handler 1357 .handle_request_response( 1358 trusted_client_keys.public_key(), 1359 RadrootsNostrConnectRequestMessage::new( 1360 "req-sign-2", 1361 RadrootsNostrConnectRequest::SignEvent(unsigned_event( 1362 runtime.user_identity().public_key(), 1363 1, 1364 "second", 1365 )), 1366 ), 1367 ) 1368 .expect("second sign request"); 1369 assert_eq!( 1370 second, 1371 RadrootsNostrConnectResponse::AuthUrl("https://auth.example/challenge".to_owned()) 1372 ); 1373 } 1374 1375 #[test] 1376 fn trusted_client_auth_challenge_reissue_is_throttled() { 1377 let trusted_client_keys = client_keys_from_hex( 1378 "5858585858585858585858585858585858585858585858585858585858585858", 1379 ); 1380 let runtime = runtime_with_config(MycConnectionApproval::ExplicitUser, |config| { 1381 config.policy.trusted_client_pubkeys = vec![trusted_client_keys.public_key().to_hex()]; 1382 config.policy.permission_ceiling = vec![sign_event_permission(1)].into(); 1383 config.policy.allowed_sign_event_kinds = vec![1]; 1384 config.policy.auth_url = Some("https://auth.example/challenge".to_owned()); 1385 config.policy.auth_pending_ttl_secs = 1; 1386 config.policy.auth_challenge_rate_limit_window_secs = Some(60); 1387 config.policy.auth_challenge_rate_limit_max_attempts = Some(1); 1388 }); 1389 let handler = handler(&runtime); 1390 1391 let _ = handler 1392 .handle_request_response( 1393 trusted_client_keys.public_key(), 1394 RadrootsNostrConnectRequestMessage::new( 1395 "req-connect", 1396 RadrootsNostrConnectRequest::Connect { 1397 remote_signer_public_key: runtime.signer_identity().public_key(), 1398 secret: None, 1399 requested_permissions: vec![sign_event_permission(1)].into(), 1400 }, 1401 ), 1402 ) 1403 .expect("connect"); 1404 1405 let first = handler 1406 .handle_request_response( 1407 trusted_client_keys.public_key(), 1408 RadrootsNostrConnectRequestMessage::new( 1409 "req-sign-1", 1410 RadrootsNostrConnectRequest::SignEvent(unsigned_event( 1411 runtime.user_identity().public_key(), 1412 1, 1413 "first", 1414 )), 1415 ), 1416 ) 1417 .expect("first sign request"); 1418 assert_eq!( 1419 first, 1420 RadrootsNostrConnectResponse::AuthUrl("https://auth.example/challenge".to_owned()) 1421 ); 1422 1423 std::thread::sleep(std::time::Duration::from_secs(2)); 1424 1425 let second = handler 1426 .handle_request_response( 1427 trusted_client_keys.public_key(), 1428 RadrootsNostrConnectRequestMessage::new( 1429 "req-sign-2", 1430 RadrootsNostrConnectRequest::SignEvent(unsigned_event( 1431 runtime.user_identity().public_key(), 1432 1, 1433 "second", 1434 )), 1435 ), 1436 ) 1437 .expect("second sign request"); 1438 assert!(matches!( 1439 second, 1440 RadrootsNostrConnectResponse::Error { error, .. } 1441 if error.contains("auth challenge issuance throttled by policy") 1442 )); 1443 } 1444 1445 #[test] 1446 fn base_methods_return_spec_results_after_connect() { 1447 let runtime = runtime(); 1448 let handler = handler(&runtime); 1449 handler 1450 .handle_request_response( 1451 client_keys().public_key(), 1452 RadrootsNostrConnectRequestMessage::new( 1453 "req-connect", 1454 RadrootsNostrConnectRequest::Connect { 1455 remote_signer_public_key: runtime.signer_identity().public_key(), 1456 secret: None, 1457 requested_permissions: vec![RadrootsNostrConnectPermission::new( 1458 RadrootsNostrConnectMethod::SwitchRelays, 1459 )] 1460 .into(), 1461 }, 1462 ), 1463 ) 1464 .expect("connect"); 1465 1466 let public_key = handler 1467 .handle_request_response( 1468 client_keys().public_key(), 1469 RadrootsNostrConnectRequestMessage::new( 1470 "req-pubkey", 1471 RadrootsNostrConnectRequest::GetPublicKey, 1472 ), 1473 ) 1474 .expect("get public key"); 1475 assert_eq!( 1476 public_key, 1477 RadrootsNostrConnectResponse::UserPublicKey(runtime.user_identity().public_key()) 1478 ); 1479 1480 let pong = handler 1481 .handle_request_response( 1482 client_keys().public_key(), 1483 RadrootsNostrConnectRequestMessage::new( 1484 "req-ping", 1485 RadrootsNostrConnectRequest::Ping, 1486 ), 1487 ) 1488 .expect("ping"); 1489 assert_eq!(pong, RadrootsNostrConnectResponse::Pong); 1490 1491 let relays = handler 1492 .handle_request_response( 1493 client_keys().public_key(), 1494 RadrootsNostrConnectRequestMessage::new( 1495 "req-switch", 1496 RadrootsNostrConnectRequest::SwitchRelays, 1497 ), 1498 ) 1499 .expect("switch relays"); 1500 assert_eq!( 1501 relays, 1502 RadrootsNostrConnectResponse::RelayList( 1503 runtime.transport().expect("transport").relays().to_vec() 1504 ) 1505 ); 1506 1507 let capability = handler 1508 .handle_request_response( 1509 client_keys().public_key(), 1510 RadrootsNostrConnectRequestMessage::new( 1511 "req-capability", 1512 RadrootsNostrConnectRequest::GetSessionCapability, 1513 ), 1514 ) 1515 .expect("get session capability"); 1516 assert_eq!( 1517 capability, 1518 RadrootsNostrConnectResponse::RemoteSessionCapability( 1519 radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability { 1520 user_public_key: runtime.user_identity().public_key(), 1521 relays: runtime.transport().expect("transport").relays().to_vec(), 1522 permissions: vec![RadrootsNostrConnectPermission::new( 1523 RadrootsNostrConnectMethod::SwitchRelays, 1524 )] 1525 .into(), 1526 }, 1527 ) 1528 ); 1529 } 1530 1531 #[test] 1532 fn new_connections_preserve_requested_permissions_without_expansion() { 1533 let runtime = runtime(); 1534 let handler = handler(&runtime); 1535 handler 1536 .handle_request_response( 1537 client_keys().public_key(), 1538 RadrootsNostrConnectRequestMessage::new( 1539 "req-connect", 1540 RadrootsNostrConnectRequest::Connect { 1541 remote_signer_public_key: runtime.signer_identity().public_key(), 1542 secret: None, 1543 requested_permissions: vec![sign_event_permission(1)].into(), 1544 }, 1545 ), 1546 ) 1547 .expect("connect"); 1548 1549 let connection = runtime 1550 .signer_manager() 1551 .expect("manager") 1552 .list_connections() 1553 .expect("connections") 1554 .into_iter() 1555 .next() 1556 .expect("connection"); 1557 assert_eq!( 1558 connection.granted_permissions().as_slice(), 1559 &[sign_event_permission(1)] 1560 ); 1561 } 1562 1563 #[test] 1564 fn sign_event_returns_signed_event_for_managed_user_key() { 1565 let runtime = runtime(); 1566 let handler = handler(&runtime); 1567 connect_with_permissions(&handler, &runtime, vec![sign_event_permission(1)]); 1568 1569 let response = handler 1570 .handle_request_response( 1571 client_keys().public_key(), 1572 RadrootsNostrConnectRequestMessage::new( 1573 "req-sign", 1574 RadrootsNostrConnectRequest::SignEvent(unsigned_event( 1575 runtime.user_identity().public_key(), 1576 1, 1577 "hello world", 1578 )), 1579 ), 1580 ) 1581 .expect("sign event"); 1582 1583 let RadrootsNostrConnectResponse::SignedEvent(event) = response else { 1584 panic!("unexpected sign_event response"); 1585 }; 1586 assert_eq!(event.pubkey, runtime.user_identity().public_key()); 1587 assert_eq!(event.kind.as_u16(), 1); 1588 assert_eq!(event.content, "hello world"); 1589 assert!(event.verify_signature()); 1590 } 1591 1592 #[test] 1593 fn sign_event_is_denied_without_permission() { 1594 let runtime = runtime(); 1595 let handler = handler(&runtime); 1596 connect_with_permissions(&handler, &runtime, Vec::new()); 1597 1598 let response = handler 1599 .handle_request_response( 1600 client_keys().public_key(), 1601 RadrootsNostrConnectRequestMessage::new( 1602 "req-sign", 1603 RadrootsNostrConnectRequest::SignEvent(unsigned_event( 1604 runtime.user_identity().public_key(), 1605 1, 1606 "hello world", 1607 )), 1608 ), 1609 ) 1610 .expect("sign event"); 1611 1612 assert_eq!( 1613 response, 1614 RadrootsNostrConnectResponse::Error { 1615 result: None, 1616 error: "unauthorized sign_event".to_owned(), 1617 } 1618 ); 1619 } 1620 1621 #[test] 1622 fn sign_event_rejects_pubkey_mismatch() { 1623 let runtime = runtime(); 1624 let handler = handler(&runtime); 1625 connect_with_permissions(&handler, &runtime, vec![sign_event_permission(1)]); 1626 1627 let response = handler 1628 .handle_request_response( 1629 client_keys().public_key(), 1630 RadrootsNostrConnectRequestMessage::new( 1631 "req-sign", 1632 RadrootsNostrConnectRequest::SignEvent(unsigned_event( 1633 client_keys().public_key(), 1634 1, 1635 "hello world", 1636 )), 1637 ), 1638 ) 1639 .expect("sign event"); 1640 1641 assert_eq!( 1642 response, 1643 RadrootsNostrConnectResponse::Error { 1644 result: None, 1645 error: "sign_event pubkey does not match the managed user identity".to_owned(), 1646 } 1647 ); 1648 } 1649 1650 #[test] 1651 fn nip04_encrypt_and_decrypt_roundtrip_on_managed_user_identity() { 1652 let runtime = runtime(); 1653 let handler = handler(&runtime); 1654 connect_with_permissions( 1655 &handler, 1656 &runtime, 1657 vec![ 1658 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Encrypt), 1659 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Decrypt), 1660 ], 1661 ); 1662 1663 let encrypt_response = handler 1664 .handle_request_response( 1665 client_keys().public_key(), 1666 RadrootsNostrConnectRequestMessage::new( 1667 "req-nip04-encrypt", 1668 RadrootsNostrConnectRequest::Nip04Encrypt { 1669 public_key: client_keys().public_key(), 1670 plaintext: "hello from myc".to_owned(), 1671 }, 1672 ), 1673 ) 1674 .expect("nip04 encrypt"); 1675 let RadrootsNostrConnectResponse::Nip04Encrypt(ciphertext) = encrypt_response else { 1676 panic!("unexpected nip04 encrypt response"); 1677 }; 1678 assert_eq!( 1679 nip04::decrypt( 1680 client_keys().secret_key(), 1681 &runtime.user_identity().public_key(), 1682 ciphertext.clone(), 1683 ) 1684 .expect("client decrypt"), 1685 "hello from myc" 1686 ); 1687 1688 let client_ciphertext = nip04::encrypt( 1689 client_keys().secret_key(), 1690 &runtime.user_identity().public_key(), 1691 "hello to myc", 1692 ) 1693 .expect("client encrypt"); 1694 let decrypt_response = handler 1695 .handle_request_response( 1696 client_keys().public_key(), 1697 RadrootsNostrConnectRequestMessage::new( 1698 "req-nip04-decrypt", 1699 RadrootsNostrConnectRequest::Nip04Decrypt { 1700 public_key: client_keys().public_key(), 1701 ciphertext: client_ciphertext, 1702 }, 1703 ), 1704 ) 1705 .expect("nip04 decrypt"); 1706 assert_eq!( 1707 decrypt_response, 1708 RadrootsNostrConnectResponse::Nip04Decrypt("hello to myc".to_owned()) 1709 ); 1710 } 1711 1712 #[test] 1713 fn nip44_encrypt_and_decrypt_roundtrip_on_managed_user_identity() { 1714 let runtime = runtime(); 1715 let handler = handler(&runtime); 1716 connect_with_permissions( 1717 &handler, 1718 &runtime, 1719 vec![ 1720 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Encrypt), 1721 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Decrypt), 1722 ], 1723 ); 1724 1725 let encrypt_response = handler 1726 .handle_request_response( 1727 client_keys().public_key(), 1728 RadrootsNostrConnectRequestMessage::new( 1729 "req-nip44-encrypt", 1730 RadrootsNostrConnectRequest::Nip44Encrypt { 1731 public_key: client_keys().public_key(), 1732 plaintext: "hello from myc".to_owned(), 1733 }, 1734 ), 1735 ) 1736 .expect("nip44 encrypt"); 1737 let RadrootsNostrConnectResponse::Nip44Encrypt(ciphertext) = encrypt_response else { 1738 panic!("unexpected nip44 encrypt response"); 1739 }; 1740 assert_eq!( 1741 nip44::decrypt( 1742 client_keys().secret_key(), 1743 &runtime.user_identity().public_key(), 1744 ciphertext.clone(), 1745 ) 1746 .expect("client decrypt"), 1747 "hello from myc" 1748 ); 1749 1750 let client_ciphertext = nip44::encrypt( 1751 client_keys().secret_key(), 1752 &runtime.user_identity().public_key(), 1753 "hello to myc", 1754 Version::V2, 1755 ) 1756 .expect("client encrypt"); 1757 let decrypt_response = handler 1758 .handle_request_response( 1759 client_keys().public_key(), 1760 RadrootsNostrConnectRequestMessage::new( 1761 "req-nip44-decrypt", 1762 RadrootsNostrConnectRequest::Nip44Decrypt { 1763 public_key: client_keys().public_key(), 1764 ciphertext: client_ciphertext, 1765 }, 1766 ), 1767 ) 1768 .expect("nip44 decrypt"); 1769 assert_eq!( 1770 decrypt_response, 1771 RadrootsNostrConnectResponse::Nip44Decrypt("hello to myc".to_owned()) 1772 ); 1773 } 1774 1775 #[test] 1776 fn nip04_decrypt_is_denied_without_matching_permission() { 1777 let runtime = runtime(); 1778 let handler = handler(&runtime); 1779 connect_with_permissions( 1780 &handler, 1781 &runtime, 1782 vec![RadrootsNostrConnectPermission::new( 1783 RadrootsNostrConnectMethod::Nip04Encrypt, 1784 )], 1785 ); 1786 1787 let response = handler 1788 .handle_request_response( 1789 client_keys().public_key(), 1790 RadrootsNostrConnectRequestMessage::new( 1791 "req-nip04-decrypt", 1792 RadrootsNostrConnectRequest::Nip04Decrypt { 1793 public_key: client_keys().public_key(), 1794 ciphertext: "invalid".to_owned(), 1795 }, 1796 ), 1797 ) 1798 .expect("nip04 decrypt"); 1799 1800 assert_eq!( 1801 response, 1802 RadrootsNostrConnectResponse::Error { 1803 result: None, 1804 error: "unauthorized nip04_decrypt".to_owned(), 1805 } 1806 ); 1807 } 1808 }