nip46.rs (79758B)
1 use nostr::UnsignedEvent; 2 use radroots_identity::RadrootsIdentityPublic; 3 use radroots_nostr::prelude::{ 4 RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrFilter, RadrootsNostrKind, 5 RadrootsNostrPublicKey, RadrootsNostrRelayUrl, RadrootsNostrTag, RadrootsNostrTimestamp, 6 radroots_nostr_filter_tag, 7 }; 8 use radroots_nostr_connect::prelude::{ 9 RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectError, RadrootsNostrConnectPermissions, 10 RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, 11 }; 12 13 use crate::backend::RadrootsNostrSignerBackend; 14 use crate::error::RadrootsNostrSignerError; 15 use crate::evaluation::{ 16 RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerRequestAction, 17 RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerRequestResponseHint, 18 RadrootsNostrSignerSessionLookup, 19 }; 20 use crate::model::{ 21 RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerConnectionId, 22 RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerRequestAuditRecord, 23 RadrootsNostrSignerRequestDecision, 24 }; 25 26 pub trait RadrootsNostrSignerNip46Signer: Clone + Send + Sync { 27 fn signer_public_key_hex(&self) -> String; 28 fn decrypt_request( 29 &self, 30 client_public_key: &RadrootsNostrPublicKey, 31 ciphertext: &str, 32 ) -> Result<String, RadrootsNostrSignerError>; 33 fn encrypt_response( 34 &self, 35 client_public_key: &RadrootsNostrPublicKey, 36 payload: &str, 37 ) -> Result<String, RadrootsNostrSignerError>; 38 fn user_identity(&self) -> RadrootsIdentityPublic; 39 fn sign_user_event( 40 &self, 41 unsigned_event: UnsignedEvent, 42 ) -> Result<RadrootsNostrEvent, RadrootsNostrSignerError>; 43 fn nip04_encrypt( 44 &self, 45 public_key: &RadrootsNostrPublicKey, 46 plaintext: &str, 47 ) -> Result<String, RadrootsNostrSignerError>; 48 fn nip04_decrypt( 49 &self, 50 public_key: &RadrootsNostrPublicKey, 51 ciphertext: &str, 52 ) -> Result<String, RadrootsNostrSignerError>; 53 fn nip44_encrypt( 54 &self, 55 public_key: &RadrootsNostrPublicKey, 56 plaintext: &str, 57 ) -> Result<String, RadrootsNostrSignerError>; 58 fn nip44_decrypt( 59 &self, 60 public_key: &RadrootsNostrPublicKey, 61 ciphertext: &str, 62 ) -> Result<String, RadrootsNostrSignerError>; 63 } 64 65 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 66 pub enum RadrootsNostrSignerNip46ConnectDecision { 67 Allow, 68 RequireApproval, 69 Deny, 70 } 71 72 pub trait RadrootsNostrSignerNip46Policy<B: RadrootsNostrSignerBackend>: 73 Clone + Send + Sync 74 { 75 fn connect_decision( 76 &self, 77 client_public_key: &RadrootsNostrPublicKey, 78 ) -> RadrootsNostrSignerNip46ConnectDecision; 79 80 fn connect_rate_limit_denied_reason( 81 &self, 82 client_public_key: &RadrootsNostrPublicKey, 83 ) -> Option<String>; 84 85 fn approval_requirement_for_client( 86 &self, 87 client_public_key: &RadrootsNostrPublicKey, 88 ) -> Option<RadrootsNostrSignerApprovalRequirement>; 89 90 fn filtered_requested_permissions( 91 &self, 92 requested_permissions: &RadrootsNostrConnectPermissions, 93 ) -> RadrootsNostrConnectPermissions; 94 95 fn auto_granted_permissions( 96 &self, 97 requested_permissions: &RadrootsNostrConnectPermissions, 98 ) -> RadrootsNostrConnectPermissions; 99 100 fn prepare_request( 101 &self, 102 backend: &B, 103 connection: &RadrootsNostrSignerConnectionRecord, 104 request_message: &RadrootsNostrConnectRequestMessage, 105 ) -> Result<Option<String>, RadrootsNostrSignerError>; 106 } 107 108 #[derive(Clone)] 109 pub struct RadrootsNostrSignerNip46Codec<S> { 110 signer: S, 111 } 112 113 #[derive(Clone)] 114 pub struct RadrootsNostrSignerNip46Handler<B, P, S> { 115 backend: B, 116 policy: P, 117 relays: Vec<RadrootsNostrRelayUrl>, 118 codec: RadrootsNostrSignerNip46Codec<S>, 119 } 120 121 #[derive(Debug, Clone, PartialEq, Eq)] 122 pub enum RadrootsNostrSignerHandledRequest { 123 Respond { 124 response: Box<RadrootsNostrConnectResponse>, 125 connection_id: Option<RadrootsNostrSignerConnectionId>, 126 consume_connect_secret_for: Option<RadrootsNostrSignerConnectionId>, 127 }, 128 Ignore, 129 } 130 131 #[derive(Debug, Clone)] 132 pub struct RadrootsNostrSignerHandledRequestOutcome { 133 pub handled_request: RadrootsNostrSignerHandledRequest, 134 pub audit: Option<RadrootsNostrSignerRequestAuditRecord>, 135 } 136 137 enum RadrootsNostrSignerPreparedRequestEvaluation { 138 Denied { 139 reason: String, 140 audit: RadrootsNostrSignerRequestAuditRecord, 141 }, 142 Evaluation(Box<RadrootsNostrSignerRequestEvaluation>), 143 } 144 145 impl<S: RadrootsNostrSignerNip46Signer> RadrootsNostrSignerNip46Codec<S> { 146 pub fn new(signer: S) -> Self { 147 Self { signer } 148 } 149 150 pub fn filter(&self) -> Result<RadrootsNostrFilter, RadrootsNostrSignerError> { 151 let filter = RadrootsNostrFilter::new() 152 .kind(RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND)) 153 .since(RadrootsNostrTimestamp::now()); 154 Ok(radroots_nostr_filter_tag( 155 filter, 156 "p", 157 vec![self.signer.signer_public_key_hex()], 158 )?) 159 } 160 161 pub fn parse_request_event( 162 &self, 163 event: &RadrootsNostrEvent, 164 ) -> Result<RadrootsNostrConnectRequestMessage, RadrootsNostrSignerError> { 165 let decrypted = self.signer.decrypt_request(&event.pubkey, &event.content)?; 166 Ok(serde_json::from_str(&decrypted).map_err(RadrootsNostrConnectError::from)?) 167 } 168 169 pub fn build_response_event( 170 &self, 171 client_public_key: RadrootsNostrPublicKey, 172 request_id: impl Into<String>, 173 response: RadrootsNostrConnectResponse, 174 ) -> Result<RadrootsNostrEventBuilder, RadrootsNostrSignerError> { 175 let envelope = response.into_envelope(request_id.into())?; 176 let payload = serde_json::to_string(&envelope).map_err(RadrootsNostrConnectError::from)?; 177 let ciphertext = self.signer.encrypt_response(&client_public_key, &payload)?; 178 179 Ok(RadrootsNostrEventBuilder::new( 180 RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), 181 ciphertext, 182 ) 183 .tags(vec![RadrootsNostrTag::public_key(client_public_key)])) 184 } 185 186 pub fn sign_event_response( 187 &self, 188 unsigned_event: UnsignedEvent, 189 ) -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError> { 190 let user_public_key = self.signer.user_identity().public_key_hex; 191 if unsigned_event.pubkey.to_hex() != user_public_key { 192 return Ok(RadrootsNostrConnectResponse::Error { 193 result: None, 194 error: "sign_event pubkey does not match the managed user identity".to_owned(), 195 }); 196 } 197 198 match self.signer.sign_user_event(unsigned_event) { 199 Ok(event) => Ok(RadrootsNostrConnectResponse::SignedEvent(event)), 200 Err(error) => Ok(RadrootsNostrConnectResponse::Error { 201 result: None, 202 error: format!("failed to sign event: {error}"), 203 }), 204 } 205 } 206 207 pub fn crypto_response( 208 &self, 209 request: RadrootsNostrConnectRequest, 210 ) -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError> { 211 Ok(match request { 212 RadrootsNostrConnectRequest::Nip04Encrypt { 213 public_key, 214 plaintext, 215 } => match self.signer.nip04_encrypt(&public_key, &plaintext) { 216 Ok(ciphertext) => RadrootsNostrConnectResponse::Nip04Encrypt(ciphertext), 217 Err(error) => RadrootsNostrConnectResponse::Error { 218 result: None, 219 error: format!("nip04 encrypt failed: {error}"), 220 }, 221 }, 222 RadrootsNostrConnectRequest::Nip04Decrypt { 223 public_key, 224 ciphertext, 225 } => match self.signer.nip04_decrypt(&public_key, &ciphertext) { 226 Ok(plaintext) => RadrootsNostrConnectResponse::Nip04Decrypt(plaintext), 227 Err(error) => RadrootsNostrConnectResponse::Error { 228 result: None, 229 error: format!("nip04 decrypt failed: {error}"), 230 }, 231 }, 232 RadrootsNostrConnectRequest::Nip44Encrypt { 233 public_key, 234 plaintext, 235 } => match self.signer.nip44_encrypt(&public_key, &plaintext) { 236 Ok(ciphertext) => RadrootsNostrConnectResponse::Nip44Encrypt(ciphertext), 237 Err(error) => RadrootsNostrConnectResponse::Error { 238 result: None, 239 error: format!("nip44 encrypt failed: {error}"), 240 }, 241 }, 242 RadrootsNostrConnectRequest::Nip44Decrypt { 243 public_key, 244 ciphertext, 245 } => match self.signer.nip44_decrypt(&public_key, &ciphertext) { 246 Ok(plaintext) => RadrootsNostrConnectResponse::Nip44Decrypt(plaintext), 247 Err(error) => RadrootsNostrConnectResponse::Error { 248 result: None, 249 error: format!("nip44 decrypt failed: {error}"), 250 }, 251 }, 252 other => RadrootsNostrConnectResponse::Error { 253 result: None, 254 error: format!("request `{}` is not a crypto method", other.method()), 255 }, 256 }) 257 } 258 } 259 260 impl<B, P, S> RadrootsNostrSignerNip46Handler<B, P, S> 261 where 262 B: RadrootsNostrSignerBackend + Clone, 263 P: RadrootsNostrSignerNip46Policy<B>, 264 S: RadrootsNostrSignerNip46Signer, 265 { 266 pub fn new(backend: B, policy: P, relays: Vec<RadrootsNostrRelayUrl>, signer: S) -> Self { 267 Self { 268 backend, 269 policy, 270 relays, 271 codec: RadrootsNostrSignerNip46Codec::new(signer), 272 } 273 } 274 275 pub fn filter(&self) -> Result<RadrootsNostrFilter, RadrootsNostrSignerError> { 276 self.codec.filter() 277 } 278 279 pub fn parse_request_event( 280 &self, 281 event: &RadrootsNostrEvent, 282 ) -> Result<RadrootsNostrConnectRequestMessage, RadrootsNostrSignerError> { 283 self.codec.parse_request_event(event) 284 } 285 286 pub fn build_response_event( 287 &self, 288 client_public_key: RadrootsNostrPublicKey, 289 request_id: impl Into<String>, 290 response: RadrootsNostrConnectResponse, 291 ) -> Result<RadrootsNostrEventBuilder, RadrootsNostrSignerError> { 292 self.codec 293 .build_response_event(client_public_key, request_id, response) 294 } 295 296 pub fn handle_request( 297 &self, 298 client_public_key: RadrootsNostrPublicKey, 299 request_message: RadrootsNostrConnectRequestMessage, 300 ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> { 301 match request_message.request.clone() { 302 RadrootsNostrConnectRequest::Connect { secret, .. } => { 303 self.handle_connect_request(client_public_key, request_message.request, secret) 304 } 305 RadrootsNostrConnectRequest::SignEvent(unsigned_event) => { 306 self.handle_sign_event_request(client_public_key, request_message, unsigned_event) 307 } 308 RadrootsNostrConnectRequest::Nip04Encrypt { .. } 309 | RadrootsNostrConnectRequest::Nip04Decrypt { .. } 310 | RadrootsNostrConnectRequest::Nip44Encrypt { .. } 311 | RadrootsNostrConnectRequest::Nip44Decrypt { .. } => { 312 self.handle_crypto_request(client_public_key, request_message) 313 } 314 RadrootsNostrConnectRequest::GetPublicKey 315 | RadrootsNostrConnectRequest::GetSessionCapability 316 | RadrootsNostrConnectRequest::Ping 317 | RadrootsNostrConnectRequest::SwitchRelays => { 318 self.handle_base_request(client_public_key, request_message) 319 } 320 _ => Ok(RadrootsNostrSignerHandledRequestOutcome::new( 321 RadrootsNostrSignerHandledRequest::respond(RadrootsNostrConnectResponse::Error { 322 result: None, 323 error: format!( 324 "method `{}` is not implemented yet", 325 request_message.request.method() 326 ), 327 }), 328 None, 329 )), 330 } 331 } 332 333 pub fn handle_authorized_request_evaluation( 334 &self, 335 request_message: RadrootsNostrConnectRequestMessage, 336 evaluation: RadrootsNostrSignerRequestEvaluation, 337 ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> { 338 let audit = evaluation.audit.clone(); 339 let handled_request = self.handled_request_for_evaluation(request_message, evaluation)?; 340 Ok(RadrootsNostrSignerHandledRequestOutcome::new( 341 handled_request, 342 Some(audit), 343 )) 344 } 345 346 fn handle_connect_request( 347 &self, 348 client_public_key: RadrootsNostrPublicKey, 349 request: RadrootsNostrConnectRequest, 350 secret: Option<String>, 351 ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> { 352 let connect_decision = self.policy.connect_decision(&client_public_key); 353 if let Some(connect_secret) = secret.as_deref() 354 && let Some(connection) = self 355 .backend 356 .find_connection_by_connect_secret(connect_secret)? 357 && connection.connect_secret_is_consumed() 358 { 359 return Ok(RadrootsNostrSignerHandledRequestOutcome::ignore()); 360 } 361 if !matches!( 362 connect_decision, 363 RadrootsNostrSignerNip46ConnectDecision::Deny 364 ) && let Some(reason) = self 365 .policy 366 .connect_rate_limit_denied_reason(&client_public_key) 367 { 368 return Ok(RadrootsNostrSignerHandledRequestOutcome::respond( 369 RadrootsNostrConnectResponse::Error { 370 result: None, 371 error: reason, 372 }, 373 )); 374 } 375 376 let evaluation = self 377 .backend 378 .evaluate_connect_request(client_public_key, request)?; 379 380 match evaluation { 381 RadrootsNostrSignerConnectEvaluation::ExistingConnection(connection) => { 382 if matches!( 383 connect_decision, 384 RadrootsNostrSignerNip46ConnectDecision::Deny 385 ) { 386 return Ok(RadrootsNostrSignerHandledRequestOutcome::respond( 387 RadrootsNostrConnectResponse::Error { 388 result: None, 389 error: "client public key denied by policy".to_owned(), 390 }, 391 )); 392 } 393 Ok(RadrootsNostrSignerHandledRequestOutcome::new( 394 connect_response_outcome(&connection, secret), 395 None, 396 )) 397 } 398 RadrootsNostrSignerConnectEvaluation::RegistrationRequired(proposal) => { 399 let requested_permissions = self 400 .policy 401 .filtered_requested_permissions(&proposal.requested_permissions); 402 let Some(approval_requirement) = self 403 .policy 404 .approval_requirement_for_client(&client_public_key) 405 else { 406 return Ok(RadrootsNostrSignerHandledRequestOutcome::respond( 407 RadrootsNostrConnectResponse::Error { 408 result: None, 409 error: "client public key denied by policy".to_owned(), 410 }, 411 )); 412 }; 413 let draft = proposal 414 .into_connection_draft(self.codec.signer.user_identity()) 415 .with_requested_permissions(requested_permissions) 416 .with_relays(self.relays.clone()) 417 .with_approval_requirement(approval_requirement); 418 let connection = self.backend.register_connection(draft)?; 419 if approval_requirement == RadrootsNostrSignerApprovalRequirement::NotRequired { 420 let granted_permissions = self 421 .policy 422 .auto_granted_permissions(&connection.requested_permissions); 423 let _ = self 424 .backend 425 .set_granted_permissions(&connection.connection_id, granted_permissions)?; 426 } 427 Ok(RadrootsNostrSignerHandledRequestOutcome::new( 428 connect_response_outcome(&connection, secret), 429 None, 430 )) 431 } 432 } 433 } 434 435 fn handle_base_request( 436 &self, 437 client_public_key: RadrootsNostrPublicKey, 438 request_message: RadrootsNostrConnectRequestMessage, 439 ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> { 440 let connection = match self.lookup_connection(client_public_key)? { 441 Ok(connection) => connection, 442 Err(response) => { 443 return Ok(RadrootsNostrSignerHandledRequestOutcome::respond(response)); 444 } 445 }; 446 447 match self.evaluate_request_with_policy(&connection, request_message)? { 448 RadrootsNostrSignerPreparedRequestEvaluation::Denied { reason, audit } => { 449 Ok(RadrootsNostrSignerHandledRequestOutcome::new( 450 RadrootsNostrSignerHandledRequest::respond_for_connection( 451 Some(connection.connection_id.clone()), 452 RadrootsNostrConnectResponse::Error { 453 result: None, 454 error: reason, 455 }, 456 ), 457 Some(audit), 458 )) 459 } 460 RadrootsNostrSignerPreparedRequestEvaluation::Evaluation(evaluation) => { 461 let evaluation = *evaluation; 462 let audit = evaluation.audit.clone(); 463 let response_hint = match &evaluation.action { 464 RadrootsNostrSignerRequestAction::Allowed { response_hint, .. } => { 465 Some(response_hint.clone()) 466 } 467 _ => None, 468 }; 469 Ok(RadrootsNostrSignerHandledRequestOutcome::new( 470 handled_request_for_action(&evaluation.connection, evaluation.action, || { 471 Ok(response_from_hint( 472 &evaluation.connection, 473 response_hint.expect("allowed action carries response hint"), 474 )) 475 })?, 476 Some(audit), 477 )) 478 } 479 } 480 } 481 482 fn handle_sign_event_request( 483 &self, 484 client_public_key: RadrootsNostrPublicKey, 485 request_message: RadrootsNostrConnectRequestMessage, 486 unsigned_event: UnsignedEvent, 487 ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> { 488 let connection = match self.lookup_connection(client_public_key)? { 489 Ok(connection) => connection, 490 Err(response) => { 491 return Ok(RadrootsNostrSignerHandledRequestOutcome::respond(response)); 492 } 493 }; 494 495 match self.evaluate_request_with_policy(&connection, request_message)? { 496 RadrootsNostrSignerPreparedRequestEvaluation::Denied { reason, audit } => { 497 Ok(RadrootsNostrSignerHandledRequestOutcome::new( 498 RadrootsNostrSignerHandledRequest::respond_for_connection( 499 Some(connection.connection_id.clone()), 500 RadrootsNostrConnectResponse::Error { 501 result: None, 502 error: reason, 503 }, 504 ), 505 Some(audit), 506 )) 507 } 508 RadrootsNostrSignerPreparedRequestEvaluation::Evaluation(evaluation) => { 509 let evaluation = *evaluation; 510 Ok(RadrootsNostrSignerHandledRequestOutcome::new( 511 self.handled_request_for_authorized_action( 512 &evaluation.connection, 513 evaluation.action, 514 || self.codec.sign_event_response(unsigned_event), 515 )?, 516 Some(evaluation.audit), 517 )) 518 } 519 } 520 } 521 522 fn handle_crypto_request( 523 &self, 524 client_public_key: RadrootsNostrPublicKey, 525 request_message: RadrootsNostrConnectRequestMessage, 526 ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> { 527 let request = request_message.request.clone(); 528 let connection = match self.lookup_connection(client_public_key)? { 529 Ok(connection) => connection, 530 Err(response) => { 531 return Ok(RadrootsNostrSignerHandledRequestOutcome::respond(response)); 532 } 533 }; 534 535 match self.evaluate_request_with_policy(&connection, request_message)? { 536 RadrootsNostrSignerPreparedRequestEvaluation::Denied { reason, audit } => { 537 Ok(RadrootsNostrSignerHandledRequestOutcome::new( 538 RadrootsNostrSignerHandledRequest::respond_for_connection( 539 Some(connection.connection_id.clone()), 540 RadrootsNostrConnectResponse::Error { 541 result: None, 542 error: reason, 543 }, 544 ), 545 Some(audit), 546 )) 547 } 548 RadrootsNostrSignerPreparedRequestEvaluation::Evaluation(evaluation) => { 549 let evaluation = *evaluation; 550 Ok(RadrootsNostrSignerHandledRequestOutcome::new( 551 self.handled_request_for_authorized_action( 552 &evaluation.connection, 553 evaluation.action, 554 || self.codec.crypto_response(request), 555 )?, 556 Some(evaluation.audit), 557 )) 558 } 559 } 560 } 561 562 fn handled_request_for_evaluation( 563 &self, 564 request_message: RadrootsNostrConnectRequestMessage, 565 evaluation: RadrootsNostrSignerRequestEvaluation, 566 ) -> Result<RadrootsNostrSignerHandledRequest, RadrootsNostrSignerError> { 567 match request_message.request.clone() { 568 RadrootsNostrConnectRequest::SignEvent(unsigned_event) => self 569 .handled_request_for_authorized_action( 570 &evaluation.connection, 571 evaluation.action, 572 || self.codec.sign_event_response(unsigned_event), 573 ), 574 RadrootsNostrConnectRequest::Nip04Encrypt { .. } 575 | RadrootsNostrConnectRequest::Nip04Decrypt { .. } 576 | RadrootsNostrConnectRequest::Nip44Encrypt { .. } 577 | RadrootsNostrConnectRequest::Nip44Decrypt { .. } => self 578 .handled_request_for_authorized_action( 579 &evaluation.connection, 580 evaluation.action, 581 || self.codec.crypto_response(request_message.request), 582 ), 583 RadrootsNostrConnectRequest::GetPublicKey 584 | RadrootsNostrConnectRequest::GetSessionCapability 585 | RadrootsNostrConnectRequest::Ping 586 | RadrootsNostrConnectRequest::SwitchRelays => { 587 let response_hint = match &evaluation.action { 588 RadrootsNostrSignerRequestAction::Allowed { response_hint, .. } => { 589 Some(response_hint.clone()) 590 } 591 _ => None, 592 }; 593 self.handled_request_for_authorized_action( 594 &evaluation.connection, 595 evaluation.action, 596 || { 597 Ok(response_from_hint( 598 &evaluation.connection, 599 response_hint.expect("allowed action carries response hint"), 600 )) 601 }, 602 ) 603 } 604 other => Ok(RadrootsNostrSignerHandledRequest::respond_for_connection( 605 Some(evaluation.connection.connection_id.clone()), 606 RadrootsNostrConnectResponse::Error { 607 result: None, 608 error: format!("method `{}` is not implemented yet", other.method()), 609 }, 610 )), 611 } 612 } 613 614 fn handled_request_for_authorized_action<F>( 615 &self, 616 connection: &RadrootsNostrSignerConnectionRecord, 617 action: RadrootsNostrSignerRequestAction, 618 on_allowed: F, 619 ) -> Result<RadrootsNostrSignerHandledRequest, RadrootsNostrSignerError> 620 where 621 F: FnOnce() -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError>, 622 { 623 handled_request_for_action(connection, action, on_allowed) 624 } 625 626 fn evaluate_request_with_policy( 627 &self, 628 connection: &RadrootsNostrSignerConnectionRecord, 629 request_message: RadrootsNostrConnectRequestMessage, 630 ) -> Result<RadrootsNostrSignerPreparedRequestEvaluation, RadrootsNostrSignerError> { 631 if let Some(reason) = 632 self.policy 633 .prepare_request(&self.backend, connection, &request_message)? 634 { 635 let audit = self.backend.record_request( 636 &connection.connection_id, 637 &request_message.id, 638 request_message.request.method(), 639 RadrootsNostrSignerRequestDecision::Denied, 640 Some(reason.clone()), 641 )?; 642 return Ok(RadrootsNostrSignerPreparedRequestEvaluation::Denied { reason, audit }); 643 } 644 645 Ok(RadrootsNostrSignerPreparedRequestEvaluation::Evaluation( 646 Box::new( 647 self.backend 648 .evaluate_request(&connection.connection_id, request_message)?, 649 ), 650 )) 651 } 652 653 fn lookup_connection( 654 &self, 655 client_public_key: RadrootsNostrPublicKey, 656 ) -> Result< 657 Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrConnectResponse>, 658 RadrootsNostrSignerError, 659 > { 660 Ok( 661 match self.backend.lookup_session(&client_public_key, None)? { 662 RadrootsNostrSignerSessionLookup::Connection(connection) => Ok(*connection), 663 RadrootsNostrSignerSessionLookup::None => { 664 Err(RadrootsNostrConnectResponse::Error { 665 result: None, 666 error: "unauthorized".to_owned(), 667 }) 668 } 669 RadrootsNostrSignerSessionLookup::Ambiguous(_) => { 670 Err(RadrootsNostrConnectResponse::Error { 671 result: None, 672 error: "ambiguous client sessions".to_owned(), 673 }) 674 } 675 }, 676 ) 677 } 678 } 679 680 impl RadrootsNostrSignerHandledRequest { 681 pub fn respond(response: RadrootsNostrConnectResponse) -> Self { 682 Self::respond_for_connection(None, response) 683 } 684 685 pub fn respond_for_connection( 686 connection_id: Option<RadrootsNostrSignerConnectionId>, 687 response: RadrootsNostrConnectResponse, 688 ) -> Self { 689 Self::Respond { 690 response: Box::new(response), 691 connection_id, 692 consume_connect_secret_for: None, 693 } 694 } 695 696 pub fn into_publish_parts( 697 self, 698 ) -> Option<( 699 RadrootsNostrConnectResponse, 700 Option<RadrootsNostrSignerConnectionId>, 701 Option<RadrootsNostrSignerConnectionId>, 702 )> { 703 match self { 704 Self::Respond { 705 response, 706 connection_id, 707 consume_connect_secret_for, 708 } => Some((*response, connection_id, consume_connect_secret_for)), 709 Self::Ignore => None, 710 } 711 } 712 } 713 714 impl RadrootsNostrSignerHandledRequestOutcome { 715 pub fn new( 716 handled_request: RadrootsNostrSignerHandledRequest, 717 audit: Option<RadrootsNostrSignerRequestAuditRecord>, 718 ) -> Self { 719 Self { 720 handled_request, 721 audit, 722 } 723 } 724 725 pub fn respond(response: RadrootsNostrConnectResponse) -> Self { 726 Self::new(RadrootsNostrSignerHandledRequest::respond(response), None) 727 } 728 729 pub fn ignore() -> Self { 730 Self::new(RadrootsNostrSignerHandledRequest::Ignore, None) 731 } 732 } 733 734 pub fn connect_response_outcome( 735 connection: &RadrootsNostrSignerConnectionRecord, 736 secret: Option<String>, 737 ) -> RadrootsNostrSignerHandledRequest { 738 let consume_connect_secret_for = secret.as_ref().map(|_| connection.connection_id.clone()); 739 RadrootsNostrSignerHandledRequest::Respond { 740 response: Box::new(match secret { 741 Some(secret) => RadrootsNostrConnectResponse::ConnectSecretEcho(secret), 742 None => RadrootsNostrConnectResponse::ConnectAcknowledged, 743 }), 744 connection_id: Some(connection.connection_id.clone()), 745 consume_connect_secret_for, 746 } 747 } 748 749 pub fn response_from_hint( 750 connection: &RadrootsNostrSignerConnectionRecord, 751 hint: RadrootsNostrSignerRequestResponseHint, 752 ) -> RadrootsNostrConnectResponse { 753 match hint { 754 RadrootsNostrSignerRequestResponseHint::Pong => RadrootsNostrConnectResponse::Pong, 755 RadrootsNostrSignerRequestResponseHint::UserPublicKey(public_key) => { 756 RadrootsNostrConnectResponse::UserPublicKey(public_key) 757 } 758 RadrootsNostrSignerRequestResponseHint::RemoteSessionCapability(capability) => { 759 RadrootsNostrConnectResponse::RemoteSessionCapability(capability) 760 } 761 RadrootsNostrSignerRequestResponseHint::RelayList(relays) => { 762 if relays == connection.relays { 763 RadrootsNostrConnectResponse::RelayList(relays) 764 } else { 765 RadrootsNostrConnectResponse::RelayList(connection.relays.clone()) 766 } 767 } 768 RadrootsNostrSignerRequestResponseHint::None => RadrootsNostrConnectResponse::Error { 769 result: None, 770 error: "request evaluation did not provide a response hint".to_owned(), 771 }, 772 } 773 } 774 775 pub fn handled_request_for_action<F>( 776 connection: &RadrootsNostrSignerConnectionRecord, 777 action: RadrootsNostrSignerRequestAction, 778 on_allowed: F, 779 ) -> Result<RadrootsNostrSignerHandledRequest, RadrootsNostrSignerError> 780 where 781 F: FnOnce() -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError>, 782 { 783 Ok(match action { 784 RadrootsNostrSignerRequestAction::Denied { reason } => { 785 RadrootsNostrSignerHandledRequest::respond_for_connection( 786 Some(connection.connection_id.clone()), 787 RadrootsNostrConnectResponse::Error { 788 result: None, 789 error: reason, 790 }, 791 ) 792 } 793 RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => { 794 RadrootsNostrSignerHandledRequest::respond_for_connection( 795 Some(connection.connection_id.clone()), 796 RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url), 797 ) 798 } 799 RadrootsNostrSignerRequestAction::Allowed { .. } => { 800 RadrootsNostrSignerHandledRequest::respond_for_connection( 801 Some(connection.connection_id.clone()), 802 on_allowed()?, 803 ) 804 } 805 }) 806 } 807 808 #[cfg(test)] 809 #[cfg_attr(coverage_nightly, coverage(off))] 810 mod tests { 811 use super::{ 812 RadrootsNostrSignerHandledRequest, RadrootsNostrSignerHandledRequestOutcome, 813 RadrootsNostrSignerNip46ConnectDecision, RadrootsNostrSignerNip46Handler, 814 RadrootsNostrSignerNip46Policy, RadrootsNostrSignerNip46Signer, 815 }; 816 use crate::backend::{RadrootsNostrEmbeddedSignerBackend, RadrootsNostrSignerBackend}; 817 use crate::error::RadrootsNostrSignerError; 818 use crate::evaluation::{ 819 RadrootsNostrSignerRequestAction, RadrootsNostrSignerRequestResponseHint, 820 }; 821 use crate::model::{ 822 RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerAuthChallenge, 823 RadrootsNostrSignerAuthState, RadrootsNostrSignerConnectionDraft, 824 RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerPendingRequest, 825 }; 826 use crate::test_support::{fixture_alice_identity, fixture_carol_public_key, primary_relay}; 827 use nostr::{Keys, Timestamp, UnsignedEvent}; 828 use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; 829 use radroots_nostr::prelude::{ 830 RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrKind, RadrootsNostrPublicKey, 831 RadrootsNostrTagKind, 832 }; 833 use radroots_nostr_connect::prelude::{ 834 RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectMethod, 835 RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, 836 RadrootsNostrConnectRemoteSessionCapability, RadrootsNostrConnectRequest, 837 RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, 838 }; 839 840 #[derive(Clone)] 841 struct TestSigner { 842 signer_identity: RadrootsIdentity, 843 user_identity: RadrootsIdentity, 844 sign_events: bool, 845 fail_crypto: bool, 846 } 847 848 #[derive(Clone)] 849 struct TestPolicy { 850 connect_decision: RadrootsNostrSignerNip46ConnectDecision, 851 rate_limit_reason: Option<&'static str>, 852 approval_requirement: Option<RadrootsNostrSignerApprovalRequirement>, 853 prepare_denial: Option<&'static str>, 854 } 855 856 impl Default for TestPolicy { 857 fn default() -> Self { 858 Self { 859 connect_decision: RadrootsNostrSignerNip46ConnectDecision::Allow, 860 rate_limit_reason: None, 861 approval_requirement: Some(RadrootsNostrSignerApprovalRequirement::NotRequired), 862 prepare_denial: None, 863 } 864 } 865 } 866 867 impl RadrootsNostrSignerNip46Signer for TestSigner { 868 fn signer_public_key_hex(&self) -> String { 869 self.signer_identity.public_key().to_hex() 870 } 871 872 fn decrypt_request( 873 &self, 874 _client_public_key: &RadrootsNostrPublicKey, 875 ciphertext: &str, 876 ) -> Result<String, RadrootsNostrSignerError> { 877 Ok(ciphertext.to_owned()) 878 } 879 880 fn encrypt_response( 881 &self, 882 _client_public_key: &RadrootsNostrPublicKey, 883 payload: &str, 884 ) -> Result<String, RadrootsNostrSignerError> { 885 Ok(payload.to_owned()) 886 } 887 888 fn user_identity(&self) -> RadrootsIdentityPublic { 889 self.user_identity.to_public() 890 } 891 892 fn sign_user_event( 893 &self, 894 unsigned_event: UnsignedEvent, 895 ) -> Result<RadrootsNostrEvent, RadrootsNostrSignerError> { 896 if self.sign_events { 897 return unsigned_event 898 .sign_with_keys(self.user_identity.keys()) 899 .map_err(|error| RadrootsNostrSignerError::Sign(error.to_string())); 900 } 901 Err(RadrootsNostrSignerError::Sign( 902 "test signer does not sign events".to_owned(), 903 )) 904 } 905 906 fn nip04_encrypt( 907 &self, 908 _public_key: &RadrootsNostrPublicKey, 909 plaintext: &str, 910 ) -> Result<String, RadrootsNostrSignerError> { 911 if self.fail_crypto { 912 return Err(RadrootsNostrSignerError::Sign( 913 "test crypto failure".to_owned(), 914 )); 915 } 916 Ok(plaintext.to_owned()) 917 } 918 919 fn nip04_decrypt( 920 &self, 921 _public_key: &RadrootsNostrPublicKey, 922 ciphertext: &str, 923 ) -> Result<String, RadrootsNostrSignerError> { 924 if self.fail_crypto { 925 return Err(RadrootsNostrSignerError::Sign( 926 "test crypto failure".to_owned(), 927 )); 928 } 929 Ok(ciphertext.to_owned()) 930 } 931 932 fn nip44_encrypt( 933 &self, 934 _public_key: &RadrootsNostrPublicKey, 935 plaintext: &str, 936 ) -> Result<String, RadrootsNostrSignerError> { 937 if self.fail_crypto { 938 return Err(RadrootsNostrSignerError::Sign( 939 "test crypto failure".to_owned(), 940 )); 941 } 942 Ok(plaintext.to_owned()) 943 } 944 945 fn nip44_decrypt( 946 &self, 947 _public_key: &RadrootsNostrPublicKey, 948 ciphertext: &str, 949 ) -> Result<String, RadrootsNostrSignerError> { 950 if self.fail_crypto { 951 return Err(RadrootsNostrSignerError::Sign( 952 "test crypto failure".to_owned(), 953 )); 954 } 955 Ok(ciphertext.to_owned()) 956 } 957 } 958 959 impl<B: RadrootsNostrSignerBackend> RadrootsNostrSignerNip46Policy<B> for TestPolicy { 960 fn connect_decision( 961 &self, 962 _client_public_key: &RadrootsNostrPublicKey, 963 ) -> RadrootsNostrSignerNip46ConnectDecision { 964 self.connect_decision 965 } 966 967 fn connect_rate_limit_denied_reason( 968 &self, 969 _client_public_key: &RadrootsNostrPublicKey, 970 ) -> Option<String> { 971 self.rate_limit_reason.map(ToOwned::to_owned) 972 } 973 974 fn approval_requirement_for_client( 975 &self, 976 _client_public_key: &RadrootsNostrPublicKey, 977 ) -> Option<RadrootsNostrSignerApprovalRequirement> { 978 self.approval_requirement 979 } 980 981 fn filtered_requested_permissions( 982 &self, 983 requested_permissions: &RadrootsNostrConnectPermissions, 984 ) -> RadrootsNostrConnectPermissions { 985 requested_permissions.clone() 986 } 987 988 fn auto_granted_permissions( 989 &self, 990 requested_permissions: &RadrootsNostrConnectPermissions, 991 ) -> RadrootsNostrConnectPermissions { 992 requested_permissions.clone() 993 } 994 995 fn prepare_request( 996 &self, 997 _backend: &B, 998 _connection: &crate::model::RadrootsNostrSignerConnectionRecord, 999 _request_message: &RadrootsNostrConnectRequestMessage, 1000 ) -> Result<Option<String>, RadrootsNostrSignerError> { 1001 Ok(self.prepare_denial.map(ToOwned::to_owned)) 1002 } 1003 } 1004 1005 fn test_signer() -> TestSigner { 1006 test_signer_with_options(false, false) 1007 } 1008 1009 fn test_signer_with_options(sign_events: bool, fail_crypto: bool) -> TestSigner { 1010 TestSigner { 1011 signer_identity: RadrootsIdentity::from_secret_key_str( 1012 "1111111111111111111111111111111111111111111111111111111111111111", 1013 ) 1014 .expect("signer identity"), 1015 user_identity: RadrootsIdentity::from_secret_key_str( 1016 "2222222222222222222222222222222222222222222222222222222222222222", 1017 ) 1018 .expect("user identity"), 1019 sign_events, 1020 fail_crypto, 1021 } 1022 } 1023 1024 fn embedded_backend() -> RadrootsNostrEmbeddedSignerBackend { 1025 RadrootsNostrEmbeddedSignerBackend::new( 1026 crate::manager::RadrootsNostrSignerManager::new_in_memory(), 1027 test_signer().signer_identity.clone(), 1028 ) 1029 .expect("embedded backend") 1030 } 1031 1032 fn handler_with_backend( 1033 backend: RadrootsNostrEmbeddedSignerBackend, 1034 ) -> RadrootsNostrSignerNip46Handler<RadrootsNostrEmbeddedSignerBackend, TestPolicy, TestSigner> 1035 { 1036 handler_with_policy(backend, TestPolicy::default()) 1037 } 1038 1039 fn handler_with_policy( 1040 backend: RadrootsNostrEmbeddedSignerBackend, 1041 policy: TestPolicy, 1042 ) -> RadrootsNostrSignerNip46Handler<RadrootsNostrEmbeddedSignerBackend, TestPolicy, TestSigner> 1043 { 1044 RadrootsNostrSignerNip46Handler::new(backend, policy, vec![primary_relay()], test_signer()) 1045 } 1046 1047 fn connect_request(secret: Option<&str>) -> RadrootsNostrConnectRequestMessage { 1048 connect_request_with_permissions( 1049 secret, 1050 vec![RadrootsNostrConnectPermission::new( 1051 RadrootsNostrConnectMethod::Nip04Encrypt, 1052 )], 1053 ) 1054 } 1055 1056 fn connect_request_with_permissions( 1057 secret: Option<&str>, 1058 permissions: Vec<RadrootsNostrConnectPermission>, 1059 ) -> RadrootsNostrConnectRequestMessage { 1060 let signer_public_key = test_signer().signer_identity.public_key(); 1061 RadrootsNostrConnectRequestMessage::new( 1062 "req-connect", 1063 RadrootsNostrConnectRequest::Connect { 1064 remote_signer_public_key: signer_public_key, 1065 secret: secret.map(ToOwned::to_owned), 1066 requested_permissions: permissions.into(), 1067 }, 1068 ) 1069 } 1070 1071 fn all_runtime_permissions() -> Vec<RadrootsNostrConnectPermission> { 1072 vec![ 1073 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SignEvent), 1074 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Encrypt), 1075 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Decrypt), 1076 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Encrypt), 1077 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Decrypt), 1078 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays), 1079 ] 1080 } 1081 1082 fn request_message( 1083 id: &str, 1084 request: RadrootsNostrConnectRequest, 1085 ) -> RadrootsNostrConnectRequestMessage { 1086 RadrootsNostrConnectRequestMessage::new(id, request) 1087 } 1088 1089 fn unsigned_user_event(kind: u16) -> UnsignedEvent { 1090 serde_json::from_value(serde_json::json!({ 1091 "pubkey": test_signer().user_identity.public_key().to_hex(), 1092 "created_at": Timestamp::from(1).as_secs(), 1093 "kind": kind, 1094 "tags": [], 1095 "content": "hello", 1096 })) 1097 .expect("unsigned event") 1098 } 1099 1100 fn registered_connection( 1101 backend: &RadrootsNostrEmbeddedSignerBackend, 1102 client_public_key: &RadrootsNostrPublicKey, 1103 ) -> RadrootsNostrSignerConnectionRecord { 1104 backend 1105 .find_connections_by_client_public_key(client_public_key) 1106 .expect("connections") 1107 .into_iter() 1108 .next() 1109 .expect("connection") 1110 } 1111 1112 fn connect_with_permissions( 1113 handler: &RadrootsNostrSignerNip46Handler< 1114 RadrootsNostrEmbeddedSignerBackend, 1115 TestPolicy, 1116 TestSigner, 1117 >, 1118 client_public_key: RadrootsNostrPublicKey, 1119 permissions: Vec<RadrootsNostrConnectPermission>, 1120 ) { 1121 let outcome = handler 1122 .handle_request( 1123 client_public_key, 1124 connect_request_with_permissions(None, permissions), 1125 ) 1126 .expect("connect"); 1127 assert!(matches!( 1128 outcome.handled_request, 1129 RadrootsNostrSignerHandledRequest::Respond { .. } 1130 )); 1131 } 1132 1133 fn response_from_outcome( 1134 outcome: RadrootsNostrSignerHandledRequestOutcome, 1135 ) -> RadrootsNostrConnectResponse { 1136 match outcome.handled_request { 1137 RadrootsNostrSignerHandledRequest::Respond { response, .. } => *response, 1138 other => panic!("unexpected handled request: {other:?}"), 1139 } 1140 } 1141 1142 #[test] 1143 fn codec_and_handler_facades_cover_rpc_event_surface() { 1144 let codec = super::RadrootsNostrSignerNip46Codec::new(test_signer()); 1145 let _ = codec.filter().expect("codec filter"); 1146 let client_public_key = fixture_carol_public_key(); 1147 let request = request_message("req-parse", RadrootsNostrConnectRequest::Ping); 1148 let raw = serde_json::to_string(&request).expect("serialize request"); 1149 let event = RadrootsNostrEventBuilder::new( 1150 RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), 1151 raw, 1152 ) 1153 .sign_with_keys(&Keys::generate()) 1154 .expect("sign request event"); 1155 1156 let parsed = codec.parse_request_event(&event).expect("parse request"); 1157 assert_eq!(parsed, request); 1158 1159 let response_builder = codec 1160 .build_response_event( 1161 client_public_key, 1162 "req-parse", 1163 RadrootsNostrConnectResponse::Pong, 1164 ) 1165 .expect("response builder"); 1166 let response_event = response_builder.build(test_signer().signer_identity.public_key()); 1167 assert_eq!( 1168 response_event.kind, 1169 RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) 1170 ); 1171 assert!(response_event.tags.iter().any(|tag| { 1172 tag.kind() == RadrootsNostrTagKind::p() 1173 && tag.content() == Some(client_public_key.to_hex().as_str()) 1174 })); 1175 1176 let handler = handler_with_backend(embedded_backend()); 1177 let _ = handler.filter().expect("handler filter"); 1178 assert_eq!( 1179 handler.parse_request_event(&event).expect("handler parse"), 1180 request 1181 ); 1182 let handler_event = handler 1183 .build_response_event( 1184 client_public_key, 1185 "req-handler", 1186 RadrootsNostrConnectResponse::ConnectAcknowledged, 1187 ) 1188 .expect("handler response") 1189 .build(test_signer().signer_identity.public_key()); 1190 assert_eq!( 1191 handler_event.kind, 1192 RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) 1193 ); 1194 } 1195 1196 #[test] 1197 fn codec_crypto_and_signing_responses_cover_method_matrix() { 1198 let codec = super::RadrootsNostrSignerNip46Codec::new(test_signer()); 1199 let client_public_key = fixture_carol_public_key(); 1200 1201 assert_eq!( 1202 codec 1203 .crypto_response(RadrootsNostrConnectRequest::Nip04Encrypt { 1204 public_key: client_public_key, 1205 plaintext: "plain".to_owned(), 1206 }) 1207 .expect("nip04 encrypt"), 1208 RadrootsNostrConnectResponse::Nip04Encrypt("plain".to_owned()) 1209 ); 1210 assert_eq!( 1211 codec 1212 .crypto_response(RadrootsNostrConnectRequest::Nip04Decrypt { 1213 public_key: client_public_key, 1214 ciphertext: "cipher".to_owned(), 1215 }) 1216 .expect("nip04 decrypt"), 1217 RadrootsNostrConnectResponse::Nip04Decrypt("cipher".to_owned()) 1218 ); 1219 assert_eq!( 1220 codec 1221 .crypto_response(RadrootsNostrConnectRequest::Nip44Encrypt { 1222 public_key: client_public_key, 1223 plaintext: "plain44".to_owned(), 1224 }) 1225 .expect("nip44 encrypt"), 1226 RadrootsNostrConnectResponse::Nip44Encrypt("plain44".to_owned()) 1227 ); 1228 assert_eq!( 1229 codec 1230 .crypto_response(RadrootsNostrConnectRequest::Nip44Decrypt { 1231 public_key: client_public_key, 1232 ciphertext: "cipher44".to_owned(), 1233 }) 1234 .expect("nip44 decrypt"), 1235 RadrootsNostrConnectResponse::Nip44Decrypt("cipher44".to_owned()) 1236 ); 1237 1238 let non_crypto = codec 1239 .crypto_response(RadrootsNostrConnectRequest::Ping) 1240 .expect("non crypto response"); 1241 assert!(matches!( 1242 non_crypto, 1243 RadrootsNostrConnectResponse::Error { .. } 1244 )); 1245 1246 let failing_codec = 1247 super::RadrootsNostrSignerNip46Codec::new(test_signer_with_options(false, true)); 1248 for request in [ 1249 RadrootsNostrConnectRequest::Nip04Encrypt { 1250 public_key: client_public_key, 1251 plaintext: "plain".to_owned(), 1252 }, 1253 RadrootsNostrConnectRequest::Nip04Decrypt { 1254 public_key: client_public_key, 1255 ciphertext: "cipher".to_owned(), 1256 }, 1257 RadrootsNostrConnectRequest::Nip44Encrypt { 1258 public_key: client_public_key, 1259 plaintext: "plain44".to_owned(), 1260 }, 1261 RadrootsNostrConnectRequest::Nip44Decrypt { 1262 public_key: client_public_key, 1263 ciphertext: "cipher44".to_owned(), 1264 }, 1265 ] { 1266 assert!(matches!( 1267 failing_codec 1268 .crypto_response(request) 1269 .expect("failing crypto response"), 1270 RadrootsNostrConnectResponse::Error { .. } 1271 )); 1272 } 1273 1274 let signing = codec 1275 .sign_event_response(unsigned_user_event(1)) 1276 .expect("signing response"); 1277 match signing { 1278 RadrootsNostrConnectResponse::Error { error, .. } => { 1279 assert!(error.contains("failed to sign event")); 1280 } 1281 other => panic!("unexpected sign response: {other:?}"), 1282 } 1283 1284 let signed = 1285 super::RadrootsNostrSignerNip46Codec::new(test_signer_with_options(true, false)) 1286 .sign_event_response(unsigned_user_event(1)) 1287 .expect("signed response"); 1288 assert!(matches!( 1289 signed, 1290 RadrootsNostrConnectResponse::SignedEvent(_) 1291 )); 1292 } 1293 1294 #[test] 1295 fn handler_connect_policy_paths_cover_registration_branches() { 1296 let client_public_key = fixture_carol_public_key(); 1297 1298 let rate_limited = handler_with_policy( 1299 embedded_backend(), 1300 TestPolicy { 1301 rate_limit_reason: Some("slow down"), 1302 ..TestPolicy::default() 1303 }, 1304 ) 1305 .handle_request(client_public_key, connect_request(None)) 1306 .expect("rate limit outcome"); 1307 assert_eq!( 1308 response_from_outcome(rate_limited), 1309 RadrootsNostrConnectResponse::Error { 1310 result: None, 1311 error: "slow down".to_owned(), 1312 } 1313 ); 1314 1315 let denied_registration = handler_with_policy( 1316 embedded_backend(), 1317 TestPolicy { 1318 approval_requirement: None, 1319 ..TestPolicy::default() 1320 }, 1321 ) 1322 .handle_request(client_public_key, connect_request(None)) 1323 .expect("registration denial"); 1324 assert_eq!( 1325 response_from_outcome(denied_registration), 1326 RadrootsNostrConnectResponse::Error { 1327 result: None, 1328 error: "client public key denied by policy".to_owned(), 1329 } 1330 ); 1331 1332 let approval_backend = embedded_backend(); 1333 let approval_handler = handler_with_policy( 1334 approval_backend.clone(), 1335 TestPolicy { 1336 approval_requirement: Some(RadrootsNostrSignerApprovalRequirement::ExplicitUser), 1337 ..TestPolicy::default() 1338 }, 1339 ); 1340 let _ = approval_handler 1341 .handle_request(client_public_key, connect_request(None)) 1342 .expect("approval connect"); 1343 let approval_connection = registered_connection(&approval_backend, &client_public_key); 1344 assert_eq!( 1345 approval_connection.approval_requirement, 1346 RadrootsNostrSignerApprovalRequirement::ExplicitUser 1347 ); 1348 } 1349 1350 #[test] 1351 fn handler_connect_existing_connection_paths_cover_policy_edges() { 1352 let client_public_key = fixture_carol_public_key(); 1353 let secret = "connect-secret"; 1354 let existing_backend = embedded_backend(); 1355 let existing_handler = handler_with_backend(existing_backend.clone()); 1356 1357 let first = existing_handler 1358 .handle_request(client_public_key, connect_request(Some(secret))) 1359 .expect("initial connect"); 1360 assert_eq!( 1361 response_from_outcome(first), 1362 RadrootsNostrConnectResponse::ConnectSecretEcho(secret.to_owned()) 1363 ); 1364 let existing = existing_handler 1365 .handle_request(client_public_key, connect_request(Some(secret))) 1366 .expect("existing connect by secret"); 1367 assert_eq!( 1368 response_from_outcome(existing), 1369 RadrootsNostrConnectResponse::ConnectSecretEcho(secret.to_owned()) 1370 ); 1371 1372 let denied_backend = embedded_backend(); 1373 let denied_handler = handler_with_backend(denied_backend.clone()); 1374 let _ = denied_handler 1375 .handle_request(client_public_key, connect_request(Some(secret))) 1376 .expect("denied seed connect"); 1377 let denying_handler = handler_with_policy( 1378 denied_backend, 1379 TestPolicy { 1380 connect_decision: RadrootsNostrSignerNip46ConnectDecision::Deny, 1381 ..TestPolicy::default() 1382 }, 1383 ); 1384 let denied = denying_handler 1385 .handle_request(client_public_key, connect_request(Some(secret))) 1386 .expect("existing connect denied"); 1387 assert_eq!( 1388 response_from_outcome(denied), 1389 RadrootsNostrConnectResponse::Error { 1390 result: None, 1391 error: "client public key denied by policy".to_owned(), 1392 } 1393 ); 1394 } 1395 1396 #[test] 1397 fn handler_request_paths_cover_base_sign_crypto_denied_and_challenged() { 1398 let backend = embedded_backend(); 1399 let handler = handler_with_backend(backend.clone()); 1400 let client_public_key = fixture_carol_public_key(); 1401 connect_with_permissions(&handler, client_public_key, all_runtime_permissions()); 1402 1403 assert!(matches!( 1404 response_from_outcome( 1405 handler 1406 .handle_request( 1407 client_public_key, 1408 request_message("req-pubkey", RadrootsNostrConnectRequest::GetPublicKey), 1409 ) 1410 .expect("pubkey") 1411 ), 1412 RadrootsNostrConnectResponse::UserPublicKey(_) 1413 )); 1414 assert!(matches!( 1415 response_from_outcome( 1416 handler 1417 .handle_request( 1418 client_public_key, 1419 request_message( 1420 "req-capability", 1421 RadrootsNostrConnectRequest::GetSessionCapability, 1422 ), 1423 ) 1424 .expect("capability") 1425 ), 1426 RadrootsNostrConnectResponse::RemoteSessionCapability(_) 1427 )); 1428 assert_eq!( 1429 response_from_outcome( 1430 handler 1431 .handle_request( 1432 client_public_key, 1433 request_message("req-relays", RadrootsNostrConnectRequest::SwitchRelays), 1434 ) 1435 .expect("relays") 1436 ), 1437 RadrootsNostrConnectResponse::RelayList(vec![primary_relay()]) 1438 ); 1439 assert!(matches!( 1440 response_from_outcome( 1441 handler 1442 .handle_request( 1443 client_public_key, 1444 request_message( 1445 "req-sign", 1446 RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)), 1447 ), 1448 ) 1449 .expect("sign") 1450 ), 1451 RadrootsNostrConnectResponse::Error { .. } 1452 )); 1453 assert_eq!( 1454 response_from_outcome( 1455 handler 1456 .handle_request( 1457 client_public_key, 1458 request_message( 1459 "req-nip04-decrypt", 1460 RadrootsNostrConnectRequest::Nip04Decrypt { 1461 public_key: client_public_key, 1462 ciphertext: "cipher".to_owned(), 1463 }, 1464 ), 1465 ) 1466 .expect("nip04 decrypt") 1467 ), 1468 RadrootsNostrConnectResponse::Nip04Decrypt("cipher".to_owned()) 1469 ); 1470 assert_eq!( 1471 response_from_outcome( 1472 handler 1473 .handle_request( 1474 client_public_key, 1475 request_message( 1476 "req-nip44-encrypt", 1477 RadrootsNostrConnectRequest::Nip44Encrypt { 1478 public_key: client_public_key, 1479 plaintext: "plain".to_owned(), 1480 }, 1481 ), 1482 ) 1483 .expect("nip44 encrypt") 1484 ), 1485 RadrootsNostrConnectResponse::Nip44Encrypt("plain".to_owned()) 1486 ); 1487 1488 let unimplemented = handler 1489 .handle_request( 1490 client_public_key, 1491 request_message( 1492 "req-custom", 1493 RadrootsNostrConnectRequest::Custom { 1494 method: RadrootsNostrConnectMethod::Custom("publish_note".to_owned()), 1495 params: vec![], 1496 }, 1497 ), 1498 ) 1499 .expect("custom"); 1500 assert!(matches!( 1501 response_from_outcome(unimplemented), 1502 RadrootsNostrConnectResponse::Error { .. } 1503 )); 1504 1505 let limited_backend = embedded_backend(); 1506 let limited_handler = handler_with_backend(limited_backend); 1507 connect_with_permissions( 1508 &limited_handler, 1509 client_public_key, 1510 vec![RadrootsNostrConnectPermission::new( 1511 RadrootsNostrConnectMethod::Nip04Encrypt, 1512 )], 1513 ); 1514 let denied_crypto = limited_handler 1515 .handle_request( 1516 client_public_key, 1517 request_message( 1518 "req-denied", 1519 RadrootsNostrConnectRequest::Nip04Decrypt { 1520 public_key: client_public_key, 1521 ciphertext: "cipher".to_owned(), 1522 }, 1523 ), 1524 ) 1525 .expect("denied crypto"); 1526 assert!(matches!( 1527 response_from_outcome(denied_crypto), 1528 RadrootsNostrConnectResponse::Error { .. } 1529 )); 1530 1531 let denied_backend = embedded_backend(); 1532 let open_handler = handler_with_backend(denied_backend.clone()); 1533 connect_with_permissions(&open_handler, client_public_key, all_runtime_permissions()); 1534 let denying_handler = handler_with_policy( 1535 denied_backend, 1536 TestPolicy { 1537 prepare_denial: Some("policy blocked"), 1538 ..TestPolicy::default() 1539 }, 1540 ); 1541 let denied_base = denying_handler 1542 .handle_request( 1543 client_public_key, 1544 request_message("req-policy-denied", RadrootsNostrConnectRequest::Ping), 1545 ) 1546 .expect("policy denied"); 1547 assert!(matches!( 1548 response_from_outcome(denied_base), 1549 RadrootsNostrConnectResponse::Error { .. } 1550 )); 1551 let denied_sign = denying_handler 1552 .handle_request( 1553 client_public_key, 1554 request_message( 1555 "req-policy-denied-sign", 1556 RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)), 1557 ), 1558 ) 1559 .expect("policy denied sign"); 1560 assert!(matches!( 1561 response_from_outcome(denied_sign), 1562 RadrootsNostrConnectResponse::Error { .. } 1563 )); 1564 let denied_crypto = denying_handler 1565 .handle_request( 1566 client_public_key, 1567 request_message( 1568 "req-policy-denied-crypto", 1569 RadrootsNostrConnectRequest::Nip44Encrypt { 1570 public_key: client_public_key, 1571 plaintext: "plain".to_owned(), 1572 }, 1573 ), 1574 ) 1575 .expect("policy denied crypto"); 1576 assert!(matches!( 1577 response_from_outcome(denied_crypto), 1578 RadrootsNostrConnectResponse::Error { .. } 1579 )); 1580 1581 let challenge_backend = embedded_backend(); 1582 let challenge_handler = handler_with_backend(challenge_backend.clone()); 1583 connect_with_permissions( 1584 &challenge_handler, 1585 client_public_key, 1586 all_runtime_permissions(), 1587 ); 1588 let challenged = registered_connection(&challenge_backend, &client_public_key); 1589 challenge_backend 1590 .manager() 1591 .require_auth_challenge(&challenged.connection_id, "https://example.test/auth") 1592 .expect("require challenge"); 1593 let auth_url = challenge_handler 1594 .handle_request( 1595 client_public_key, 1596 request_message("req-challenge", RadrootsNostrConnectRequest::Ping), 1597 ) 1598 .expect("challenge"); 1599 assert_eq!( 1600 response_from_outcome(auth_url), 1601 RadrootsNostrConnectResponse::AuthUrl("https://example.test/auth".to_owned()) 1602 ); 1603 } 1604 1605 #[test] 1606 fn handler_rejects_unauthorized_base_sign_and_crypto_requests() { 1607 let handler = handler_with_backend(embedded_backend()); 1608 let client_public_key = fixture_carol_public_key(); 1609 1610 for request in [ 1611 RadrootsNostrConnectRequest::Ping, 1612 RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)), 1613 RadrootsNostrConnectRequest::Nip04Decrypt { 1614 public_key: client_public_key, 1615 ciphertext: "cipher".to_owned(), 1616 }, 1617 ] { 1618 let outcome = handler 1619 .handle_request( 1620 client_public_key, 1621 request_message("req-unauthorized", request), 1622 ) 1623 .expect("unauthorized request"); 1624 assert_eq!( 1625 response_from_outcome(outcome), 1626 RadrootsNostrConnectResponse::Error { 1627 result: None, 1628 error: "unauthorized".to_owned(), 1629 } 1630 ); 1631 } 1632 } 1633 1634 #[test] 1635 fn handler_allowed_sign_and_crypto_requests_execute_codec_paths() { 1636 let backend = embedded_backend(); 1637 let handler = RadrootsNostrSignerNip46Handler::new( 1638 backend, 1639 TestPolicy::default(), 1640 vec![primary_relay()], 1641 test_signer_with_options(true, false), 1642 ); 1643 let client_public_key = fixture_carol_public_key(); 1644 connect_with_permissions( 1645 &handler, 1646 client_public_key, 1647 vec![ 1648 RadrootsNostrConnectPermission::with_parameter( 1649 RadrootsNostrConnectMethod::SignEvent, 1650 "kind:1", 1651 ), 1652 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Encrypt), 1653 ], 1654 ); 1655 1656 assert!(matches!( 1657 response_from_outcome( 1658 handler 1659 .handle_request( 1660 client_public_key, 1661 request_message( 1662 "req-allowed-sign", 1663 RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)), 1664 ), 1665 ) 1666 .expect("allowed sign") 1667 ), 1668 RadrootsNostrConnectResponse::SignedEvent(_) 1669 )); 1670 assert_eq!( 1671 response_from_outcome( 1672 handler 1673 .handle_request( 1674 client_public_key, 1675 request_message( 1676 "req-allowed-nip04-encrypt", 1677 RadrootsNostrConnectRequest::Nip04Encrypt { 1678 public_key: client_public_key, 1679 plaintext: "plain".to_owned(), 1680 }, 1681 ), 1682 ) 1683 .expect("allowed nip04 encrypt") 1684 ), 1685 RadrootsNostrConnectResponse::Nip04Encrypt("plain".to_owned()) 1686 ); 1687 } 1688 1689 #[test] 1690 fn handler_authorized_evaluation_facade_covers_request_variants() { 1691 let backend = embedded_backend(); 1692 let handler = handler_with_backend(backend.clone()); 1693 let client_public_key = fixture_carol_public_key(); 1694 connect_with_permissions(&handler, client_public_key, all_runtime_permissions()); 1695 let connection = registered_connection(&backend, &client_public_key); 1696 1697 let base = request_message("req-eval-ping", RadrootsNostrConnectRequest::Ping); 1698 let base_eval = backend 1699 .evaluate_request(&connection.connection_id, base.clone()) 1700 .expect("base evaluation"); 1701 assert_eq!( 1702 response_from_outcome( 1703 handler 1704 .handle_authorized_request_evaluation(base, base_eval) 1705 .expect("base authorized") 1706 ), 1707 RadrootsNostrConnectResponse::Pong 1708 ); 1709 let mut denied_base_eval = backend 1710 .evaluate_request( 1711 &connection.connection_id, 1712 request_message("req-eval-denied-ping", RadrootsNostrConnectRequest::Ping), 1713 ) 1714 .expect("denied base evaluation"); 1715 denied_base_eval.action = RadrootsNostrSignerRequestAction::Denied { 1716 reason: "blocked".to_owned(), 1717 }; 1718 assert!(matches!( 1719 response_from_outcome( 1720 handler 1721 .handle_authorized_request_evaluation( 1722 request_message("req-eval-denied-ping", RadrootsNostrConnectRequest::Ping), 1723 denied_base_eval, 1724 ) 1725 .expect("denied base authorized") 1726 ), 1727 RadrootsNostrConnectResponse::Error { .. } 1728 )); 1729 1730 let crypto = request_message( 1731 "req-eval-crypto", 1732 RadrootsNostrConnectRequest::Nip44Decrypt { 1733 public_key: client_public_key, 1734 ciphertext: "sealed".to_owned(), 1735 }, 1736 ); 1737 let crypto_eval = backend 1738 .evaluate_request(&connection.connection_id, crypto.clone()) 1739 .expect("crypto evaluation"); 1740 assert_eq!( 1741 response_from_outcome( 1742 handler 1743 .handle_authorized_request_evaluation(crypto, crypto_eval) 1744 .expect("crypto authorized") 1745 ), 1746 RadrootsNostrConnectResponse::Nip44Decrypt("sealed".to_owned()) 1747 ); 1748 1749 let sign = request_message( 1750 "req-eval-sign", 1751 RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)), 1752 ); 1753 let sign_eval = backend 1754 .evaluate_request(&connection.connection_id, sign.clone()) 1755 .expect("sign evaluation"); 1756 assert!(matches!( 1757 response_from_outcome( 1758 handler 1759 .handle_authorized_request_evaluation(sign, sign_eval) 1760 .expect("sign authorized") 1761 ), 1762 RadrootsNostrConnectResponse::Error { .. } 1763 )); 1764 1765 let custom = request_message( 1766 "req-eval-custom", 1767 RadrootsNostrConnectRequest::Custom { 1768 method: RadrootsNostrConnectMethod::Custom("do_work".to_owned()), 1769 params: vec![], 1770 }, 1771 ); 1772 let custom_eval = backend 1773 .evaluate_request(&connection.connection_id, custom.clone()) 1774 .expect("custom evaluation"); 1775 assert!(matches!( 1776 response_from_outcome( 1777 handler 1778 .handle_authorized_request_evaluation(custom, custom_eval) 1779 .expect("custom authorized") 1780 ), 1781 RadrootsNostrConnectResponse::Error { .. } 1782 )); 1783 } 1784 1785 #[test] 1786 fn standalone_response_helpers_cover_publish_parts_and_hints() { 1787 let backend = embedded_backend(); 1788 let handler = handler_with_backend(backend.clone()); 1789 let client_public_key = fixture_carol_public_key(); 1790 connect_with_permissions(&handler, client_public_key, all_runtime_permissions()); 1791 let connection = registered_connection(&backend, &client_public_key); 1792 1793 let parts = super::connect_response_outcome(&connection, Some("secret".to_owned())) 1794 .into_publish_parts() 1795 .expect("publish parts"); 1796 assert_eq!( 1797 parts.0, 1798 RadrootsNostrConnectResponse::ConnectSecretEcho("secret".to_owned()) 1799 ); 1800 assert_eq!(parts.1, Some(connection.connection_id.clone())); 1801 assert_eq!(parts.2, Some(connection.connection_id.clone())); 1802 assert!( 1803 RadrootsNostrSignerHandledRequest::Ignore 1804 .into_publish_parts() 1805 .is_none() 1806 ); 1807 assert!( 1808 RadrootsNostrSignerHandledRequest::respond(RadrootsNostrConnectResponse::Pong) 1809 .into_publish_parts() 1810 .is_some() 1811 ); 1812 assert_eq!( 1813 response_from_outcome(RadrootsNostrSignerHandledRequestOutcome::respond( 1814 RadrootsNostrConnectResponse::Pong, 1815 )), 1816 RadrootsNostrConnectResponse::Pong 1817 ); 1818 1819 assert_eq!( 1820 super::response_from_hint( 1821 &connection, 1822 RadrootsNostrSignerRequestResponseHint::UserPublicKey(client_public_key), 1823 ), 1824 RadrootsNostrConnectResponse::UserPublicKey(client_public_key) 1825 ); 1826 let capability = RadrootsNostrConnectRemoteSessionCapability { 1827 user_public_key: client_public_key, 1828 relays: vec![primary_relay()], 1829 permissions: all_runtime_permissions().into(), 1830 }; 1831 assert_eq!( 1832 super::response_from_hint( 1833 &connection, 1834 RadrootsNostrSignerRequestResponseHint::RemoteSessionCapability(capability.clone(),), 1835 ), 1836 RadrootsNostrConnectResponse::RemoteSessionCapability(capability) 1837 ); 1838 assert_eq!( 1839 super::response_from_hint( 1840 &connection, 1841 RadrootsNostrSignerRequestResponseHint::RelayList(vec![primary_relay()]), 1842 ), 1843 RadrootsNostrConnectResponse::RelayList(vec![primary_relay()]) 1844 ); 1845 assert_eq!( 1846 super::response_from_hint( 1847 &connection, 1848 RadrootsNostrSignerRequestResponseHint::RelayList(Vec::new()), 1849 ), 1850 RadrootsNostrConnectResponse::RelayList(vec![primary_relay()]) 1851 ); 1852 assert!(matches!( 1853 super::response_from_hint(&connection, RadrootsNostrSignerRequestResponseHint::None), 1854 RadrootsNostrConnectResponse::Error { .. } 1855 )); 1856 1857 let denied = super::handled_request_for_action( 1858 &connection, 1859 RadrootsNostrSignerRequestAction::Denied { 1860 reason: "blocked".to_owned(), 1861 }, 1862 || Ok(RadrootsNostrConnectResponse::Pong), 1863 ) 1864 .expect("denied action"); 1865 assert!(matches!( 1866 denied, 1867 RadrootsNostrSignerHandledRequest::Respond { .. } 1868 )); 1869 1870 let allowed = super::handled_request_for_action( 1871 &connection, 1872 RadrootsNostrSignerRequestAction::Allowed { 1873 required_permission: None, 1874 response_hint: RadrootsNostrSignerRequestResponseHint::Pong, 1875 }, 1876 || Ok(RadrootsNostrConnectResponse::Pong), 1877 ) 1878 .expect("allowed action"); 1879 assert!(matches!( 1880 allowed, 1881 RadrootsNostrSignerHandledRequest::Respond { .. } 1882 )); 1883 1884 let challenged = super::handled_request_for_action( 1885 &connection, 1886 RadrootsNostrSignerRequestAction::Challenged { 1887 auth_challenge: RadrootsNostrSignerAuthChallenge::new( 1888 "https://example.test/auth", 1889 1, 1890 ) 1891 .expect("challenge"), 1892 pending_request: RadrootsNostrSignerPendingRequest::new( 1893 request_message("req-pending", RadrootsNostrConnectRequest::Ping), 1894 1, 1895 ) 1896 .expect("pending"), 1897 }, 1898 || Ok(RadrootsNostrConnectResponse::Pong), 1899 ) 1900 .expect("challenged action"); 1901 assert!(matches!( 1902 challenged, 1903 RadrootsNostrSignerHandledRequest::Respond { .. } 1904 )); 1905 } 1906 1907 #[test] 1908 fn handler_reports_ambiguous_client_sessions() { 1909 let backend = embedded_backend(); 1910 let client_public_key = fixture_carol_public_key(); 1911 backend 1912 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1913 client_public_key, 1914 test_signer().user_identity.to_public(), 1915 )) 1916 .expect("first connection"); 1917 backend 1918 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1919 client_public_key, 1920 RadrootsIdentity::from_secret_key_str( 1921 "3333333333333333333333333333333333333333333333333333333333333333", 1922 ) 1923 .expect("second user identity") 1924 .to_public(), 1925 )) 1926 .expect("second connection"); 1927 1928 let outcome = handler_with_backend(backend) 1929 .handle_request( 1930 client_public_key, 1931 request_message("req-ambiguous", RadrootsNostrConnectRequest::Ping), 1932 ) 1933 .expect("ambiguous request"); 1934 assert_eq!( 1935 response_from_outcome(outcome), 1936 RadrootsNostrConnectResponse::Error { 1937 result: None, 1938 error: "ambiguous client sessions".to_owned(), 1939 } 1940 ); 1941 } 1942 1943 #[test] 1944 fn handler_registers_connections_and_returns_audit_for_authorized_requests() { 1945 let backend = embedded_backend(); 1946 let handler = handler_with_backend(backend.clone()); 1947 let client_public_key = fixture_carol_public_key(); 1948 1949 let connect = handler 1950 .handle_request(client_public_key, connect_request(None)) 1951 .expect("connect outcome"); 1952 assert!(connect.audit.is_none()); 1953 match connect.handled_request { 1954 RadrootsNostrSignerHandledRequest::Respond { response, .. } => { 1955 assert_eq!(*response, RadrootsNostrConnectResponse::ConnectAcknowledged); 1956 } 1957 other => panic!("unexpected connect outcome: {other:?}"), 1958 } 1959 1960 let ping = handler 1961 .handle_request( 1962 client_public_key, 1963 RadrootsNostrConnectRequestMessage::new( 1964 "req-ping", 1965 RadrootsNostrConnectRequest::Ping, 1966 ), 1967 ) 1968 .expect("ping outcome"); 1969 match ping.handled_request { 1970 RadrootsNostrSignerHandledRequest::Respond { response, .. } => { 1971 assert_eq!(*response, RadrootsNostrConnectResponse::Pong); 1972 } 1973 other => panic!("unexpected ping outcome: {other:?}"), 1974 } 1975 let audit = ping.audit.expect("audit"); 1976 assert_eq!(audit.request_id.as_str(), "req-ping"); 1977 assert_eq!( 1978 backend 1979 .find_connections_by_client_public_key(&client_public_key) 1980 .expect("connections") 1981 .len(), 1982 1 1983 ); 1984 } 1985 1986 #[test] 1987 fn handler_ignores_reused_consumed_connect_secrets() { 1988 let backend = embedded_backend(); 1989 let handler = handler_with_backend(backend.clone()); 1990 let client_public_key = fixture_carol_public_key(); 1991 let secret = "connect-secret"; 1992 1993 let first = handler 1994 .handle_request(client_public_key, connect_request(Some(secret))) 1995 .expect("first connect"); 1996 assert!(first.audit.is_none()); 1997 1998 let connection = backend 1999 .find_connections_by_client_public_key(&client_public_key) 2000 .expect("connections") 2001 .into_iter() 2002 .next() 2003 .expect("connection"); 2004 backend 2005 .mark_connect_secret_consumed(&connection.connection_id) 2006 .expect("consume secret"); 2007 2008 let reused = handler_with_backend(backend) 2009 .handle_request(client_public_key, connect_request(Some(secret))) 2010 .expect("reused outcome"); 2011 assert_eq!( 2012 reused.handled_request, 2013 RadrootsNostrSignerHandledRequest::Ignore 2014 ); 2015 } 2016 2017 #[test] 2018 fn sign_event_response_rejects_wrong_user_pubkey() { 2019 let codec = super::RadrootsNostrSignerNip46Codec::new(test_signer()); 2020 let response = codec 2021 .sign_event_response( 2022 serde_json::from_value(serde_json::json!({ 2023 "pubkey": fixture_alice_identity().public_key_hex, 2024 "created_at": 1, 2025 "kind": 1, 2026 "tags": [], 2027 "content": "hello", 2028 })) 2029 .expect("unsigned event"), 2030 ) 2031 .expect("response"); 2032 2033 assert_eq!( 2034 response, 2035 RadrootsNostrConnectResponse::Error { 2036 result: None, 2037 error: "sign_event pubkey does not match the managed user identity".to_owned(), 2038 } 2039 ); 2040 } 2041 2042 #[test] 2043 fn connect_decision_enum_covers_all_states() { 2044 assert_eq!( 2045 [ 2046 RadrootsNostrSignerNip46ConnectDecision::Allow, 2047 RadrootsNostrSignerNip46ConnectDecision::RequireApproval, 2048 RadrootsNostrSignerNip46ConnectDecision::Deny, 2049 ] 2050 .len(), 2051 3 2052 ); 2053 } 2054 2055 #[test] 2056 fn connect_request_keeps_requested_permissions() { 2057 let request = connect_request(None); 2058 assert_eq!( 2059 request.request, 2060 RadrootsNostrConnectRequest::Connect { 2061 remote_signer_public_key: test_signer().signer_identity.public_key(), 2062 secret: None, 2063 requested_permissions: vec![RadrootsNostrConnectPermission::new( 2064 RadrootsNostrConnectMethod::Nip04Encrypt, 2065 )] 2066 .into(), 2067 } 2068 ); 2069 } 2070 2071 #[test] 2072 fn handler_registration_initializes_non_terminal_connection_state() { 2073 let backend = embedded_backend(); 2074 let handler = handler_with_backend(backend.clone()); 2075 let _ = handler 2076 .handle_request(fixture_carol_public_key(), connect_request(None)) 2077 .expect("connect"); 2078 let connection = backend 2079 .find_connections_by_client_public_key(&fixture_carol_public_key()) 2080 .expect("connections") 2081 .into_iter() 2082 .next() 2083 .expect("connection"); 2084 assert!(matches!( 2085 connection.auth_state, 2086 RadrootsNostrSignerAuthState::NotRequired 2087 | RadrootsNostrSignerAuthState::Pending 2088 | RadrootsNostrSignerAuthState::Authorized 2089 )); 2090 assert_eq!( 2091 connection.user_identity.id, 2092 test_signer().user_identity().id 2093 ); 2094 } 2095 }