model.rs (60315B)
1 use crate::error::RadrootsNostrSignerError; 2 use hex::encode as hex_encode; 3 use nostr::{PublicKey, RelayUrl}; 4 use radroots_identity::RadrootsIdentityPublic; 5 use radroots_nostr_connect::prelude::{ 6 RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, 7 RadrootsNostrConnectRequestMessage, 8 }; 9 use serde::{Deserialize, Deserializer, Serialize}; 10 use sha2::{Digest, Sha256}; 11 use std::fmt; 12 use std::str::FromStr; 13 use url::Url; 14 use uuid::Uuid; 15 16 pub const RADROOTS_NOSTR_SIGNER_STORE_VERSION: u32 = 1; 17 18 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 19 pub struct RadrootsNostrSignerConnectionId(String); 20 21 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 22 pub struct RadrootsNostrSignerRequestId(String); 23 24 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 25 pub struct RadrootsNostrSignerWorkflowId(String); 26 27 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 28 pub enum RadrootsNostrSignerApprovalRequirement { 29 NotRequired, 30 ExplicitUser, 31 } 32 33 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 34 pub enum RadrootsNostrSignerApprovalState { 35 NotRequired, 36 Pending, 37 Approved, 38 Rejected, 39 } 40 41 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 42 pub enum RadrootsNostrSignerConnectionStatus { 43 Pending, 44 Active, 45 Rejected, 46 Revoked, 47 } 48 49 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 50 #[serde(rename_all = "snake_case")] 51 pub enum RadrootsNostrSignerPublishWorkflowKind { 52 ConnectSecretFinalization, 53 AuthReplayFinalization, 54 } 55 56 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 57 #[serde(rename_all = "snake_case")] 58 pub enum RadrootsNostrSignerPublishWorkflowState { 59 PendingPublish, 60 PublishedPendingFinalize, 61 } 62 63 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 64 pub enum RadrootsNostrSignerRequestDecision { 65 Allowed, 66 Denied, 67 Challenged, 68 } 69 70 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] 71 pub enum RadrootsNostrSignerAuthState { 72 #[default] 73 NotRequired, 74 Pending, 75 Authorized, 76 } 77 78 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 79 #[serde(rename_all = "snake_case")] 80 pub enum RadrootsNostrSignerSecretDigestAlgorithm { 81 Sha256, 82 } 83 84 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 85 pub struct RadrootsNostrSignerConnectSecretHash { 86 pub algorithm: RadrootsNostrSignerSecretDigestAlgorithm, 87 pub digest_hex: String, 88 } 89 90 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 91 pub struct RadrootsNostrSignerAuthChallenge { 92 pub auth_url: String, 93 pub required_at_unix: u64, 94 #[serde(default, skip_serializing_if = "Option::is_none")] 95 pub authorized_at_unix: Option<u64>, 96 } 97 98 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 99 pub struct RadrootsNostrSignerPendingRequest { 100 pub request_message: RadrootsNostrConnectRequestMessage, 101 pub created_at_unix: u64, 102 } 103 104 #[derive(Debug, Clone)] 105 pub struct RadrootsNostrSignerAuthorizationOutcome { 106 pub connection: RadrootsNostrSignerConnectionRecord, 107 pub pending_request: Option<RadrootsNostrSignerPendingRequest>, 108 } 109 110 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 111 pub struct RadrootsNostrSignerPermissionGrant { 112 #[serde( 113 serialize_with = "serialize_permission", 114 deserialize_with = "deserialize_permission" 115 )] 116 pub permission: RadrootsNostrConnectPermission, 117 pub granted_at_unix: u64, 118 } 119 120 #[derive(Debug, Clone)] 121 pub struct RadrootsNostrSignerConnectionDraft { 122 pub client_public_key: PublicKey, 123 pub user_identity: RadrootsIdentityPublic, 124 pub connect_secret: Option<String>, 125 pub requested_permissions: RadrootsNostrConnectPermissions, 126 pub relays: Vec<RelayUrl>, 127 pub approval_requirement: RadrootsNostrSignerApprovalRequirement, 128 } 129 130 #[derive(Debug, Clone, Serialize, Deserialize)] 131 pub struct RadrootsNostrSignerConnectionRecord { 132 pub connection_id: RadrootsNostrSignerConnectionId, 133 pub client_public_key: PublicKey, 134 pub signer_identity: RadrootsIdentityPublic, 135 pub user_identity: RadrootsIdentityPublic, 136 #[serde( 137 default, 138 alias = "connect_secret", 139 deserialize_with = "deserialize_connect_secret_hash_option", 140 skip_serializing_if = "Option::is_none" 141 )] 142 pub connect_secret_hash: Option<RadrootsNostrSignerConnectSecretHash>, 143 #[serde(default, skip_serializing_if = "Option::is_none")] 144 pub connect_secret_consumed_at_unix: Option<u64>, 145 pub requested_permissions: RadrootsNostrConnectPermissions, 146 #[serde(default)] 147 pub granted_permissions: Vec<RadrootsNostrSignerPermissionGrant>, 148 #[serde(default)] 149 pub relays: Vec<RelayUrl>, 150 pub approval_requirement: RadrootsNostrSignerApprovalRequirement, 151 pub approval_state: RadrootsNostrSignerApprovalState, 152 #[serde(default)] 153 pub auth_state: RadrootsNostrSignerAuthState, 154 #[serde(default, skip_serializing_if = "Option::is_none")] 155 pub auth_challenge: Option<RadrootsNostrSignerAuthChallenge>, 156 #[serde(default, skip_serializing_if = "Option::is_none")] 157 pub pending_request: Option<RadrootsNostrSignerPendingRequest>, 158 pub status: RadrootsNostrSignerConnectionStatus, 159 #[serde(default, skip_serializing_if = "Option::is_none")] 160 pub status_reason: Option<String>, 161 pub created_at_unix: u64, 162 pub updated_at_unix: u64, 163 #[serde(default, skip_serializing_if = "Option::is_none")] 164 pub last_authenticated_at_unix: Option<u64>, 165 #[serde(default, skip_serializing_if = "Option::is_none")] 166 pub last_request_at_unix: Option<u64>, 167 } 168 169 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 170 pub struct RadrootsNostrSignerRequestAuditRecord { 171 pub request_id: RadrootsNostrSignerRequestId, 172 pub connection_id: RadrootsNostrSignerConnectionId, 173 pub method: RadrootsNostrConnectMethod, 174 pub decision: RadrootsNostrSignerRequestDecision, 175 #[serde(default, skip_serializing_if = "Option::is_none")] 176 pub message: Option<String>, 177 pub created_at_unix: u64, 178 } 179 180 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 181 pub struct RadrootsNostrSignerPublishWorkflowRecord { 182 pub workflow_id: RadrootsNostrSignerWorkflowId, 183 pub connection_id: RadrootsNostrSignerConnectionId, 184 pub kind: RadrootsNostrSignerPublishWorkflowKind, 185 pub state: RadrootsNostrSignerPublishWorkflowState, 186 #[serde(default, skip_serializing_if = "Option::is_none")] 187 pub pending_request: Option<RadrootsNostrSignerPendingRequest>, 188 #[serde(default, skip_serializing_if = "Option::is_none")] 189 pub authorized_at_unix: Option<u64>, 190 pub created_at_unix: u64, 191 pub updated_at_unix: u64, 192 } 193 194 #[derive(Debug, Clone, Serialize, Deserialize)] 195 pub struct RadrootsNostrSignerStoreState { 196 pub version: u32, 197 pub signer_identity: Option<RadrootsIdentityPublic>, 198 pub connections: Vec<RadrootsNostrSignerConnectionRecord>, 199 pub audit_records: Vec<RadrootsNostrSignerRequestAuditRecord>, 200 #[serde(default)] 201 pub publish_workflows: Vec<RadrootsNostrSignerPublishWorkflowRecord>, 202 } 203 204 #[derive(Debug, Clone, Deserialize)] 205 #[serde(untagged)] 206 enum RadrootsNostrSignerConnectSecretHashRepr { 207 Hash(RadrootsNostrSignerConnectSecretHash), 208 LegacyPlaintext(String), 209 } 210 211 impl RadrootsNostrSignerConnectionId { 212 pub fn new_v7() -> Self { 213 Self(Uuid::now_v7().to_string()) 214 } 215 216 pub fn parse(value: &str) -> Result<Self, RadrootsNostrSignerError> { 217 let trimmed = value.trim(); 218 if trimmed.is_empty() { 219 return Err(RadrootsNostrSignerError::InvalidConnectionId( 220 value.to_owned(), 221 )); 222 } 223 Ok(Self(trimmed.to_owned())) 224 } 225 226 pub fn as_str(&self) -> &str { 227 self.0.as_str() 228 } 229 230 pub fn into_string(self) -> String { 231 self.0 232 } 233 } 234 235 impl fmt::Display for RadrootsNostrSignerConnectionId { 236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 237 f.write_str(self.as_str()) 238 } 239 } 240 241 impl AsRef<str> for RadrootsNostrSignerConnectionId { 242 fn as_ref(&self) -> &str { 243 self.as_str() 244 } 245 } 246 247 impl FromStr for RadrootsNostrSignerConnectionId { 248 type Err = RadrootsNostrSignerError; 249 250 fn from_str(value: &str) -> Result<Self, Self::Err> { 251 Self::parse(value) 252 } 253 } 254 255 impl RadrootsNostrSignerRequestId { 256 pub fn new_v7() -> Self { 257 Self(Uuid::now_v7().to_string()) 258 } 259 260 pub fn parse(value: &str) -> Result<Self, RadrootsNostrSignerError> { 261 let trimmed = value.trim(); 262 if trimmed.is_empty() { 263 return Err(RadrootsNostrSignerError::InvalidRequestId(value.to_owned())); 264 } 265 Ok(Self(trimmed.to_owned())) 266 } 267 268 pub fn as_str(&self) -> &str { 269 self.0.as_str() 270 } 271 272 pub fn into_string(self) -> String { 273 self.0 274 } 275 } 276 277 impl RadrootsNostrSignerWorkflowId { 278 pub fn new_v7() -> Self { 279 Self(Uuid::now_v7().to_string()) 280 } 281 282 pub fn parse(value: &str) -> Result<Self, RadrootsNostrSignerError> { 283 let trimmed = value.trim(); 284 if trimmed.is_empty() { 285 return Err(RadrootsNostrSignerError::InvalidWorkflowId( 286 value.to_owned(), 287 )); 288 } 289 Ok(Self(trimmed.to_owned())) 290 } 291 292 pub fn as_str(&self) -> &str { 293 self.0.as_str() 294 } 295 296 pub fn into_string(self) -> String { 297 self.0 298 } 299 } 300 301 impl fmt::Display for RadrootsNostrSignerWorkflowId { 302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 303 f.write_str(self.as_str()) 304 } 305 } 306 307 impl AsRef<str> for RadrootsNostrSignerWorkflowId { 308 fn as_ref(&self) -> &str { 309 self.as_str() 310 } 311 } 312 313 impl FromStr for RadrootsNostrSignerWorkflowId { 314 type Err = RadrootsNostrSignerError; 315 316 fn from_str(value: &str) -> Result<Self, Self::Err> { 317 Self::parse(value) 318 } 319 } 320 321 impl fmt::Display for RadrootsNostrSignerRequestId { 322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 323 f.write_str(self.as_str()) 324 } 325 } 326 327 impl AsRef<str> for RadrootsNostrSignerRequestId { 328 fn as_ref(&self) -> &str { 329 self.as_str() 330 } 331 } 332 333 impl FromStr for RadrootsNostrSignerRequestId { 334 type Err = RadrootsNostrSignerError; 335 336 fn from_str(value: &str) -> Result<Self, Self::Err> { 337 Self::parse(value) 338 } 339 } 340 341 impl RadrootsNostrSignerConnectSecretHash { 342 pub fn from_secret(secret: &str) -> Option<Self> { 343 normalize_optional_string(secret).map(|normalized| { 344 let mut hasher = Sha256::new(); 345 hasher.update(normalized.as_bytes()); 346 Self { 347 algorithm: RadrootsNostrSignerSecretDigestAlgorithm::Sha256, 348 digest_hex: hex_encode(hasher.finalize()), 349 } 350 }) 351 } 352 353 pub fn matches_secret(&self, secret: &str) -> bool { 354 Self::from_secret(secret).as_ref() == Some(self) 355 } 356 357 fn normalize(self) -> Result<Self, String> { 358 let digest_hex = self.digest_hex.trim().to_ascii_lowercase(); 359 if digest_hex.len() != 64 || !digest_hex.chars().all(|ch| ch.is_ascii_hexdigit()) { 360 return Err("invalid connect secret digest".into()); 361 } 362 Ok(Self { 363 algorithm: self.algorithm, 364 digest_hex, 365 }) 366 } 367 } 368 369 impl RadrootsNostrSignerAuthChallenge { 370 pub fn new(auth_url: &str, required_at_unix: u64) -> Result<Self, RadrootsNostrSignerError> { 371 let auth_url = normalize_optional_string(auth_url) 372 .ok_or_else(|| RadrootsNostrSignerError::InvalidAuthUrl(auth_url.to_owned()))?; 373 let auth_url: String = Url::parse(&auth_url) 374 .map_err(|_| RadrootsNostrSignerError::InvalidAuthUrl(auth_url.clone()))? 375 .into(); 376 Ok(Self { 377 auth_url, 378 required_at_unix, 379 authorized_at_unix: None, 380 }) 381 } 382 383 pub fn mark_authorized(&mut self, authorized_at_unix: u64) { 384 self.authorized_at_unix = Some(authorized_at_unix); 385 } 386 } 387 388 impl<'de> Deserialize<'de> for RadrootsNostrSignerAuthChallenge { 389 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 390 where 391 D: Deserializer<'de>, 392 { 393 #[derive(Deserialize)] 394 struct RawAuthChallenge { 395 auth_url: String, 396 required_at_unix: u64, 397 #[serde(default)] 398 authorized_at_unix: Option<u64>, 399 } 400 401 let raw = RawAuthChallenge::deserialize(deserializer)?; 402 let mut challenge = 403 Self::new(&raw.auth_url, raw.required_at_unix).map_err(serde::de::Error::custom)?; 404 challenge.authorized_at_unix = raw.authorized_at_unix; 405 Ok(challenge) 406 } 407 } 408 409 impl RadrootsNostrSignerPendingRequest { 410 pub fn new( 411 request_message: RadrootsNostrConnectRequestMessage, 412 created_at_unix: u64, 413 ) -> Result<Self, RadrootsNostrSignerError> { 414 let normalized_id = RadrootsNostrSignerRequestId::parse(&request_message.id)?; 415 Ok(Self { 416 request_message: RadrootsNostrConnectRequestMessage::new( 417 normalized_id.as_str(), 418 request_message.request, 419 ), 420 created_at_unix, 421 }) 422 } 423 424 pub fn request_message(&self) -> RadrootsNostrConnectRequestMessage { 425 self.request_message.clone() 426 } 427 428 pub fn request_id(&self) -> RadrootsNostrSignerRequestId { 429 RadrootsNostrSignerRequestId::parse(&self.request_message.id) 430 .expect("pending request ids are validated on construction") 431 } 432 } 433 434 impl RadrootsNostrSignerAuthorizationOutcome { 435 pub fn new( 436 connection: RadrootsNostrSignerConnectionRecord, 437 pending_request: Option<RadrootsNostrSignerPendingRequest>, 438 ) -> Self { 439 Self { 440 connection, 441 pending_request, 442 } 443 } 444 } 445 446 impl RadrootsNostrSignerPermissionGrant { 447 pub fn new(permission: RadrootsNostrConnectPermission, granted_at_unix: u64) -> Self { 448 Self { 449 permission, 450 granted_at_unix, 451 } 452 } 453 } 454 455 impl RadrootsNostrSignerConnectionDraft { 456 pub fn new(client_public_key: PublicKey, user_identity: RadrootsIdentityPublic) -> Self { 457 Self { 458 client_public_key, 459 user_identity, 460 connect_secret: None, 461 requested_permissions: RadrootsNostrConnectPermissions::default(), 462 relays: Vec::new(), 463 approval_requirement: RadrootsNostrSignerApprovalRequirement::NotRequired, 464 } 465 } 466 467 pub fn with_connect_secret(mut self, connect_secret: impl Into<String>) -> Self { 468 self.connect_secret = Some(connect_secret.into()); 469 self 470 } 471 472 pub fn with_requested_permissions( 473 mut self, 474 requested_permissions: RadrootsNostrConnectPermissions, 475 ) -> Self { 476 self.requested_permissions = requested_permissions; 477 self 478 } 479 480 pub fn with_relays(mut self, relays: Vec<RelayUrl>) -> Self { 481 self.relays = relays; 482 self 483 } 484 485 pub fn with_approval_requirement( 486 mut self, 487 approval_requirement: RadrootsNostrSignerApprovalRequirement, 488 ) -> Self { 489 self.approval_requirement = approval_requirement; 490 self 491 } 492 } 493 494 impl RadrootsNostrSignerConnectionRecord { 495 pub fn new( 496 connection_id: RadrootsNostrSignerConnectionId, 497 signer_identity: RadrootsIdentityPublic, 498 draft: RadrootsNostrSignerConnectionDraft, 499 created_at_unix: u64, 500 ) -> Self { 501 let (approval_state, status) = match draft.approval_requirement { 502 RadrootsNostrSignerApprovalRequirement::NotRequired => ( 503 RadrootsNostrSignerApprovalState::NotRequired, 504 RadrootsNostrSignerConnectionStatus::Active, 505 ), 506 RadrootsNostrSignerApprovalRequirement::ExplicitUser => ( 507 RadrootsNostrSignerApprovalState::Pending, 508 RadrootsNostrSignerConnectionStatus::Pending, 509 ), 510 }; 511 512 Self { 513 connection_id, 514 client_public_key: draft.client_public_key, 515 signer_identity, 516 user_identity: draft.user_identity, 517 connect_secret_hash: draft 518 .connect_secret 519 .as_deref() 520 .and_then(RadrootsNostrSignerConnectSecretHash::from_secret), 521 connect_secret_consumed_at_unix: None, 522 requested_permissions: draft.requested_permissions, 523 granted_permissions: Vec::new(), 524 relays: draft.relays, 525 approval_requirement: draft.approval_requirement, 526 approval_state, 527 auth_state: RadrootsNostrSignerAuthState::NotRequired, 528 auth_challenge: None, 529 pending_request: None, 530 status, 531 status_reason: None, 532 created_at_unix, 533 updated_at_unix: created_at_unix, 534 last_authenticated_at_unix: None, 535 last_request_at_unix: None, 536 } 537 } 538 539 pub fn granted_permissions(&self) -> RadrootsNostrConnectPermissions { 540 self.granted_permissions 541 .iter() 542 .map(|grant| grant.permission.clone()) 543 .collect::<Vec<_>>() 544 .into() 545 } 546 547 pub fn effective_permissions(&self) -> RadrootsNostrConnectPermissions { 548 let granted_permissions = self.granted_permissions(); 549 if !granted_permissions.is_empty() { 550 granted_permissions 551 } else if self.approval_state == RadrootsNostrSignerApprovalState::NotRequired { 552 self.requested_permissions.clone() 553 } else { 554 RadrootsNostrConnectPermissions::default() 555 } 556 } 557 558 pub fn is_terminal(&self) -> bool { 559 matches!( 560 self.status, 561 RadrootsNostrSignerConnectionStatus::Rejected 562 | RadrootsNostrSignerConnectionStatus::Revoked 563 ) 564 } 565 566 pub fn connect_secret_is_consumed(&self) -> bool { 567 self.connect_secret_hash.is_some() && self.connect_secret_consumed_at_unix.is_some() 568 } 569 570 pub fn touch_updated(&mut self, updated_at_unix: u64) { 571 self.updated_at_unix = updated_at_unix; 572 } 573 574 pub fn mark_authenticated(&mut self, authenticated_at_unix: u64) { 575 self.last_authenticated_at_unix = Some(authenticated_at_unix); 576 self.updated_at_unix = authenticated_at_unix; 577 } 578 579 pub fn mark_request(&mut self, request_at_unix: u64) { 580 self.last_request_at_unix = Some(request_at_unix); 581 self.updated_at_unix = request_at_unix; 582 } 583 584 pub fn mark_connect_secret_consumed(&mut self, consumed_at_unix: u64) { 585 if self.connect_secret_hash.is_none() || self.connect_secret_consumed_at_unix.is_some() { 586 return; 587 } 588 self.connect_secret_consumed_at_unix = Some(consumed_at_unix); 589 self.updated_at_unix = consumed_at_unix; 590 } 591 592 pub fn require_auth_challenge(&mut self, auth_challenge: RadrootsNostrSignerAuthChallenge) { 593 self.auth_state = RadrootsNostrSignerAuthState::Pending; 594 self.auth_challenge = Some(auth_challenge.clone()); 595 self.pending_request = None; 596 self.updated_at_unix = auth_challenge.required_at_unix; 597 } 598 599 pub fn set_pending_request(&mut self, pending_request: RadrootsNostrSignerPendingRequest) { 600 self.pending_request = Some(pending_request.clone()); 601 self.updated_at_unix = pending_request.created_at_unix; 602 } 603 604 pub fn authorize_auth_challenge( 605 &mut self, 606 authorized_at_unix: u64, 607 ) -> Option<RadrootsNostrSignerPendingRequest> { 608 self.auth_state = RadrootsNostrSignerAuthState::Authorized; 609 if let Some(auth_challenge) = self.auth_challenge.as_mut() { 610 auth_challenge.mark_authorized(authorized_at_unix); 611 } 612 self.last_authenticated_at_unix = Some(authorized_at_unix); 613 self.updated_at_unix = authorized_at_unix; 614 self.pending_request.take() 615 } 616 617 pub fn restore_pending_auth_challenge( 618 &mut self, 619 pending_request: RadrootsNostrSignerPendingRequest, 620 restored_at_unix: u64, 621 ) { 622 self.auth_state = RadrootsNostrSignerAuthState::Pending; 623 if let Some(auth_challenge) = self.auth_challenge.as_mut() { 624 let previous_authorized_at_unix = auth_challenge.authorized_at_unix.take(); 625 if self.last_authenticated_at_unix == previous_authorized_at_unix { 626 self.last_authenticated_at_unix = None; 627 } 628 } 629 self.pending_request = Some(pending_request); 630 self.updated_at_unix = restored_at_unix; 631 } 632 } 633 634 impl RadrootsNostrSignerRequestAuditRecord { 635 pub fn new( 636 request_id: RadrootsNostrSignerRequestId, 637 connection_id: RadrootsNostrSignerConnectionId, 638 method: RadrootsNostrConnectMethod, 639 decision: RadrootsNostrSignerRequestDecision, 640 message: Option<String>, 641 created_at_unix: u64, 642 ) -> Self { 643 Self { 644 request_id, 645 connection_id, 646 method, 647 decision, 648 message, 649 created_at_unix, 650 } 651 } 652 } 653 654 impl RadrootsNostrSignerPublishWorkflowRecord { 655 pub fn new_connect_secret_finalization( 656 connection_id: RadrootsNostrSignerConnectionId, 657 created_at_unix: u64, 658 ) -> Self { 659 Self { 660 workflow_id: RadrootsNostrSignerWorkflowId::new_v7(), 661 connection_id, 662 kind: RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization, 663 state: RadrootsNostrSignerPublishWorkflowState::PendingPublish, 664 pending_request: None, 665 authorized_at_unix: None, 666 created_at_unix, 667 updated_at_unix: created_at_unix, 668 } 669 } 670 671 pub fn new_auth_replay_finalization( 672 connection_id: RadrootsNostrSignerConnectionId, 673 pending_request: RadrootsNostrSignerPendingRequest, 674 authorized_at_unix: u64, 675 ) -> Self { 676 Self { 677 workflow_id: RadrootsNostrSignerWorkflowId::new_v7(), 678 connection_id, 679 kind: RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization, 680 state: RadrootsNostrSignerPublishWorkflowState::PendingPublish, 681 pending_request: Some(pending_request), 682 authorized_at_unix: Some(authorized_at_unix), 683 created_at_unix: authorized_at_unix, 684 updated_at_unix: authorized_at_unix, 685 } 686 } 687 688 pub fn mark_published(&mut self, updated_at_unix: u64) { 689 self.state = RadrootsNostrSignerPublishWorkflowState::PublishedPendingFinalize; 690 self.updated_at_unix = updated_at_unix; 691 } 692 } 693 694 impl Default for RadrootsNostrSignerStoreState { 695 fn default() -> Self { 696 Self { 697 version: RADROOTS_NOSTR_SIGNER_STORE_VERSION, 698 signer_identity: None, 699 connections: Vec::new(), 700 audit_records: Vec::new(), 701 publish_workflows: Vec::new(), 702 } 703 } 704 } 705 706 fn serialize_permission<S>( 707 permission: &RadrootsNostrConnectPermission, 708 serializer: S, 709 ) -> Result<S::Ok, S::Error> 710 where 711 S: serde::Serializer, 712 { 713 serializer.serialize_str(&permission.to_string()) 714 } 715 716 fn deserialize_permission<'de, D>( 717 deserializer: D, 718 ) -> Result<RadrootsNostrConnectPermission, D::Error> 719 where 720 D: serde::Deserializer<'de>, 721 { 722 let value = String::deserialize(deserializer)?; 723 value.parse().map_err(serde::de::Error::custom) 724 } 725 726 fn deserialize_connect_secret_hash_option<'de, D>( 727 deserializer: D, 728 ) -> Result<Option<RadrootsNostrSignerConnectSecretHash>, D::Error> 729 where 730 D: Deserializer<'de>, 731 { 732 let value = Option::<RadrootsNostrSignerConnectSecretHashRepr>::deserialize(deserializer)?; 733 match value { 734 None => Ok(None), 735 Some(RadrootsNostrSignerConnectSecretHashRepr::Hash(hash)) => { 736 hash.normalize().map(Some).map_err(serde::de::Error::custom) 737 } 738 Some(RadrootsNostrSignerConnectSecretHashRepr::LegacyPlaintext(secret)) => { 739 Ok(RadrootsNostrSignerConnectSecretHash::from_secret(&secret)) 740 } 741 } 742 } 743 744 fn normalize_optional_string(value: &str) -> Option<String> { 745 let trimmed = value.trim(); 746 if trimmed.is_empty() { 747 None 748 } else { 749 Some(trimmed.to_owned()) 750 } 751 } 752 753 #[cfg(test)] 754 mod tests { 755 use super::*; 756 use crate::test_support::{ 757 api_primary_https, fixture_alice_identity, fixture_bob_identity, fixture_carol_public_key, 758 primary_relay, synthetic_public_identity, synthetic_public_key, 759 }; 760 use nostr::PublicKey; 761 use radroots_identity::RadrootsIdentityPublic; 762 use serde_json::json; 763 use std::str::FromStr; 764 use tempfile::tempdir; 765 766 fn public_identity(index: u32) -> RadrootsIdentityPublic { 767 synthetic_public_identity(index) 768 } 769 770 fn public_key(index: u32) -> PublicKey { 771 synthetic_public_key(index) 772 } 773 774 fn request_message(id: &str) -> RadrootsNostrConnectRequestMessage { 775 RadrootsNostrConnectRequestMessage::new( 776 id, 777 radroots_nostr_connect::prelude::RadrootsNostrConnectRequest::Ping, 778 ) 779 } 780 781 #[test] 782 fn connection_and_request_ids_parse_and_display() { 783 let connection_id = RadrootsNostrSignerConnectionId::parse("conn-1").expect("connection"); 784 let request_id = RadrootsNostrSignerRequestId::parse("req-1").expect("request"); 785 let workflow_id = RadrootsNostrSignerWorkflowId::parse("wf-1").expect("workflow"); 786 787 assert_eq!(connection_id.as_str(), "conn-1"); 788 assert_eq!(request_id.as_str(), "req-1"); 789 assert_eq!(workflow_id.as_str(), "wf-1"); 790 assert_eq!(connection_id.as_ref(), "conn-1"); 791 assert_eq!(request_id.as_ref(), "req-1"); 792 assert_eq!(workflow_id.as_ref(), "wf-1"); 793 assert_eq!(connection_id.to_string(), "conn-1"); 794 assert_eq!(request_id.to_string(), "req-1"); 795 assert_eq!(workflow_id.to_string(), "wf-1"); 796 assert_eq!(connection_id.clone().into_string(), "conn-1"); 797 assert_eq!(request_id.clone().into_string(), "req-1"); 798 assert_eq!(workflow_id.clone().into_string(), "wf-1"); 799 800 let parsed_connection = 801 RadrootsNostrSignerConnectionId::from_str("conn-1").expect("from_str connection"); 802 let parsed_request = 803 RadrootsNostrSignerRequestId::from_str("req-1").expect("from_str request"); 804 let parsed_workflow = 805 RadrootsNostrSignerWorkflowId::from_str("wf-1").expect("from_str workflow"); 806 assert_eq!(parsed_connection, connection_id); 807 assert_eq!(parsed_request, request_id); 808 assert_eq!(parsed_workflow, workflow_id); 809 } 810 811 #[test] 812 fn generated_ids_are_non_empty() { 813 let connection_id = RadrootsNostrSignerConnectionId::new_v7(); 814 let request_id = RadrootsNostrSignerRequestId::new_v7(); 815 let workflow_id = RadrootsNostrSignerWorkflowId::new_v7(); 816 817 assert!(!connection_id.as_ref().is_empty()); 818 assert!(!request_id.as_ref().is_empty()); 819 assert!(!workflow_id.as_ref().is_empty()); 820 } 821 822 #[test] 823 fn ids_reject_empty_values() { 824 let connection_err = 825 RadrootsNostrSignerConnectionId::parse(" ").expect_err("empty connection"); 826 let request_err = RadrootsNostrSignerRequestId::parse("").expect_err("empty request"); 827 let workflow_err = RadrootsNostrSignerWorkflowId::parse(" ").expect_err("empty workflow"); 828 829 assert!(connection_err.to_string().contains("invalid connection id")); 830 assert!(request_err.to_string().contains("invalid request id")); 831 assert!(workflow_err.to_string().contains("invalid workflow id")); 832 } 833 834 #[test] 835 fn connection_draft_builders_apply_values() { 836 let permission = RadrootsNostrConnectPermission::with_parameter( 837 RadrootsNostrConnectMethod::SignEvent, 838 "kind:1", 839 ); 840 let relay = primary_relay(); 841 let draft = RadrootsNostrSignerConnectionDraft::new( 842 fixture_carol_public_key(), 843 fixture_bob_identity(), 844 ) 845 .with_connect_secret(" secret ") 846 .with_requested_permissions(vec![permission.clone()].into()) 847 .with_relays(vec![relay.clone()]) 848 .with_approval_requirement(RadrootsNostrSignerApprovalRequirement::ExplicitUser); 849 850 assert_eq!(draft.connect_secret.as_deref(), Some(" secret ")); 851 assert_eq!(draft.requested_permissions.as_slice(), &[permission]); 852 assert_eq!(draft.relays, vec![relay]); 853 assert_eq!( 854 draft.approval_requirement, 855 RadrootsNostrSignerApprovalRequirement::ExplicitUser 856 ); 857 } 858 859 #[test] 860 fn connection_record_defaults_follow_approval_requirement_and_tracking_helpers() { 861 let signer_identity = fixture_alice_identity(); 862 let user_identity = fixture_bob_identity(); 863 let connection_id = RadrootsNostrSignerConnectionId::parse("conn-1").expect("id"); 864 let draft = 865 RadrootsNostrSignerConnectionDraft::new(fixture_carol_public_key(), user_identity) 866 .with_connect_secret(" secret ") 867 .with_approval_requirement(RadrootsNostrSignerApprovalRequirement::ExplicitUser); 868 let mut record = 869 RadrootsNostrSignerConnectionRecord::new(connection_id, signer_identity, draft, 10); 870 871 assert_eq!(record.status, RadrootsNostrSignerConnectionStatus::Pending); 872 assert_eq!( 873 record.approval_state, 874 RadrootsNostrSignerApprovalState::Pending 875 ); 876 assert_eq!(record.auth_state, RadrootsNostrSignerAuthState::NotRequired); 877 assert!( 878 record 879 .connect_secret_hash 880 .as_ref() 881 .expect("connect secret hash") 882 .matches_secret("secret") 883 ); 884 assert!(!record.connect_secret_is_consumed()); 885 assert!(!record.is_terminal()); 886 887 record.touch_updated(12); 888 record.mark_authenticated(14); 889 record.mark_request(16); 890 record.mark_connect_secret_consumed(17); 891 record.require_auth_challenge( 892 RadrootsNostrSignerAuthChallenge::new( 893 format!("{}/path", api_primary_https()).as_str(), 894 18, 895 ) 896 .expect("auth challenge"), 897 ); 898 record.set_pending_request( 899 RadrootsNostrSignerPendingRequest::new(request_message("req-1"), 20) 900 .expect("pending request"), 901 ); 902 let replay = record.authorize_auth_challenge(22).expect("replay"); 903 let no_challenge_replay = RadrootsNostrSignerConnectionRecord::new( 904 RadrootsNostrSignerConnectionId::parse("conn-1b").expect("id"), 905 public_identity(0x9), 906 RadrootsNostrSignerConnectionDraft::new(public_key(0x10), public_identity(0x11)), 907 24, 908 ) 909 .authorize_auth_challenge(25); 910 911 assert_eq!(record.updated_at_unix, 22); 912 assert_eq!(record.connect_secret_consumed_at_unix, Some(17)); 913 assert!(record.connect_secret_is_consumed()); 914 assert_eq!(record.auth_state, RadrootsNostrSignerAuthState::Authorized); 915 assert_eq!( 916 record 917 .auth_challenge 918 .as_ref() 919 .expect("auth challenge") 920 .authorized_at_unix, 921 Some(22) 922 ); 923 assert!(record.pending_request.is_none()); 924 assert_eq!(record.last_authenticated_at_unix, Some(22)); 925 assert_eq!(record.last_request_at_unix, Some(16)); 926 assert_eq!(replay.request_id().as_str(), "req-1"); 927 assert!(no_challenge_replay.is_none()); 928 929 record.restore_pending_auth_challenge(replay, 23); 930 931 assert_eq!(record.auth_state, RadrootsNostrSignerAuthState::Pending); 932 assert_eq!( 933 record 934 .auth_challenge 935 .as_ref() 936 .expect("restored challenge") 937 .authorized_at_unix, 938 None 939 ); 940 assert_eq!(record.last_authenticated_at_unix, None); 941 assert_eq!(record.updated_at_unix, 23); 942 assert_eq!( 943 record 944 .pending_request 945 .as_ref() 946 .expect("restored pending request") 947 .request_id() 948 .as_str(), 949 "req-1" 950 ); 951 } 952 953 #[test] 954 fn connection_record_noop_consumption_and_restore_paths_preserve_state() { 955 let mut no_secret_record = RadrootsNostrSignerConnectionRecord::new( 956 RadrootsNostrSignerConnectionId::parse("conn-no-secret").expect("id"), 957 public_identity(0x12), 958 RadrootsNostrSignerConnectionDraft::new(public_key(0x13), public_identity(0x14)), 959 30, 960 ); 961 let no_secret_updated_at = no_secret_record.updated_at_unix; 962 assert!(!no_secret_record.connect_secret_is_consumed()); 963 964 no_secret_record.mark_connect_secret_consumed(31); 965 966 assert_eq!(no_secret_record.connect_secret_consumed_at_unix, None); 967 assert_eq!(no_secret_record.updated_at_unix, no_secret_updated_at); 968 assert!(!no_secret_record.connect_secret_is_consumed()); 969 970 let restored_without_challenge = 971 RadrootsNostrSignerPendingRequest::new(request_message("req-no-challenge"), 32) 972 .expect("pending request"); 973 no_secret_record.last_authenticated_at_unix = Some(29); 974 no_secret_record.restore_pending_auth_challenge(restored_without_challenge.clone(), 33); 975 976 assert_eq!(no_secret_record.last_authenticated_at_unix, Some(29)); 977 assert_eq!( 978 no_secret_record.pending_request.as_ref(), 979 Some(&restored_without_challenge) 980 ); 981 assert_eq!(no_secret_record.updated_at_unix, 33); 982 983 let mut restored_record = RadrootsNostrSignerConnectionRecord::new( 984 RadrootsNostrSignerConnectionId::parse("conn-restore-preserve").expect("id"), 985 public_identity(0x15), 986 RadrootsNostrSignerConnectionDraft::new(public_key(0x16), public_identity(0x17)), 987 40, 988 ); 989 restored_record.require_auth_challenge( 990 RadrootsNostrSignerAuthChallenge::new( 991 format!("{}/preserve", api_primary_https()).as_str(), 992 41, 993 ) 994 .expect("auth challenge"), 995 ); 996 restored_record.set_pending_request( 997 RadrootsNostrSignerPendingRequest::new(request_message("req-preserve"), 42) 998 .expect("pending request"), 999 ); 1000 let replay = restored_record 1001 .authorize_auth_challenge(43) 1002 .expect("authorize challenge"); 1003 restored_record.last_authenticated_at_unix = Some(99); 1004 1005 restored_record.restore_pending_auth_challenge(replay.clone(), 44); 1006 1007 assert_eq!( 1008 restored_record.auth_state, 1009 RadrootsNostrSignerAuthState::Pending 1010 ); 1011 assert_eq!(restored_record.last_authenticated_at_unix, Some(99)); 1012 assert_eq!( 1013 restored_record 1014 .auth_challenge 1015 .as_ref() 1016 .expect("restored challenge") 1017 .authorized_at_unix, 1018 None 1019 ); 1020 assert_eq!(restored_record.pending_request.as_ref(), Some(&replay)); 1021 assert_eq!(restored_record.updated_at_unix, 44); 1022 } 1023 1024 #[test] 1025 fn granted_permissions_and_request_audit_build_correctly() { 1026 let permission = RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping); 1027 let grant = RadrootsNostrSignerPermissionGrant::new(permission.clone(), 42); 1028 let mut record = RadrootsNostrSignerConnectionRecord::new( 1029 RadrootsNostrSignerConnectionId::parse("conn-2").expect("id"), 1030 public_identity(0x6), 1031 RadrootsNostrSignerConnectionDraft::new(public_key(0x7), public_identity(0x8)), 1032 20, 1033 ); 1034 record.granted_permissions = vec![grant]; 1035 let audit = RadrootsNostrSignerRequestAuditRecord::new( 1036 RadrootsNostrSignerRequestId::parse("req-2").expect("request"), 1037 RadrootsNostrSignerConnectionId::parse("conn-2").expect("id"), 1038 RadrootsNostrConnectMethod::Ping, 1039 RadrootsNostrSignerRequestDecision::Allowed, 1040 Some("ok".into()), 1041 25, 1042 ); 1043 1044 assert_eq!(record.granted_permissions().as_slice(), &[permission]); 1045 assert_eq!(audit.message.as_deref(), Some("ok")); 1046 assert_eq!(audit.created_at_unix, 25); 1047 1048 let json = serde_json::to_string(&record.granted_permissions[0]).expect("serialize grant"); 1049 let decoded: RadrootsNostrSignerPermissionGrant = 1050 serde_json::from_str(&json).expect("deserialize grant"); 1051 assert_eq!( 1052 decoded.permission, 1053 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping) 1054 ); 1055 } 1056 1057 #[test] 1058 fn publish_workflow_records_cover_connect_secret_and_auth_replay_lifecycle() { 1059 let connection_id = RadrootsNostrSignerConnectionId::parse("conn-workflow").expect("id"); 1060 let pending_request = 1061 RadrootsNostrSignerPendingRequest::new(request_message("req-workflow"), 41) 1062 .expect("pending request"); 1063 1064 let connect_secret = 1065 RadrootsNostrSignerPublishWorkflowRecord::new_connect_secret_finalization( 1066 connection_id.clone(), 1067 40, 1068 ); 1069 assert_eq!( 1070 connect_secret.kind, 1071 RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization 1072 ); 1073 assert_eq!( 1074 connect_secret.state, 1075 RadrootsNostrSignerPublishWorkflowState::PendingPublish 1076 ); 1077 assert!(connect_secret.pending_request.is_none()); 1078 assert!(connect_secret.authorized_at_unix.is_none()); 1079 1080 let mut auth_replay = 1081 RadrootsNostrSignerPublishWorkflowRecord::new_auth_replay_finalization( 1082 connection_id, 1083 pending_request.clone(), 1084 42, 1085 ); 1086 assert_eq!( 1087 auth_replay.kind, 1088 RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization 1089 ); 1090 assert_eq!( 1091 auth_replay.state, 1092 RadrootsNostrSignerPublishWorkflowState::PendingPublish 1093 ); 1094 assert_eq!(auth_replay.pending_request, Some(pending_request)); 1095 assert_eq!(auth_replay.authorized_at_unix, Some(42)); 1096 1097 auth_replay.mark_published(43); 1098 assert_eq!( 1099 auth_replay.state, 1100 RadrootsNostrSignerPublishWorkflowState::PublishedPendingFinalize 1101 ); 1102 assert_eq!(auth_replay.updated_at_unix, 43); 1103 } 1104 1105 #[test] 1106 fn effective_permissions_prefers_grants_then_auto_requested_then_empty() { 1107 let requested: RadrootsNostrConnectPermissions = vec![RadrootsNostrConnectPermission::new( 1108 RadrootsNostrConnectMethod::Nip04Encrypt, 1109 )] 1110 .into(); 1111 let auto_record = RadrootsNostrSignerConnectionRecord::new( 1112 RadrootsNostrSignerConnectionId::new_v7(), 1113 public_identity(0x31), 1114 RadrootsNostrSignerConnectionDraft::new(public_key(0x32), public_identity(0x33)) 1115 .with_requested_permissions(requested.clone()), 1116 1, 1117 ); 1118 assert_eq!(auto_record.effective_permissions(), requested); 1119 1120 let mut granted_record = auto_record.clone(); 1121 granted_record.granted_permissions = vec![RadrootsNostrSignerPermissionGrant::new( 1122 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping), 1123 2, 1124 )]; 1125 assert_eq!( 1126 granted_record.effective_permissions(), 1127 vec![RadrootsNostrConnectPermission::new( 1128 RadrootsNostrConnectMethod::Ping 1129 )] 1130 .into() 1131 ); 1132 1133 let mut approved_without_grants = auto_record; 1134 approved_without_grants.approval_state = RadrootsNostrSignerApprovalState::Approved; 1135 assert!(approved_without_grants.effective_permissions().is_empty()); 1136 } 1137 1138 #[test] 1139 fn permission_serde_helpers_round_trip_through_wrapper() { 1140 #[derive(Debug, Serialize, Deserialize)] 1141 struct PermissionWrapper { 1142 #[serde( 1143 serialize_with = "serialize_permission", 1144 deserialize_with = "deserialize_permission" 1145 )] 1146 permission: RadrootsNostrConnectPermission, 1147 } 1148 1149 let wrapper = PermissionWrapper { 1150 permission: RadrootsNostrConnectPermission::with_parameter( 1151 RadrootsNostrConnectMethod::SignEvent, 1152 "kind:1", 1153 ), 1154 }; 1155 1156 let json = serde_json::to_vec_pretty(&wrapper).expect("serialize wrapper"); 1157 let temp = tempdir().expect("tempdir"); 1158 let path = temp.path().join("permission.json"); 1159 std::fs::write(&path, &json).expect("write permission"); 1160 let file = std::fs::File::open(&path).expect("open permission"); 1161 let reader = std::io::BufReader::new(file); 1162 let decoded: PermissionWrapper = 1163 serde_json::from_reader(reader).expect("deserialize wrapper"); 1164 1165 assert_eq!(decoded.permission, wrapper.permission); 1166 1167 let value = serde_json::to_value(&wrapper).expect("serialize wrapper to value"); 1168 let decoded_from_value: PermissionWrapper = 1169 serde_json::from_value(value).expect("deserialize wrapper from value"); 1170 assert_eq!(decoded_from_value.permission, wrapper.permission); 1171 1172 let invalid = serde_json::from_str::<PermissionWrapper>(r#"{"permission":1}"#) 1173 .expect_err("invalid permission type"); 1174 assert!(invalid.to_string().contains("invalid type")); 1175 1176 let invalid_from_value = 1177 serde_json::from_value::<PermissionWrapper>(json!({ "permission": 1 })) 1178 .expect_err("invalid permission type from value"); 1179 assert!(invalid_from_value.to_string().contains("invalid type")); 1180 1181 let invalid_path = temp.path().join("invalid-permission.json"); 1182 std::fs::write(&invalid_path, br#"{"permission":1}"#).expect("write invalid permission"); 1183 let invalid_file = std::fs::File::open(&invalid_path).expect("open invalid permission"); 1184 let invalid_reader = std::io::BufReader::new(invalid_file); 1185 let invalid_from_reader = serde_json::from_reader::<_, PermissionWrapper>(invalid_reader) 1186 .expect_err("invalid permission type from reader"); 1187 assert!(invalid_from_reader.to_string().contains("invalid type")); 1188 } 1189 1190 #[test] 1191 fn connect_secret_hash_and_pending_request_helpers_validate_inputs() { 1192 let hash = 1193 RadrootsNostrSignerConnectSecretHash::from_secret(" secret ").expect("secret hash"); 1194 assert!(hash.matches_secret("secret")); 1195 assert!(!hash.matches_secret("other")); 1196 assert!(RadrootsNostrSignerConnectSecretHash::from_secret(" ").is_none()); 1197 1198 let pending = RadrootsNostrSignerPendingRequest::new(request_message("req-2"), 30) 1199 .expect("pending request"); 1200 assert_eq!(pending.request_id().as_str(), "req-2"); 1201 assert_eq!(pending.request_message().id, "req-2"); 1202 1203 let invalid_pending = RadrootsNostrSignerPendingRequest::new(request_message(" "), 30) 1204 .expect_err("invalid pending request id"); 1205 assert!(invalid_pending.to_string().contains("invalid request id")); 1206 1207 let auth_url = format!(" {} ", api_primary_https()); 1208 let challenge = 1209 RadrootsNostrSignerAuthChallenge::new(auth_url.as_str(), 31).expect("challenge"); 1210 assert_eq!(challenge.auth_url, format!("{}/", api_primary_https())); 1211 1212 let invalid_challenge = 1213 RadrootsNostrSignerAuthChallenge::new("not-a-url", 31).expect_err("invalid challenge"); 1214 assert!(invalid_challenge.to_string().contains("invalid auth url")); 1215 1216 let empty_challenge = 1217 RadrootsNostrSignerAuthChallenge::new(" ", 31).expect_err("empty challenge"); 1218 assert!(empty_challenge.to_string().contains("invalid auth url")); 1219 } 1220 1221 #[test] 1222 fn auth_challenge_deserialize_rejects_invalid_urls_across_entrypoints() { 1223 let invalid_json = json!({ 1224 "auth_url": " ", 1225 "required_at_unix": 44 1226 }); 1227 1228 let invalid_from_value = 1229 serde_json::from_value::<RadrootsNostrSignerAuthChallenge>(invalid_json.clone()) 1230 .expect_err("invalid auth challenge from value"); 1231 assert!(invalid_from_value.to_string().contains("invalid auth url")); 1232 1233 let invalid_from_str = 1234 serde_json::from_str::<RadrootsNostrSignerAuthChallenge>(&invalid_json.to_string()) 1235 .expect_err("invalid auth challenge from str"); 1236 assert!(invalid_from_str.to_string().contains("invalid auth url")); 1237 1238 let temp = tempdir().expect("tempdir"); 1239 let path = temp.path().join("invalid-auth-challenge.json"); 1240 std::fs::write( 1241 &path, 1242 serde_json::to_vec(&invalid_json).expect("serialize invalid auth challenge"), 1243 ) 1244 .expect("write invalid auth challenge"); 1245 let file = std::fs::File::open(&path).expect("open invalid auth challenge"); 1246 let reader = std::io::BufReader::new(file); 1247 let invalid_from_reader = 1248 serde_json::from_reader::<_, RadrootsNostrSignerAuthChallenge>(reader) 1249 .expect_err("invalid auth challenge from reader"); 1250 assert!(invalid_from_reader.to_string().contains("invalid auth url")); 1251 1252 let invalid_shape_json = json!({ 1253 "auth_url": 1, 1254 "required_at_unix": 44 1255 }); 1256 let invalid_shape_from_value = 1257 serde_json::from_value::<RadrootsNostrSignerAuthChallenge>(invalid_shape_json.clone()) 1258 .expect_err("invalid auth challenge shape from value"); 1259 assert!( 1260 invalid_shape_from_value 1261 .to_string() 1262 .contains("invalid type") 1263 ); 1264 1265 let invalid_shape_from_str = serde_json::from_str::<RadrootsNostrSignerAuthChallenge>( 1266 &invalid_shape_json.to_string(), 1267 ) 1268 .expect_err("invalid auth challenge shape from str"); 1269 assert!(invalid_shape_from_str.to_string().contains("invalid type")); 1270 1271 let invalid_shape_path = temp.path().join("invalid-auth-challenge-shape.json"); 1272 std::fs::write( 1273 &invalid_shape_path, 1274 serde_json::to_vec(&invalid_shape_json) 1275 .expect("serialize invalid auth challenge shape"), 1276 ) 1277 .expect("write invalid auth challenge shape"); 1278 let invalid_shape_file = 1279 std::fs::File::open(&invalid_shape_path).expect("open invalid auth challenge shape"); 1280 let invalid_shape_reader = std::io::BufReader::new(invalid_shape_file); 1281 let invalid_shape_from_reader = 1282 serde_json::from_reader::<_, RadrootsNostrSignerAuthChallenge>(invalid_shape_reader) 1283 .expect_err("invalid auth challenge shape from reader"); 1284 assert!( 1285 invalid_shape_from_reader 1286 .to_string() 1287 .contains("invalid type") 1288 ); 1289 } 1290 1291 #[test] 1292 fn connection_record_serde_migrates_legacy_connect_secret_and_validates_new_fields() { 1293 let record_json = json!({ 1294 "connection_id": "conn-legacy", 1295 "client_public_key": public_key(0x9).to_hex(), 1296 "signer_identity": public_identity(0x10), 1297 "user_identity": public_identity(0x11), 1298 "connect_secret": " legacy-secret ", 1299 "requested_permissions": "", 1300 "granted_permissions": [], 1301 "relays": [], 1302 "approval_requirement": "NotRequired", 1303 "approval_state": "NotRequired", 1304 "status": "Active", 1305 "status_reason": null, 1306 "created_at_unix": 1, 1307 "updated_at_unix": 1, 1308 "last_authenticated_at_unix": null, 1309 "last_request_at_unix": null 1310 }); 1311 1312 let decoded_without_secret: RadrootsNostrSignerConnectionRecord = 1313 serde_json::from_value(json!({ 1314 "connection_id": "conn-no-secret", 1315 "client_public_key": public_key(0x8).to_hex(), 1316 "signer_identity": public_identity(0x7), 1317 "user_identity": public_identity(0x6), 1318 "requested_permissions": "", 1319 "granted_permissions": [], 1320 "relays": [], 1321 "approval_requirement": "NotRequired", 1322 "approval_state": "NotRequired", 1323 "status": "Active", 1324 "created_at_unix": 0, 1325 "updated_at_unix": 0, 1326 "last_authenticated_at_unix": null, 1327 "last_request_at_unix": null 1328 })) 1329 .expect("deserialize record without secret"); 1330 assert!(decoded_without_secret.connect_secret_hash.is_none()); 1331 assert!( 1332 decoded_without_secret 1333 .connect_secret_consumed_at_unix 1334 .is_none() 1335 ); 1336 1337 let decoded_with_null_secret: RadrootsNostrSignerConnectionRecord = 1338 serde_json::from_value(json!({ 1339 "connection_id": "conn-null-secret", 1340 "client_public_key": public_key(0x5).to_hex(), 1341 "signer_identity": public_identity(0x4), 1342 "user_identity": public_identity(0x3), 1343 "connect_secret_hash": null, 1344 "requested_permissions": "", 1345 "granted_permissions": [], 1346 "relays": [], 1347 "approval_requirement": "NotRequired", 1348 "approval_state": "NotRequired", 1349 "status": "Active", 1350 "created_at_unix": 0, 1351 "updated_at_unix": 0, 1352 "last_authenticated_at_unix": null, 1353 "last_request_at_unix": null 1354 })) 1355 .expect("deserialize record with null secret"); 1356 assert!(decoded_with_null_secret.connect_secret_hash.is_none()); 1357 assert!( 1358 decoded_with_null_secret 1359 .connect_secret_consumed_at_unix 1360 .is_none() 1361 ); 1362 1363 let decoded: RadrootsNostrSignerConnectionRecord = 1364 serde_json::from_value(record_json).expect("deserialize legacy record"); 1365 assert!( 1366 decoded 1367 .connect_secret_hash 1368 .as_ref() 1369 .expect("connect secret hash") 1370 .matches_secret("legacy-secret") 1371 ); 1372 1373 let encoded = serde_json::to_value(&decoded).expect("serialize record"); 1374 assert!(encoded.get("connect_secret").is_none()); 1375 assert!(encoded.get("connect_secret_hash").is_some()); 1376 assert!(encoded.get("connect_secret_consumed_at_unix").is_none()); 1377 assert_eq!( 1378 encoded 1379 .get("auth_state") 1380 .and_then(serde_json::Value::as_str), 1381 Some("NotRequired") 1382 ); 1383 1384 let valid_hash = RadrootsNostrSignerConnectSecretHash::from_secret("explicit-secret") 1385 .expect("valid hash"); 1386 let decoded_new_format: RadrootsNostrSignerConnectionRecord = 1387 serde_json::from_value(json!({ 1388 "connection_id": "conn-new", 1389 "client_public_key": public_key(0x15).to_hex(), 1390 "signer_identity": public_identity(0x16), 1391 "user_identity": public_identity(0x17), 1392 "connect_secret_hash": { 1393 "algorithm": "sha256", 1394 "digest_hex": valid_hash.digest_hex 1395 }, 1396 "connect_secret_consumed_at_unix": 23, 1397 "requested_permissions": "", 1398 "granted_permissions": [], 1399 "relays": [], 1400 "approval_requirement": "NotRequired", 1401 "approval_state": "NotRequired", 1402 "status": "Active", 1403 "created_at_unix": 3, 1404 "updated_at_unix": 3, 1405 "last_authenticated_at_unix": null, 1406 "last_request_at_unix": null 1407 })) 1408 .expect("deserialize new-format record"); 1409 assert!( 1410 decoded_new_format 1411 .connect_secret_hash 1412 .as_ref() 1413 .expect("new-format hash") 1414 .matches_secret("explicit-secret") 1415 ); 1416 assert_eq!(decoded_new_format.connect_secret_consumed_at_unix, Some(23)); 1417 assert!(decoded_new_format.connect_secret_is_consumed()); 1418 1419 let temp = tempdir().expect("tempdir"); 1420 let path = temp.path().join("connection-record.json"); 1421 let reader_json = json!({ 1422 "connection_id": "conn-reader", 1423 "client_public_key": public_key(0x21).to_hex(), 1424 "signer_identity": public_identity(0x22), 1425 "user_identity": public_identity(0x23), 1426 "connect_secret_hash": { 1427 "algorithm": "sha256", 1428 "digest_hex": RadrootsNostrSignerConnectSecretHash::from_secret("reader-secret") 1429 .expect("reader hash") 1430 .digest_hex 1431 }, 1432 "requested_permissions": "", 1433 "granted_permissions": [], 1434 "relays": [], 1435 "approval_requirement": "NotRequired", 1436 "approval_state": "NotRequired", 1437 "auth_state": "Pending", 1438 "auth_challenge": { 1439 "auth_url": format!("{}/reader", api_primary_https()), 1440 "required_at_unix": 5 1441 }, 1442 "status": "Active", 1443 "created_at_unix": 5, 1444 "updated_at_unix": 5, 1445 "last_authenticated_at_unix": null, 1446 "last_request_at_unix": null 1447 }); 1448 std::fs::write( 1449 &path, 1450 serde_json::to_vec(&reader_json).expect("serialize reader json"), 1451 ) 1452 .expect("write reader json"); 1453 let file = std::fs::File::open(&path).expect("open reader json"); 1454 let reader = std::io::BufReader::new(file); 1455 let decoded_from_reader: RadrootsNostrSignerConnectionRecord = 1456 serde_json::from_reader(reader).expect("deserialize reader record"); 1457 assert!( 1458 decoded_from_reader 1459 .connect_secret_hash 1460 .as_ref() 1461 .expect("reader hash") 1462 .matches_secret("reader-secret") 1463 ); 1464 assert_eq!( 1465 decoded_from_reader 1466 .auth_challenge 1467 .as_ref() 1468 .expect("reader auth challenge") 1469 .auth_url, 1470 format!("{}/reader", api_primary_https()) 1471 ); 1472 1473 let invalid_hash_json = json!({ 1474 "connection_id": "conn-invalid", 1475 "client_public_key": public_key(0x12).to_hex(), 1476 "signer_identity": public_identity(0x13), 1477 "user_identity": public_identity(0x14), 1478 "connect_secret_hash": { 1479 "algorithm": "sha256", 1480 "digest_hex": "not-hex" 1481 }, 1482 "requested_permissions": "", 1483 "granted_permissions": [], 1484 "relays": [], 1485 "approval_requirement": "NotRequired", 1486 "approval_state": "NotRequired", 1487 "status": "Active", 1488 "auth_state": "Authorized", 1489 "auth_challenge": { 1490 "auth_url": api_primary_https(), 1491 "required_at_unix": 2 1492 }, 1493 "status_reason": null, 1494 "created_at_unix": 2, 1495 "updated_at_unix": 2, 1496 "last_authenticated_at_unix": null, 1497 "last_request_at_unix": null 1498 }); 1499 let invalid_hash = 1500 serde_json::from_value::<RadrootsNostrSignerConnectionRecord>(invalid_hash_json) 1501 .expect_err("invalid hash"); 1502 assert!( 1503 invalid_hash 1504 .to_string() 1505 .contains("invalid connect secret digest") 1506 ); 1507 1508 let invalid_nonhex_hash = 1509 serde_json::from_value::<RadrootsNostrSignerConnectionRecord>(json!({ 1510 "connection_id": "conn-invalid-nonhex", 1511 "client_public_key": public_key(0x18).to_hex(), 1512 "signer_identity": public_identity(0x19), 1513 "user_identity": public_identity(0x20), 1514 "connect_secret_hash": { 1515 "algorithm": "sha256", 1516 "digest_hex": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" 1517 }, 1518 "requested_permissions": "", 1519 "granted_permissions": [], 1520 "relays": [], 1521 "approval_requirement": "NotRequired", 1522 "approval_state": "NotRequired", 1523 "status": "Active", 1524 "created_at_unix": 4, 1525 "updated_at_unix": 4, 1526 "last_authenticated_at_unix": null, 1527 "last_request_at_unix": null 1528 })) 1529 .expect_err("invalid nonhex hash"); 1530 assert!( 1531 invalid_nonhex_hash 1532 .to_string() 1533 .contains("invalid connect secret digest") 1534 ); 1535 1536 let invalid_connect_secret_hash_type = 1537 serde_json::from_value::<RadrootsNostrSignerConnectionRecord>(json!({ 1538 "connection_id": "conn-invalid-type", 1539 "client_public_key": public_key(0x24).to_hex(), 1540 "signer_identity": public_identity(0x25), 1541 "user_identity": public_identity(0x26), 1542 "connect_secret_hash": 7, 1543 "requested_permissions": "", 1544 "granted_permissions": [], 1545 "relays": [], 1546 "approval_requirement": "NotRequired", 1547 "approval_state": "NotRequired", 1548 "status": "Active", 1549 "created_at_unix": 6, 1550 "updated_at_unix": 6, 1551 "last_authenticated_at_unix": null, 1552 "last_request_at_unix": null 1553 })) 1554 .expect_err("invalid connect secret hash type"); 1555 assert!(!invalid_connect_secret_hash_type.to_string().is_empty()); 1556 1557 let invalid_connect_secret_hash_path = temp.path().join("invalid-connect-secret-type.json"); 1558 std::fs::write( 1559 &invalid_connect_secret_hash_path, 1560 serde_json::to_vec(&json!({ 1561 "connection_id": "conn-invalid-type-reader", 1562 "client_public_key": public_key(0x27).to_hex(), 1563 "signer_identity": public_identity(0x28), 1564 "user_identity": public_identity(0x29), 1565 "connect_secret_hash": 9, 1566 "requested_permissions": "", 1567 "granted_permissions": [], 1568 "relays": [], 1569 "approval_requirement": "NotRequired", 1570 "approval_state": "NotRequired", 1571 "status": "Active", 1572 "created_at_unix": 7, 1573 "updated_at_unix": 7, 1574 "last_authenticated_at_unix": null, 1575 "last_request_at_unix": null 1576 })) 1577 .expect("serialize invalid connect secret hash type"), 1578 ) 1579 .expect("write invalid connect secret hash type"); 1580 let invalid_connect_secret_hash_file = 1581 std::fs::File::open(&invalid_connect_secret_hash_path) 1582 .expect("open invalid connect secret hash type"); 1583 let invalid_connect_secret_hash_reader = 1584 std::io::BufReader::new(invalid_connect_secret_hash_file); 1585 let invalid_connect_secret_hash_from_reader = serde_json::from_reader::< 1586 _, 1587 RadrootsNostrSignerConnectionRecord, 1588 >(invalid_connect_secret_hash_reader) 1589 .expect_err("invalid connect secret hash type from reader"); 1590 assert!( 1591 !invalid_connect_secret_hash_from_reader 1592 .to_string() 1593 .is_empty() 1594 ); 1595 } 1596 1597 #[test] 1598 fn store_state_default_is_empty() { 1599 let state = RadrootsNostrSignerStoreState::default(); 1600 assert_eq!(state.version, RADROOTS_NOSTR_SIGNER_STORE_VERSION); 1601 assert!(state.signer_identity.is_none()); 1602 assert!(state.connections.is_empty()); 1603 assert!(state.audit_records.is_empty()); 1604 } 1605 }