identities.rs (89138B)
1 use std::sync::{Mutex, MutexGuard}; 2 3 use crate::{ 4 AppCore, AppStateRepository, BoxFuture, Clock, DurableIdentityOperation, DurableOperationKind, 5 DurableOperationPhase, DurableOperationRepository, DurableOperationStart, DurableRequestId, 6 DurableTerminalOutcome, IdentityRepository, OperationPriorState, RemovalConfirmationToken, 7 SecretStore, StagedGeneratedKey, StateTransition, 8 }; 9 #[cfg(test)] 10 use crate::{ 11 IdentityOperationKind, IdentityOperationPhase, OperationDiagnostic, OperationId, 12 OperationJournal, PendingIdentityOperation, 13 }; 14 use harvestcircle_domain::{ 15 IdentityCreatedAt, LocalKeyringBinding, NostrIdentity, NostrIdentityReference, Nsec, PublicKey, 16 SafeError, SafeErrorCode, SafeMessage, SecretKeyInput, SignerAvailability, 17 }; 18 19 pub struct GenerateIdentityReceipt { 20 identity: NostrIdentity, 21 generated_nsec: Nsec, 22 } 23 24 #[derive(Clone, Debug, Eq, PartialEq)] 25 pub struct ImportIdentityReceipt { 26 identity: NostrIdentity, 27 } 28 29 impl ImportIdentityReceipt { 30 #[must_use] 31 pub const fn identity(&self) -> &NostrIdentity { 32 &self.identity 33 } 34 } 35 36 impl GenerateIdentityReceipt { 37 #[must_use] 38 pub const fn identity(&self) -> &NostrIdentity { 39 &self.identity 40 } 41 42 #[must_use] 43 pub const fn generated_nsec(&self) -> &Nsec { 44 &self.generated_nsec 45 } 46 } 47 48 impl AppCore { 49 /// Commits a staged generated key only after its recovery acknowledgement. 50 /// 51 /// # Errors 52 /// 53 /// Returns a safe conflict, keyring, persistence, or recovery error. 54 #[allow(clippy::too_many_arguments)] 55 pub async fn commit_staged_generated_key( 56 &self, 57 request_id: &DurableRequestId, 58 staged: StagedGeneratedKey, 59 identities: &(impl IdentityRepository + ?Sized), 60 app_state: &(impl AppStateRepository + ?Sized), 61 secrets: &(impl SecretStore + ?Sized), 62 operations: &(impl DurableOperationRepository + ?Sized), 63 clock: &(impl Clock + ?Sized), 64 ) -> Result<ImportIdentityReceipt, SafeError> { 65 let expected_revision = staged.expected_revision(); 66 self.require_revision(expected_revision)?; 67 let (identity, secret) = staged.into_commit_parts(); 68 let identity = self 69 .persist_identity_durable( 70 request_id, 71 DurableOperationKind::Create, 72 expected_revision, 73 &identity, 74 secret, 75 None, 76 identities, 77 app_state, 78 secrets, 79 operations, 80 clock, 81 ) 82 .await?; 83 Ok(ImportIdentityReceipt { identity }) 84 } 85 86 /// Generates and commits one identity under a durable caller request. 87 /// 88 /// # Errors 89 /// 90 /// Returns a safe conflict, keyring, persistence, or state error. Staged recovery transport 91 /// replaces this transitional generated-secret receipt in the custody phase. 92 #[allow(clippy::too_many_arguments)] 93 pub async fn generate_identity_durable( 94 &self, 95 request_id: &DurableRequestId, 96 expected_revision: u64, 97 identities: &(impl IdentityRepository + ?Sized), 98 app_state: &(impl AppStateRepository + ?Sized), 99 secrets: &(impl SecretStore + ?Sized), 100 operations: &(impl DurableOperationRepository + ?Sized), 101 clock: &(impl Clock + ?Sized), 102 ) -> Result<GenerateIdentityReceipt, SafeError> { 103 self.require_revision(expected_revision)?; 104 let generated = self.key_material().generate()?; 105 let (public_key, npub, secret, nsec) = generated.into_parts(); 106 let identity = NostrIdentity::new( 107 NostrIdentityReference::verify(public_key, npub.as_str().to_owned())?, 108 LocalKeyringBinding::new(public_key, SignerAvailability::Available), 109 None, 110 IdentityCreatedAt::new(clock.now()), 111 None, 112 )?; 113 let identity = self 114 .persist_identity_durable( 115 request_id, 116 DurableOperationKind::Create, 117 expected_revision, 118 &identity, 119 secret, 120 None, 121 identities, 122 app_state, 123 secrets, 124 operations, 125 clock, 126 ) 127 .await?; 128 Ok(GenerateIdentityReceipt { 129 identity, 130 generated_nsec: nsec, 131 }) 132 } 133 134 /// Imports or explicitly repairs one local identity under a durable caller request. 135 /// 136 /// # Errors 137 /// 138 /// Returns a safe conflict, validation, keyring, persistence, or state error. 139 #[allow(clippy::too_many_arguments)] 140 pub async fn import_secret_key_durable( 141 &self, 142 request_id: &DurableRequestId, 143 expected_revision: u64, 144 input: SecretKeyInput, 145 identities: &(impl IdentityRepository + ?Sized), 146 app_state: &(impl AppStateRepository + ?Sized), 147 secrets: &(impl SecretStore + ?Sized), 148 operations: &(impl DurableOperationRepository + ?Sized), 149 clock: &(impl Clock + ?Sized), 150 ) -> Result<ImportIdentityReceipt, SafeError> { 151 if let Some(existing) = operations.load_durable_operation(request_id).await? { 152 if !matches!( 153 existing.kind(), 154 DurableOperationKind::Import | DurableOperationKind::Repair 155 ) { 156 return Err(operation_conflict()); 157 } 158 require_completed_identity_operation(&existing)?; 159 let imported = self.key_material().import(input)?; 160 let (public_key, _, secret) = imported.into_parts(); 161 verify_completed_identity_replay( 162 &existing, 163 request_id, 164 existing.kind(), 165 expected_revision, 166 public_key, 167 secret, 168 secrets, 169 ) 170 .await?; 171 return identities 172 .find_identity(public_key) 173 .await? 174 .map(|identity| ImportIdentityReceipt { identity }) 175 .ok_or_else(recovery_required); 176 } 177 self.require_revision(expected_revision)?; 178 let imported = self.key_material().import(input)?; 179 let (public_key, npub, secret) = imported.into_parts(); 180 let previous = identities.find_identity(public_key).await?; 181 if let Some(existing) = &previous 182 && (local_keyring_binding(existing)?.availability() 183 != SignerAvailability::CredentialMissing 184 || secrets.contains(public_key).await?) 185 { 186 return Err(identity_exists()); 187 } 188 if previous.is_none() && secrets.contains(public_key).await? { 189 return Err(identity_exists()); 190 } 191 let identity = if let Some(existing) = &previous { 192 existing 193 .with_local_keyring_availability(SignerAvailability::Available) 194 .ok_or_else(recovery_required)? 195 } else { 196 NostrIdentity::new( 197 NostrIdentityReference::verify(public_key, npub.as_str().to_owned())?, 198 LocalKeyringBinding::new(public_key, SignerAvailability::Available), 199 None, 200 IdentityCreatedAt::new(clock.now()), 201 None, 202 )? 203 }; 204 let kind = if previous.is_some() { 205 DurableOperationKind::Repair 206 } else { 207 DurableOperationKind::Import 208 }; 209 let identity = self 210 .persist_identity_durable( 211 request_id, 212 kind, 213 expected_revision, 214 &identity, 215 secret, 216 previous.as_ref(), 217 identities, 218 app_state, 219 secrets, 220 operations, 221 clock, 222 ) 223 .await?; 224 Ok(ImportIdentityReceipt { identity }) 225 } 226 227 fn require_revision(&self, expected_revision: u64) -> Result<(), SafeError> { 228 if self.snapshot().revision().value() != expected_revision { 229 return Err(operation_conflict()); 230 } 231 Ok(()) 232 } 233 234 #[allow(clippy::too_many_arguments)] 235 async fn persist_identity_durable( 236 &self, 237 request_id: &DurableRequestId, 238 kind: DurableOperationKind, 239 expected_revision: u64, 240 identity: &NostrIdentity, 241 secret: SecretKeyInput, 242 previous: Option<&NostrIdentity>, 243 identities: &(impl IdentityRepository + ?Sized), 244 app_state: &(impl AppStateRepository + ?Sized), 245 secrets: &(impl SecretStore + ?Sized), 246 operations: &(impl DurableOperationRepository + ?Sized), 247 clock: &(impl Clock + ?Sized), 248 ) -> Result<NostrIdentity, SafeError> { 249 let prior_availability = previous 250 .map(local_keyring_binding) 251 .transpose()? 252 .map(LocalKeyringBinding::availability); 253 let prior = OperationPriorState::new( 254 app_state.load_selected_identity().await?, 255 prior_availability, 256 ); 257 match operations 258 .begin_durable_operation( 259 request_id, 260 kind, 261 identity.public_key(), 262 Some(expected_revision), 263 prior, 264 clock.now(), 265 ) 266 .await? 267 { 268 DurableOperationStart::Started(_) => {} 269 DurableOperationStart::Existing(operation) => { 270 verify_completed_identity_replay( 271 &operation, 272 request_id, 273 kind, 274 expected_revision, 275 identity.public_key(), 276 secret, 277 secrets, 278 ) 279 .await?; 280 return identities 281 .find_identity(operation.identity()) 282 .await? 283 .ok_or_else(recovery_required); 284 } 285 } 286 secrets 287 .put(request_id, identity.public_key(), secret) 288 .await?; 289 operations 290 .advance_durable_operation( 291 request_id, 292 DurableOperationPhase::IntentRecorded, 293 DurableOperationPhase::CredentialWritten, 294 clock.now(), 295 None, 296 ) 297 .await?; 298 if previous.is_some() { 299 identities.update_identity(identity).await?; 300 } else { 301 identities.insert_identity(identity).await?; 302 } 303 operations 304 .advance_durable_operation( 305 request_id, 306 DurableOperationPhase::CredentialWritten, 307 DurableOperationPhase::MetadataCommitted, 308 clock.now(), 309 None, 310 ) 311 .await?; 312 app_state 313 .save_selected_identity(Some(identity.public_key())) 314 .await?; 315 operations 316 .advance_durable_operation( 317 request_id, 318 DurableOperationPhase::MetadataCommitted, 319 DurableOperationPhase::SelectionCommitted, 320 clock.now(), 321 None, 322 ) 323 .await?; 324 let snapshot = self.apply_transition(StateTransition::ReplaceRegistry { 325 identities: identities.list_identities().await?, 326 selected: Some(identity.public_key()), 327 })?; 328 operations 329 .finalize_durable_operation( 330 request_id, 331 DurableOperationPhase::SelectionCommitted, 332 DurableTerminalOutcome::Completed, 333 Some(snapshot.revision().value()), 334 clock.now(), 335 ) 336 .await?; 337 Ok(identity.clone()) 338 } 339 340 /// Issues a single-use confirmation bound to the target and current revision. 341 /// 342 /// # Errors 343 /// 344 /// Returns a safe identity or application-state error. 345 pub fn request_identity_removal( 346 &self, 347 public_key: PublicKey, 348 clock: &(impl Clock + ?Sized), 349 ) -> Result<RemovalConfirmationToken, SafeError> { 350 self.issue_removal_token(public_key, clock.now()) 351 } 352 353 pub fn cancel_identity_removal(&self, token: RemovalConfirmationToken) -> bool { 354 self.cancel_removal_token(token) 355 } 356 357 /// Permanently removes a confirmed identity and selects a deterministic fallback. 358 /// 359 /// # Errors 360 /// 361 /// Returns a safe confirmation, credential, persistence, recovery, or state error. 362 #[cfg(test)] 363 pub async fn confirm_identity_removal( 364 &self, 365 token: RemovalConfirmationToken, 366 identities: &(impl IdentityRepository + ?Sized), 367 app_state: &(impl AppStateRepository + ?Sized), 368 secrets: &(impl SecretStore + ?Sized), 369 journal: &(impl OperationJournal + ?Sized), 370 clock: &(impl Clock + ?Sized), 371 ) -> Result<crate::AppSnapshot, SafeError> { 372 let public_key = self.consume_removal_token(token, clock.now())?; 373 let registry = identities.list_identities().await?; 374 let index = registry 375 .iter() 376 .position(|identity| identity.public_key() == public_key) 377 .ok_or_else(identity_not_found)?; 378 let selected = if self.snapshot().selected_identity() == Some(public_key) { 379 registry 380 .get(index + 1) 381 .or_else(|| index.checked_sub(1).and_then(|before| registry.get(before))) 382 .map(NostrIdentity::public_key) 383 } else { 384 self.snapshot().selected_identity() 385 }; 386 let operation = journal 387 .begin_operation(IdentityOperationKind::Remove, public_key, clock.now()) 388 .await?; 389 let was_active = self 390 .snapshot() 391 .active_identity() 392 .is_some_and(|active| active.identity().public_key() == public_key); 393 if was_active { 394 self.sign_out()?; 395 } 396 let identity = ®istry[index]; 397 let local_keyring = local_keyring_binding(identity)?; 398 let request_id = DurableRequestId::new_v7(); 399 match secrets.delete(&request_id, public_key).await { 400 Ok(()) => {} 401 Err(error) 402 if error.code() == SafeErrorCode::CredentialMissing 403 && local_keyring.availability() == SignerAvailability::CredentialMissing => {} 404 Err(error) => return Err(error), 405 } 406 journal 407 .update_operation( 408 operation, 409 IdentityOperationPhase::CredentialDeleted, 410 clock.now(), 411 None, 412 ) 413 .await?; 414 identities.remove_identity(public_key).await?; 415 app_state.save_selected_identity(selected).await?; 416 journal 417 .update_operation( 418 operation, 419 IdentityOperationPhase::MetadataDeleted, 420 clock.now(), 421 None, 422 ) 423 .await?; 424 journal.finalize_operation(operation).await?; 425 self.apply_transition(StateTransition::ReplaceRegistryPreservingSession { 426 identities: identities.list_identities().await?, 427 selected, 428 }) 429 } 430 431 /// Confirms and executes an expiring removal plan as a durable request. 432 /// 433 /// # Errors 434 /// 435 /// Returns a safe expiry, conflict, credential, persistence, or recovery error. 436 #[allow(clippy::too_many_arguments)] 437 pub async fn confirm_identity_removal_durable( 438 &self, 439 request_id: &DurableRequestId, 440 token: RemovalConfirmationToken, 441 identities: &(impl IdentityRepository + ?Sized), 442 app_state: &(impl AppStateRepository + ?Sized), 443 secrets: &(impl SecretStore + ?Sized), 444 operations: &(impl DurableOperationRepository + ?Sized), 445 clock: &(impl Clock + ?Sized), 446 ) -> Result<crate::AppSnapshot, SafeError> { 447 let expected_revision = token.revision().value(); 448 let public_key = self.consume_removal_token(token, clock.now())?; 449 self.require_revision(expected_revision)?; 450 let registry = identities.list_identities().await?; 451 let index = registry 452 .iter() 453 .position(|identity| identity.public_key() == public_key) 454 .ok_or_else(identity_not_found)?; 455 let selected = if self.snapshot().selected_identity() == Some(public_key) { 456 registry 457 .get(index + 1) 458 .or_else(|| index.checked_sub(1).and_then(|before| registry.get(before))) 459 .map(NostrIdentity::public_key) 460 } else { 461 self.snapshot().selected_identity() 462 }; 463 let identity = ®istry[index]; 464 let local_keyring = local_keyring_binding(identity)?; 465 match operations 466 .begin_durable_operation( 467 request_id, 468 DurableOperationKind::Remove, 469 public_key, 470 Some(expected_revision), 471 OperationPriorState::new(selected, Some(local_keyring.availability())), 472 clock.now(), 473 ) 474 .await? 475 { 476 DurableOperationStart::Started(_) => {} 477 DurableOperationStart::Existing(operation) => { 478 return if operation 479 .terminal() 480 .is_some_and(|receipt| receipt.outcome() == DurableTerminalOutcome::Completed) 481 { 482 Ok(self.snapshot()) 483 } else { 484 Err(recovery_required()) 485 }; 486 } 487 } 488 if self 489 .snapshot() 490 .active_identity() 491 .is_some_and(|active| active.identity().public_key() == public_key) 492 { 493 self.sign_out()?; 494 } 495 match secrets.delete(request_id, public_key).await { 496 Ok(()) => {} 497 Err(error) 498 if error.code() == SafeErrorCode::CredentialMissing 499 && local_keyring.availability() == SignerAvailability::CredentialMissing => {} 500 Err(error) => return Err(error), 501 } 502 operations 503 .advance_durable_operation( 504 request_id, 505 DurableOperationPhase::IntentRecorded, 506 DurableOperationPhase::CredentialDeleted, 507 clock.now(), 508 None, 509 ) 510 .await?; 511 identities.remove_identity(public_key).await?; 512 operations 513 .advance_durable_operation( 514 request_id, 515 DurableOperationPhase::CredentialDeleted, 516 DurableOperationPhase::MetadataDeleted, 517 clock.now(), 518 None, 519 ) 520 .await?; 521 app_state.save_selected_identity(selected).await?; 522 operations 523 .advance_durable_operation( 524 request_id, 525 DurableOperationPhase::MetadataDeleted, 526 DurableOperationPhase::SelectionCommitted, 527 clock.now(), 528 None, 529 ) 530 .await?; 531 let snapshot = 532 self.apply_transition(StateTransition::ReplaceRegistryPreservingSession { 533 identities: identities.list_identities().await?, 534 selected, 535 })?; 536 operations 537 .finalize_durable_operation( 538 request_id, 539 DurableOperationPhase::SelectionCommitted, 540 DurableTerminalOutcome::Completed, 541 Some(snapshot.revision().value()), 542 clock.now(), 543 ) 544 .await?; 545 Ok(snapshot) 546 } 547 548 /// Persists and publishes a saved identity selection without activating it. 549 /// 550 /// # Errors 551 /// 552 /// Returns a safe identity, persistence, or application-state error. 553 pub async fn select_identity( 554 &self, 555 public_key: PublicKey, 556 identities: &(impl IdentityRepository + ?Sized), 557 app_state: &(impl AppStateRepository + ?Sized), 558 ) -> Result<crate::AppSnapshot, SafeError> { 559 if identities.find_identity(public_key).await?.is_none() { 560 return Err(identity_not_found()); 561 } 562 app_state.save_selected_identity(Some(public_key)).await?; 563 self.apply_transition(StateTransition::Select(public_key)) 564 } 565 566 /// Generates, stores, and selects one local Nostr identity without activating it. 567 /// 568 /// # Errors 569 /// 570 /// Returns a safe key, credential, persistence, or application-state error. 571 #[cfg(test)] 572 pub async fn generate_identity( 573 &self, 574 identities: &(impl IdentityRepository + ?Sized), 575 app_state: &(impl AppStateRepository + ?Sized), 576 secrets: &(impl SecretStore + ?Sized), 577 journal: &(impl OperationJournal + ?Sized), 578 clock: &(impl Clock + ?Sized), 579 ) -> Result<GenerateIdentityReceipt, SafeError> { 580 let generated = self.key_material().generate()?; 581 let (public_key, npub, secret, nsec) = generated.into_parts(); 582 let identity = NostrIdentity::new( 583 NostrIdentityReference::verify(public_key, npub.as_str().to_owned())?, 584 LocalKeyringBinding::new(public_key, SignerAvailability::Available), 585 None, 586 IdentityCreatedAt::new(clock.now()), 587 None, 588 )?; 589 Self::persist_identity_transaction( 590 IdentityOperationKind::Add, 591 &identity, 592 secret, 593 None, 594 identities, 595 app_state, 596 secrets, 597 journal, 598 clock, 599 ) 600 .await?; 601 let registry = identities.list_identities().await?; 602 self.apply_transition(StateTransition::ReplaceRegistry { 603 identities: registry, 604 selected: Some(public_key), 605 })?; 606 Ok(GenerateIdentityReceipt { 607 identity, 608 generated_nsec: nsec, 609 }) 610 } 611 612 /// Imports, stores, and selects one local Nostr identity without activating it. 613 /// 614 /// # Errors 615 /// 616 /// Returns a safe key, credential, persistence, or application-state error. 617 #[cfg(test)] 618 pub async fn import_secret_key( 619 &self, 620 input: SecretKeyInput, 621 identities: &(impl IdentityRepository + ?Sized), 622 app_state: &(impl AppStateRepository + ?Sized), 623 secrets: &(impl SecretStore + ?Sized), 624 journal: &(impl OperationJournal + ?Sized), 625 clock: &(impl Clock + ?Sized), 626 ) -> Result<ImportIdentityReceipt, SafeError> { 627 let imported = self.key_material().import(input)?; 628 let (public_key, npub, secret) = imported.into_parts(); 629 if let Some(existing) = identities.find_identity(public_key).await? { 630 if local_keyring_binding(&existing)?.availability() 631 != SignerAvailability::CredentialMissing 632 || secrets.contains(public_key).await? 633 { 634 return Err(identity_exists()); 635 } 636 let repaired = existing 637 .with_local_keyring_availability(SignerAvailability::Available) 638 .ok_or_else(recovery_required)?; 639 Self::persist_identity_transaction( 640 IdentityOperationKind::Import, 641 &repaired, 642 secret, 643 Some(&existing), 644 identities, 645 app_state, 646 secrets, 647 journal, 648 clock, 649 ) 650 .await?; 651 self.apply_transition(StateTransition::ReplaceRegistry { 652 identities: identities.list_identities().await?, 653 selected: Some(public_key), 654 })?; 655 return Ok(ImportIdentityReceipt { identity: repaired }); 656 } 657 if secrets.contains(public_key).await? { 658 return Err(identity_exists()); 659 } 660 let identity = NostrIdentity::new( 661 NostrIdentityReference::verify(public_key, npub.as_str().to_owned())?, 662 LocalKeyringBinding::new(public_key, SignerAvailability::Available), 663 None, 664 IdentityCreatedAt::new(clock.now()), 665 None, 666 )?; 667 Self::persist_identity_transaction( 668 IdentityOperationKind::Import, 669 &identity, 670 secret, 671 None, 672 identities, 673 app_state, 674 secrets, 675 journal, 676 clock, 677 ) 678 .await?; 679 self.apply_transition(StateTransition::ReplaceRegistry { 680 identities: identities.list_identities().await?, 681 selected: Some(public_key), 682 })?; 683 Ok(ImportIdentityReceipt { identity }) 684 } 685 686 #[cfg(test)] 687 #[allow(clippy::too_many_arguments)] 688 async fn persist_identity_transaction( 689 kind: IdentityOperationKind, 690 identity: &NostrIdentity, 691 secret: SecretKeyInput, 692 previous: Option<&NostrIdentity>, 693 identities: &(impl IdentityRepository + ?Sized), 694 app_state: &(impl AppStateRepository + ?Sized), 695 secrets: &(impl SecretStore + ?Sized), 696 journal: &(impl OperationJournal + ?Sized), 697 clock: &(impl Clock + ?Sized), 698 ) -> Result<(), SafeError> { 699 let public_key = identity.public_key(); 700 let previous_selection = app_state.load_selected_identity().await?; 701 let operation = journal 702 .begin_operation(kind, public_key, clock.now()) 703 .await?; 704 let request_id = DurableRequestId::new_v7(); 705 if let Err(error) = secrets.put(&request_id, public_key, secret).await { 706 let _ = journal.finalize_operation(operation).await; 707 return Err(error); 708 } 709 if let Err(error) = journal 710 .update_operation( 711 operation, 712 IdentityOperationPhase::CredentialWritten, 713 clock.now(), 714 None, 715 ) 716 .await 717 { 718 return compensate_identity_write( 719 operation, 720 public_key, 721 error, 722 None, 723 previous_selection, 724 identities, 725 app_state, 726 secrets, 727 journal, 728 clock, 729 ) 730 .await; 731 } 732 let metadata_result = if previous.is_some() { 733 identities.update_identity(identity).await 734 } else { 735 identities.insert_identity(identity).await 736 }; 737 if let Err(error) = metadata_result { 738 return compensate_identity_write( 739 operation, 740 public_key, 741 error, 742 previous, 743 previous_selection, 744 identities, 745 app_state, 746 secrets, 747 journal, 748 clock, 749 ) 750 .await; 751 } 752 if let Err(error) = app_state.save_selected_identity(Some(public_key)).await { 753 return compensate_identity_write( 754 operation, 755 public_key, 756 error, 757 previous, 758 previous_selection, 759 identities, 760 app_state, 761 secrets, 762 journal, 763 clock, 764 ) 765 .await; 766 } 767 journal 768 .update_operation( 769 operation, 770 IdentityOperationPhase::MetadataCommitted, 771 clock.now(), 772 None, 773 ) 774 .await?; 775 journal.finalize_operation(operation).await 776 } 777 } 778 779 #[cfg(test)] 780 #[allow(clippy::too_many_arguments)] 781 async fn compensate_identity_write( 782 operation: OperationId, 783 public_key: PublicKey, 784 original_error: SafeError, 785 previous: Option<&NostrIdentity>, 786 previous_selection: Option<PublicKey>, 787 identities: &(impl IdentityRepository + ?Sized), 788 app_state: &(impl AppStateRepository + ?Sized), 789 secrets: &(impl SecretStore + ?Sized), 790 journal: &(impl OperationJournal + ?Sized), 791 clock: &(impl Clock + ?Sized), 792 ) -> Result<(), SafeError> { 793 let metadata_rollback = if let Some(previous) = previous { 794 identities.update_identity(previous).await 795 } else { 796 identities.remove_identity(public_key).await 797 }; 798 let selection_rollback = app_state.save_selected_identity(previous_selection).await; 799 let request_id = DurableRequestId::new_v7(); 800 let credential_rollback = secrets.delete(&request_id, public_key).await; 801 if metadata_rollback.is_err() || selection_rollback.is_err() || credential_rollback.is_err() { 802 let _ = journal 803 .update_operation( 804 operation, 805 IdentityOperationPhase::CompensationPending, 806 clock.now(), 807 Some(OperationDiagnostic::CompensationFailed), 808 ) 809 .await; 810 return Err(recovery_required()); 811 } 812 let _ = journal.finalize_operation(operation).await; 813 Err(original_error) 814 } 815 816 #[cfg(test)] 817 #[derive(Default)] 818 pub struct InMemoryOperationJournal { 819 state: Mutex<InMemoryJournalState>, 820 } 821 822 #[cfg(test)] 823 #[derive(Default)] 824 struct InMemoryJournalState { 825 next_id: u64, 826 pending: Vec<PendingIdentityOperation>, 827 } 828 829 #[cfg(test)] 830 impl OperationJournal for InMemoryOperationJournal { 831 fn begin_operation<'a>( 832 &'a self, 833 kind: IdentityOperationKind, 834 subject: PublicKey, 835 updated_at: harvestcircle_domain::UnixTimestamp, 836 ) -> BoxFuture<'a, Result<OperationId, SafeError>> { 837 Box::pin(async move { 838 let mut state = self.state.lock().map_err(|_| recovery_required())?; 839 state.next_id = state.next_id.checked_add(1).ok_or_else(recovery_required)?; 840 let id = OperationId::from_raw(state.next_id); 841 state.pending.push(PendingIdentityOperation::new( 842 id, 843 kind, 844 subject, 845 IdentityOperationPhase::IntentRecorded, 846 updated_at, 847 None, 848 )); 849 Ok(id) 850 }) 851 } 852 853 fn update_operation<'a>( 854 &'a self, 855 id: OperationId, 856 phase: IdentityOperationPhase, 857 updated_at: harvestcircle_domain::UnixTimestamp, 858 diagnostic: Option<OperationDiagnostic>, 859 ) -> BoxFuture<'a, Result<(), SafeError>> { 860 Box::pin(async move { 861 let mut state = self.state.lock().map_err(|_| recovery_required())?; 862 let operation = state 863 .pending 864 .iter_mut() 865 .find(|operation| operation.id() == id) 866 .ok_or_else(recovery_required)?; 867 *operation = PendingIdentityOperation::new( 868 id, 869 operation.kind(), 870 operation.subject(), 871 phase, 872 updated_at, 873 diagnostic, 874 ); 875 Ok(()) 876 }) 877 } 878 879 fn list_pending_operations( 880 &self, 881 ) -> BoxFuture<'_, Result<Vec<PendingIdentityOperation>, SafeError>> { 882 Box::pin(async move { 883 Ok(self 884 .state 885 .lock() 886 .map_err(|_| recovery_required())? 887 .pending 888 .clone()) 889 }) 890 } 891 892 fn finalize_operation(&self, id: OperationId) -> BoxFuture<'_, Result<(), SafeError>> { 893 Box::pin(async move { 894 self.state 895 .lock() 896 .map_err(|_| recovery_required())? 897 .pending 898 .retain(|operation| operation.id() != id); 899 Ok(()) 900 }) 901 } 902 } 903 904 #[derive(Default)] 905 pub struct InMemoryIdentityRepository { 906 state: Mutex<InMemoryIdentityState>, 907 } 908 909 #[derive(Default)] 910 struct InMemoryIdentityState { 911 identities: Vec<NostrIdentity>, 912 selected: Option<PublicKey>, 913 } 914 915 impl InMemoryIdentityRepository { 916 fn state(&self) -> Result<MutexGuard<'_, InMemoryIdentityState>, SafeError> { 917 self.state.lock().map_err(|_| recovery_required()) 918 } 919 } 920 921 impl IdentityRepository for InMemoryIdentityRepository { 922 fn list_identities(&self) -> BoxFuture<'_, Result<Vec<NostrIdentity>, SafeError>> { 923 Box::pin(async move { Ok(self.state()?.identities.clone()) }) 924 } 925 926 fn find_identity( 927 &self, 928 public_key: PublicKey, 929 ) -> BoxFuture<'_, Result<Option<NostrIdentity>, SafeError>> { 930 Box::pin(async move { 931 Ok(self 932 .state()? 933 .identities 934 .iter() 935 .find(|identity| identity.public_key() == public_key) 936 .cloned()) 937 }) 938 } 939 940 fn insert_identity<'a>( 941 &'a self, 942 identity: &'a NostrIdentity, 943 ) -> BoxFuture<'a, Result<(), SafeError>> { 944 Box::pin(async move { 945 let mut state = self.state()?; 946 if state 947 .identities 948 .iter() 949 .any(|saved| saved.public_key() == identity.public_key()) 950 { 951 return Err(identity_exists()); 952 } 953 state.identities.push(identity.clone()); 954 state 955 .identities 956 .sort_by_key(|saved| (saved.created_at().timestamp(), saved.public_key())); 957 Ok(()) 958 }) 959 } 960 961 fn update_identity<'a>( 962 &'a self, 963 identity: &'a NostrIdentity, 964 ) -> BoxFuture<'a, Result<(), SafeError>> { 965 Box::pin(async move { 966 let mut state = self.state()?; 967 let saved = state 968 .identities 969 .iter_mut() 970 .find(|saved| saved.public_key() == identity.public_key()) 971 .ok_or_else(identity_not_found)?; 972 *saved = identity.clone(); 973 Ok(()) 974 }) 975 } 976 977 fn remove_identity(&self, public_key: PublicKey) -> BoxFuture<'_, Result<(), SafeError>> { 978 Box::pin(async move { 979 let mut state = self.state()?; 980 state 981 .identities 982 .retain(|identity| identity.public_key() != public_key); 983 if state.selected == Some(public_key) { 984 state.selected = None; 985 } 986 Ok(()) 987 }) 988 } 989 } 990 991 impl AppStateRepository for InMemoryIdentityRepository { 992 fn load_selected_identity(&self) -> BoxFuture<'_, Result<Option<PublicKey>, SafeError>> { 993 Box::pin(async move { Ok(self.state()?.selected) }) 994 } 995 996 fn save_selected_identity( 997 &self, 998 public_key: Option<PublicKey>, 999 ) -> BoxFuture<'_, Result<(), SafeError>> { 1000 Box::pin(async move { 1001 let mut state = self.state()?; 1002 if public_key.is_some_and(|key| { 1003 !state 1004 .identities 1005 .iter() 1006 .any(|identity| identity.public_key() == key) 1007 }) { 1008 return Err(identity_not_found()); 1009 } 1010 state.selected = public_key; 1011 Ok(()) 1012 }) 1013 } 1014 } 1015 1016 fn require_completed_identity_operation( 1017 operation: &DurableIdentityOperation, 1018 ) -> Result<(), SafeError> { 1019 if operation.phase() != DurableOperationPhase::Finalized 1020 || !operation 1021 .terminal() 1022 .is_some_and(|receipt| receipt.outcome() == DurableTerminalOutcome::Completed) 1023 { 1024 return Err(recovery_required()); 1025 } 1026 Ok(()) 1027 } 1028 1029 async fn verify_completed_identity_replay( 1030 operation: &DurableIdentityOperation, 1031 request_id: &DurableRequestId, 1032 kind: DurableOperationKind, 1033 expected_revision: u64, 1034 public_key: PublicKey, 1035 secret: SecretKeyInput, 1036 secrets: &(impl SecretStore + ?Sized), 1037 ) -> Result<(), SafeError> { 1038 if operation.request_id() != request_id 1039 || operation.kind() != kind 1040 || operation.identity() != public_key 1041 || operation.expected_revision() != Some(expected_revision) 1042 { 1043 return Err(operation_conflict()); 1044 } 1045 require_completed_identity_operation(operation)?; 1046 let receipt = operation.terminal().ok_or_else(recovery_required)?; 1047 if receipt.request_id() != request_id || receipt.identity() != public_key { 1048 return Err(operation_conflict()); 1049 } 1050 secrets.verify(request_id, public_key, secret).await 1051 } 1052 1053 const fn identity_exists() -> SafeError { 1054 SafeError::new( 1055 SafeErrorCode::IdentityAlreadyExists, 1056 SafeMessage::new("The Nostr identity is already saved."), 1057 ) 1058 } 1059 1060 const fn identity_not_found() -> SafeError { 1061 SafeError::new( 1062 SafeErrorCode::IdentityNotFound, 1063 SafeMessage::new("The identity was not found."), 1064 ) 1065 } 1066 1067 fn local_keyring_binding(identity: &NostrIdentity) -> Result<LocalKeyringBinding, SafeError> { 1068 identity 1069 .signer_binding() 1070 .as_local_keyring() 1071 .ok_or_else(recovery_required) 1072 } 1073 1074 const fn recovery_required() -> SafeError { 1075 SafeError::new( 1076 SafeErrorCode::PendingOperationRecoveryRequired, 1077 SafeMessage::new("Identity recovery is required before this operation can continue."), 1078 ) 1079 } 1080 1081 const fn operation_conflict() -> SafeError { 1082 SafeError::new( 1083 SafeErrorCode::InvalidApplicationState, 1084 SafeMessage::new("The identity operation conflicts with the current application state."), 1085 ) 1086 } 1087 1088 #[cfg(test)] 1089 mod tests { 1090 use std::sync::atomic::{AtomicBool, Ordering}; 1091 1092 use harvestcircle_domain::{ 1093 IdentityCreatedAt, LocalKeyringBinding, NostrIdentity, NostrIdentityReference, PublicKey, 1094 SafeError, SafeErrorCode, SafeMessage, SecretKeyInput, SignerAvailability, UnixTimestamp, 1095 }; 1096 1097 use super::InMemoryIdentityRepository; 1098 use crate::{ 1099 AppCore, AppStateRepository, BoxFuture, Clock, DurableIdentityOperation, 1100 DurableOperationKind, DurableOperationPhase, DurableOperationReceipt, DurableRequestId, 1101 DurableTerminalOutcome, FailureSecretStore, IdentityOperationPhase, IdentityRepository, 1102 InMemoryOperationJournal, InMemorySecretStore, OperationJournal, OperationPriorState, 1103 ProfileRefreshStatus, ProfileRepository, RelayConfiguration, SecretStore, 1104 SecretStoreOperation, SessionState, StateTransition, 1105 recovery::tests::{TestDurableRepository, operation as durable_operation}, 1106 }; 1107 1108 struct FixedClock; 1109 1110 impl Clock for FixedClock { 1111 fn now(&self) -> UnixTimestamp { 1112 UnixTimestamp::from_seconds(10).expect("time") 1113 } 1114 } 1115 1116 struct LateClock; 1117 1118 impl Clock for LateClock { 1119 fn now(&self) -> UnixTimestamp { 1120 UnixTimestamp::from_seconds(311).expect("time") 1121 } 1122 } 1123 1124 struct EmptyProfiles; 1125 1126 impl ProfileRepository for EmptyProfiles { 1127 fn load_profile( 1128 &self, 1129 _public_key: PublicKey, 1130 ) -> BoxFuture<'_, Result<Option<crate::CachedProfile>, SafeError>> { 1131 Box::pin(async { Ok(None) }) 1132 } 1133 1134 fn save_profile<'a>( 1135 &'a self, 1136 _profile: &'a crate::CachedProfile, 1137 ) -> BoxFuture<'a, Result<(), SafeError>> { 1138 Box::pin(async { Ok(()) }) 1139 } 1140 1141 fn record_refresh_status<'a>( 1142 &'a self, 1143 _public_key: PublicKey, 1144 _refreshed_at: UnixTimestamp, 1145 _status: ProfileRefreshStatus, 1146 ) -> BoxFuture<'a, Result<(), SafeError>> { 1147 Box::pin(async { Ok(()) }) 1148 } 1149 1150 fn remove_profile(&self, _public_key: PublicKey) -> BoxFuture<'_, Result<(), SafeError>> { 1151 Box::pin(async { Ok(()) }) 1152 } 1153 } 1154 1155 #[derive(Default)] 1156 struct FailingUpdateJournal(InMemoryOperationJournal); 1157 1158 impl OperationJournal for FailingUpdateJournal { 1159 fn begin_operation<'a>( 1160 &'a self, 1161 kind: crate::IdentityOperationKind, 1162 subject: PublicKey, 1163 updated_at: UnixTimestamp, 1164 ) -> BoxFuture<'a, Result<crate::OperationId, SafeError>> { 1165 self.0.begin_operation(kind, subject, updated_at) 1166 } 1167 1168 fn update_operation<'a>( 1169 &'a self, 1170 _id: crate::OperationId, 1171 _phase: IdentityOperationPhase, 1172 _updated_at: UnixTimestamp, 1173 _diagnostic: Option<crate::OperationDiagnostic>, 1174 ) -> BoxFuture<'a, Result<(), SafeError>> { 1175 Box::pin(async { 1176 Err(SafeError::new( 1177 SafeErrorCode::StorageUnavailable, 1178 SafeMessage::new("The test journal is unavailable."), 1179 )) 1180 }) 1181 } 1182 1183 fn list_pending_operations( 1184 &self, 1185 ) -> BoxFuture<'_, Result<Vec<crate::PendingIdentityOperation>, SafeError>> { 1186 self.0.list_pending_operations() 1187 } 1188 1189 fn finalize_operation( 1190 &self, 1191 id: crate::OperationId, 1192 ) -> BoxFuture<'_, Result<(), SafeError>> { 1193 self.0.finalize_operation(id) 1194 } 1195 } 1196 1197 #[derive(Default)] 1198 struct FailingInsertRepository { 1199 inner: InMemoryIdentityRepository, 1200 } 1201 1202 #[derive(Default)] 1203 struct FailingSelectionRepository { 1204 inner: InMemoryIdentityRepository, 1205 fail_next_selection: AtomicBool, 1206 } 1207 1208 impl IdentityRepository for FailingSelectionRepository { 1209 fn list_identities(&self) -> BoxFuture<'_, Result<Vec<NostrIdentity>, SafeError>> { 1210 self.inner.list_identities() 1211 } 1212 1213 fn find_identity( 1214 &self, 1215 public_key: PublicKey, 1216 ) -> BoxFuture<'_, Result<Option<NostrIdentity>, SafeError>> { 1217 self.inner.find_identity(public_key) 1218 } 1219 1220 fn insert_identity<'a>( 1221 &'a self, 1222 identity: &'a NostrIdentity, 1223 ) -> BoxFuture<'a, Result<(), SafeError>> { 1224 self.inner.insert_identity(identity) 1225 } 1226 1227 fn update_identity<'a>( 1228 &'a self, 1229 identity: &'a NostrIdentity, 1230 ) -> BoxFuture<'a, Result<(), SafeError>> { 1231 self.inner.update_identity(identity) 1232 } 1233 1234 fn remove_identity(&self, public_key: PublicKey) -> BoxFuture<'_, Result<(), SafeError>> { 1235 self.inner.remove_identity(public_key) 1236 } 1237 } 1238 1239 impl AppStateRepository for FailingSelectionRepository { 1240 fn load_selected_identity(&self) -> BoxFuture<'_, Result<Option<PublicKey>, SafeError>> { 1241 self.inner.load_selected_identity() 1242 } 1243 1244 fn save_selected_identity( 1245 &self, 1246 public_key: Option<PublicKey>, 1247 ) -> BoxFuture<'_, Result<(), SafeError>> { 1248 if self.fail_next_selection.swap(false, Ordering::SeqCst) { 1249 return Box::pin(async { 1250 Err(SafeError::new( 1251 SafeErrorCode::StorageUnavailable, 1252 SafeMessage::new("The test selection repository is unavailable."), 1253 )) 1254 }); 1255 } 1256 self.inner.save_selected_identity(public_key) 1257 } 1258 } 1259 1260 impl IdentityRepository for FailingInsertRepository { 1261 fn list_identities(&self) -> BoxFuture<'_, Result<Vec<NostrIdentity>, SafeError>> { 1262 self.inner.list_identities() 1263 } 1264 1265 fn find_identity( 1266 &self, 1267 public_key: PublicKey, 1268 ) -> BoxFuture<'_, Result<Option<NostrIdentity>, SafeError>> { 1269 self.inner.find_identity(public_key) 1270 } 1271 1272 fn insert_identity<'a>( 1273 &'a self, 1274 _identity: &'a NostrIdentity, 1275 ) -> BoxFuture<'a, Result<(), SafeError>> { 1276 Box::pin(async { 1277 Err(SafeError::new( 1278 SafeErrorCode::StorageUnavailable, 1279 SafeMessage::new("The test identity repository is unavailable."), 1280 )) 1281 }) 1282 } 1283 1284 fn update_identity<'a>( 1285 &'a self, 1286 identity: &'a NostrIdentity, 1287 ) -> BoxFuture<'a, Result<(), SafeError>> { 1288 self.inner.update_identity(identity) 1289 } 1290 1291 fn remove_identity(&self, public_key: PublicKey) -> BoxFuture<'_, Result<(), SafeError>> { 1292 self.inner.remove_identity(public_key) 1293 } 1294 } 1295 1296 impl AppStateRepository for FailingInsertRepository { 1297 fn load_selected_identity(&self) -> BoxFuture<'_, Result<Option<PublicKey>, SafeError>> { 1298 self.inner.load_selected_identity() 1299 } 1300 1301 fn save_selected_identity( 1302 &self, 1303 public_key: Option<PublicKey>, 1304 ) -> BoxFuture<'_, Result<(), SafeError>> { 1305 self.inner.save_selected_identity(public_key) 1306 } 1307 } 1308 1309 #[tokio::test] 1310 async fn generate_identity_stores_selects_and_returns_one_time_nsec_without_activation() { 1311 let core = AppCore::in_memory(RelayConfiguration::default()); 1312 let identities = InMemoryIdentityRepository::default(); 1313 let secrets = InMemorySecretStore::default(); 1314 let journal = InMemoryOperationJournal::default(); 1315 core.bootstrap().expect("bootstrap"); 1316 1317 let receipt = core 1318 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 1319 .await 1320 .expect("generate"); 1321 let public_key = receipt.identity().public_key(); 1322 assert_eq!(public_key.to_hex().len(), 64); 1323 assert!(secrets.contains(public_key).await.expect("credential")); 1324 assert_eq!( 1325 identities 1326 .load_selected_identity() 1327 .await 1328 .expect("selection"), 1329 Some(public_key) 1330 ); 1331 assert_eq!(core.snapshot().selected_identity(), Some(public_key)); 1332 assert_eq!(core.snapshot().session(), SessionState::SignedOut); 1333 assert!(core.snapshot().active_identity().is_none()); 1334 assert_eq!(receipt.generated_nsec().with_exposed_secret(str::len), 63); 1335 assert!(!format!("{:?}", core.snapshot()).contains("nsec1")); 1336 } 1337 1338 #[tokio::test] 1339 async fn import_secret_key_accepts_nsec_and_hex_without_exposing_or_activating() { 1340 for input in [ 1341 "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5", 1342 "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7", 1343 ] { 1344 let core = AppCore::in_memory(RelayConfiguration::default()); 1345 let identities = InMemoryIdentityRepository::default(); 1346 let secrets = InMemorySecretStore::default(); 1347 let journal = InMemoryOperationJournal::default(); 1348 core.bootstrap().expect("bootstrap"); 1349 let receipt = core 1350 .import_secret_key( 1351 SecretKeyInput::parse(input.to_owned()).expect("input"), 1352 &identities, 1353 &identities, 1354 &secrets, 1355 &journal, 1356 &FixedClock, 1357 ) 1358 .await 1359 .expect("import"); 1360 let public_key = receipt.identity().public_key(); 1361 assert!(secrets.contains(public_key).await.expect("credential")); 1362 assert_eq!(core.snapshot().selected_identity(), Some(public_key)); 1363 assert_eq!(core.snapshot().session(), SessionState::SignedOut); 1364 assert!(!format!("{:?}", core.snapshot()).contains(input)); 1365 } 1366 } 1367 1368 #[tokio::test] 1369 async fn import_secret_key_rejects_invalid_nsec_checksum_before_persistence() { 1370 let core = AppCore::in_memory(RelayConfiguration::default()); 1371 let identities = InMemoryIdentityRepository::default(); 1372 let secrets = InMemorySecretStore::default(); 1373 let journal = InMemoryOperationJournal::default(); 1374 core.bootstrap().expect("bootstrap"); 1375 let input = SecretKeyInput::parse( 1376 "nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq".to_owned(), 1377 ) 1378 .expect("domain shape"); 1379 let error = core 1380 .import_secret_key( 1381 input, 1382 &identities, 1383 &identities, 1384 &secrets, 1385 &journal, 1386 &FixedClock, 1387 ) 1388 .await 1389 .expect_err("invalid import"); 1390 assert_eq!(error.code(), SafeErrorCode::InvalidSecretKey); 1391 assert!(core.snapshot().identities().is_empty()); 1392 } 1393 1394 #[tokio::test] 1395 async fn duplicate_import_preserves_existing_credential_and_snapshot() { 1396 let core = AppCore::in_memory(RelayConfiguration::default()); 1397 let identities = InMemoryIdentityRepository::default(); 1398 let secrets = InMemorySecretStore::default(); 1399 let journal = InMemoryOperationJournal::default(); 1400 core.bootstrap().expect("bootstrap"); 1401 let import = || { 1402 SecretKeyInput::parse( 1403 "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7".to_owned(), 1404 ) 1405 .expect("input") 1406 }; 1407 core.import_secret_key( 1408 import(), 1409 &identities, 1410 &identities, 1411 &secrets, 1412 &journal, 1413 &FixedClock, 1414 ) 1415 .await 1416 .expect("first import"); 1417 let before = core.snapshot(); 1418 let error = core 1419 .import_secret_key( 1420 import(), 1421 &identities, 1422 &identities, 1423 &secrets, 1424 &journal, 1425 &FixedClock, 1426 ) 1427 .await 1428 .expect_err("duplicate"); 1429 assert_eq!(error.code(), SafeErrorCode::IdentityAlreadyExists); 1430 assert_eq!(core.snapshot(), before); 1431 assert_eq!(core.snapshot().identities().len(), 1); 1432 } 1433 1434 #[tokio::test] 1435 async fn duplicate_import_repairs_only_explicit_missing_credential_identity() { 1436 let core = AppCore::in_memory(RelayConfiguration::default()); 1437 let identities = InMemoryIdentityRepository::default(); 1438 let secrets = InMemorySecretStore::default(); 1439 let journal = InMemoryOperationJournal::default(); 1440 core.bootstrap().expect("bootstrap"); 1441 let input = || { 1442 SecretKeyInput::parse( 1443 "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7".to_owned(), 1444 ) 1445 .expect("input") 1446 }; 1447 let imported = core.key_material().import(input()).expect("derive"); 1448 let (public_key, npub, _) = imported.into_parts(); 1449 let missing = NostrIdentity::new( 1450 NostrIdentityReference::verify(public_key, npub.as_str().to_owned()).expect("identity"), 1451 LocalKeyringBinding::new(public_key, SignerAvailability::CredentialMissing), 1452 None, 1453 IdentityCreatedAt::new(FixedClock.now()), 1454 None, 1455 ) 1456 .expect("missing identity"); 1457 identities 1458 .insert_identity(&missing) 1459 .await 1460 .expect("missing metadata"); 1461 identities 1462 .save_selected_identity(Some(public_key)) 1463 .await 1464 .expect("selection"); 1465 core.apply_transition(StateTransition::ReplaceRegistry { 1466 identities: vec![missing], 1467 selected: Some(public_key), 1468 }) 1469 .expect("registry"); 1470 1471 let receipt = core 1472 .import_secret_key( 1473 input(), 1474 &identities, 1475 &identities, 1476 &secrets, 1477 &journal, 1478 &FixedClock, 1479 ) 1480 .await 1481 .expect("repair"); 1482 assert_eq!( 1483 receipt 1484 .identity() 1485 .signer_binding() 1486 .as_local_keyring() 1487 .expect("local keyring") 1488 .availability(), 1489 SignerAvailability::Available 1490 ); 1491 assert!(secrets.contains(public_key).await.expect("credential")); 1492 assert_eq!(core.snapshot().identities().len(), 1); 1493 } 1494 1495 #[tokio::test] 1496 async fn identity_transaction_publishes_nothing_when_credential_write_fails() { 1497 let core = AppCore::in_memory(RelayConfiguration::default()); 1498 let identities = InMemoryIdentityRepository::default(); 1499 let secrets = FailureSecretStore::default(); 1500 let journal = InMemoryOperationJournal::default(); 1501 core.bootstrap().expect("bootstrap"); 1502 secrets.fail_next(SecretStoreOperation::Put); 1503 1504 let error = core 1505 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 1506 .await 1507 .err() 1508 .expect("credential failure"); 1509 assert_eq!(error.code(), SafeErrorCode::KeyringUnavailable); 1510 assert!(core.snapshot().identities().is_empty()); 1511 assert!( 1512 journal 1513 .list_pending_operations() 1514 .await 1515 .expect("journal") 1516 .is_empty() 1517 ); 1518 } 1519 1520 #[tokio::test] 1521 async fn identity_transaction_removes_written_credential_when_metadata_fails() { 1522 let core = AppCore::in_memory(RelayConfiguration::default()); 1523 let identities = FailingInsertRepository::default(); 1524 let secrets = FailureSecretStore::default(); 1525 let journal = InMemoryOperationJournal::default(); 1526 core.bootstrap().expect("bootstrap"); 1527 1528 let error = core 1529 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 1530 .await 1531 .err() 1532 .expect("metadata failure"); 1533 assert_eq!(error.code(), SafeErrorCode::StorageUnavailable); 1534 let calls = secrets.calls(); 1535 assert_eq!(calls[0].operation(), SecretStoreOperation::Put); 1536 assert_eq!(calls[1].operation(), SecretStoreOperation::Delete); 1537 assert_eq!(calls[0].public_key(), calls[1].public_key()); 1538 assert!(core.snapshot().identities().is_empty()); 1539 assert!( 1540 journal 1541 .list_pending_operations() 1542 .await 1543 .expect("journal") 1544 .is_empty() 1545 ); 1546 } 1547 1548 #[tokio::test] 1549 async fn identity_transaction_rolls_back_metadata_and_credential_when_selection_fails() { 1550 let core = AppCore::in_memory(RelayConfiguration::default()); 1551 let identities = FailingSelectionRepository::default(); 1552 let secrets = FailureSecretStore::default(); 1553 let journal = InMemoryOperationJournal::default(); 1554 core.bootstrap().expect("bootstrap"); 1555 identities.fail_next_selection.store(true, Ordering::SeqCst); 1556 1557 let error = core 1558 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 1559 .await 1560 .err() 1561 .expect("selection failure"); 1562 1563 assert_eq!(error.code(), SafeErrorCode::StorageUnavailable); 1564 assert!( 1565 identities 1566 .list_identities() 1567 .await 1568 .expect("identities") 1569 .is_empty() 1570 ); 1571 assert_eq!( 1572 identities 1573 .load_selected_identity() 1574 .await 1575 .expect("selection"), 1576 None 1577 ); 1578 let calls = secrets.calls(); 1579 assert_eq!(calls[0].operation(), SecretStoreOperation::Put); 1580 assert_eq!(calls[1].operation(), SecretStoreOperation::Delete); 1581 assert_eq!(calls[0].public_key(), calls[1].public_key()); 1582 assert!(core.snapshot().identities().is_empty()); 1583 assert!( 1584 journal 1585 .list_pending_operations() 1586 .await 1587 .expect("journal") 1588 .is_empty() 1589 ); 1590 } 1591 1592 #[tokio::test] 1593 async fn identity_transaction_retains_non_secret_journal_when_compensation_fails() { 1594 let core = AppCore::in_memory(RelayConfiguration::default()); 1595 let identities = FailingInsertRepository::default(); 1596 let secrets = FailureSecretStore::default(); 1597 let journal = InMemoryOperationJournal::default(); 1598 core.bootstrap().expect("bootstrap"); 1599 secrets.fail_next(SecretStoreOperation::Delete); 1600 1601 let error = core 1602 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 1603 .await 1604 .err() 1605 .expect("recovery required"); 1606 assert_eq!( 1607 error.code(), 1608 SafeErrorCode::PendingOperationRecoveryRequired 1609 ); 1610 let pending = journal.list_pending_operations().await.expect("journal"); 1611 assert_eq!(pending.len(), 1); 1612 assert_eq!( 1613 pending[0].phase(), 1614 IdentityOperationPhase::CompensationPending 1615 ); 1616 assert!(!format!("{pending:?}").contains("nsec1")); 1617 assert!(core.snapshot().identities().is_empty()); 1618 } 1619 1620 #[tokio::test] 1621 async fn select_identity_persists_existing_choice_without_activating() { 1622 let core = AppCore::in_memory(RelayConfiguration::default()); 1623 let identities = InMemoryIdentityRepository::default(); 1624 let secrets = InMemorySecretStore::default(); 1625 let journal = InMemoryOperationJournal::default(); 1626 core.bootstrap().expect("bootstrap"); 1627 let first = core 1628 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 1629 .await 1630 .expect("first") 1631 .identity() 1632 .public_key(); 1633 core.generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 1634 .await 1635 .expect("second"); 1636 1637 let selected = core 1638 .select_identity(first, &identities, &identities) 1639 .await 1640 .expect("select first"); 1641 assert_eq!(selected.selected_identity(), Some(first)); 1642 assert_eq!(selected.session(), SessionState::SignedOut); 1643 assert!(selected.active_identity().is_none()); 1644 assert_eq!( 1645 identities.load_selected_identity().await.expect("saved"), 1646 Some(first) 1647 ); 1648 let missing = core 1649 .select_identity( 1650 PublicKey::from_hex( 1651 "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af", 1652 ) 1653 .expect("unknown public key"), 1654 &identities, 1655 &identities, 1656 ) 1657 .await 1658 .expect_err("missing identity"); 1659 assert_eq!(missing.code(), SafeErrorCode::IdentityNotFound); 1660 assert_eq!(core.snapshot(), selected); 1661 } 1662 1663 #[tokio::test] 1664 async fn remove_identity_requires_fresh_single_use_confirmation_and_selects_next_fallback() { 1665 let core = AppCore::in_memory(RelayConfiguration::default()); 1666 let identities = InMemoryIdentityRepository::default(); 1667 let secrets = InMemorySecretStore::default(); 1668 let journal = InMemoryOperationJournal::default(); 1669 core.bootstrap().expect("bootstrap"); 1670 let first = core 1671 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 1672 .await 1673 .expect("first") 1674 .identity() 1675 .public_key(); 1676 let second = core 1677 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 1678 .await 1679 .expect("second") 1680 .identity() 1681 .public_key(); 1682 core.select_identity(first, &identities, &identities) 1683 .await 1684 .expect("select first"); 1685 let stale = core 1686 .request_identity_removal(first, &FixedClock) 1687 .expect("stale token"); 1688 core.select_identity(second, &identities, &identities) 1689 .await 1690 .expect("change revision"); 1691 let stale_error = core 1692 .confirm_identity_removal( 1693 stale, 1694 &identities, 1695 &identities, 1696 &secrets, 1697 &journal, 1698 &FixedClock, 1699 ) 1700 .await 1701 .expect_err("stale token"); 1702 assert_eq!(stale_error.code(), SafeErrorCode::InvalidApplicationState); 1703 assert_eq!(core.snapshot().identities().len(), 2); 1704 1705 core.select_identity(first, &identities, &identities) 1706 .await 1707 .expect("reselect first"); 1708 let token = core 1709 .request_identity_removal(first, &FixedClock) 1710 .expect("token"); 1711 let removed = core 1712 .confirm_identity_removal( 1713 token, 1714 &identities, 1715 &identities, 1716 &secrets, 1717 &journal, 1718 &FixedClock, 1719 ) 1720 .await 1721 .expect("remove"); 1722 assert_eq!(removed.identities().len(), 1); 1723 assert_eq!(removed.selected_identity(), Some(second)); 1724 assert!(!secrets.contains(first).await.expect("credential removed")); 1725 assert_eq!(removed.session(), SessionState::SignedOut); 1726 } 1727 1728 #[tokio::test] 1729 async fn removal_preflight_reports_impact_expires_and_can_be_cancelled() { 1730 let core = AppCore::in_memory(RelayConfiguration::default()); 1731 let identities = InMemoryIdentityRepository::default(); 1732 let secrets = InMemorySecretStore::default(); 1733 let journal = InMemoryOperationJournal::default(); 1734 core.bootstrap().expect("bootstrap"); 1735 let identity = core 1736 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 1737 .await 1738 .expect("identity") 1739 .identity() 1740 .public_key(); 1741 let expired = core 1742 .request_identity_removal(identity, &FixedClock) 1743 .expect("plan"); 1744 assert!(expired.impact().deletes_local_credential()); 1745 assert!(!expired.impact().signs_out()); 1746 assert!( 1747 core.confirm_identity_removal( 1748 expired, 1749 &identities, 1750 &identities, 1751 &secrets, 1752 &journal, 1753 &LateClock, 1754 ) 1755 .await 1756 .is_err() 1757 ); 1758 let cancelled = core 1759 .request_identity_removal(identity, &FixedClock) 1760 .expect("replacement plan"); 1761 assert!(core.cancel_identity_removal(cancelled)); 1762 assert_eq!(core.snapshot().identities().len(), 1); 1763 } 1764 1765 #[tokio::test] 1766 async fn import_rejects_orphan_credentials_and_durable_nonterminal_replays() { 1767 const SECRET: &str = "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7"; 1768 let core = AppCore::in_memory(RelayConfiguration::default()); 1769 let identities = InMemoryIdentityRepository::default(); 1770 let secrets = InMemorySecretStore::default(); 1771 let journal = InMemoryOperationJournal::default(); 1772 core.bootstrap().expect("bootstrap"); 1773 let material = core 1774 .key_material() 1775 .import(SecretKeyInput::parse(SECRET.to_owned()).expect("secret")) 1776 .expect("key material"); 1777 let (public_key, _npub, secret) = material.into_parts(); 1778 secrets 1779 .put(&DurableRequestId::new_v7(), public_key, secret) 1780 .await 1781 .expect("orphan credential"); 1782 1783 assert_eq!( 1784 core.import_secret_key( 1785 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 1786 &identities, 1787 &identities, 1788 &secrets, 1789 &journal, 1790 &FixedClock, 1791 ) 1792 .await 1793 .expect_err("orphan credential must fail") 1794 .code(), 1795 SafeErrorCode::IdentityAlreadyExists 1796 ); 1797 1798 let pending = durable_operation( 1799 DurableOperationKind::Import, 1800 DurableOperationPhase::IntentRecorded, 1801 public_key, 1802 None, 1803 ); 1804 let request_id = pending.request_id().clone(); 1805 let operations = TestDurableRepository::new(pending); 1806 assert_eq!( 1807 core.import_secret_key_durable( 1808 &request_id, 1809 core.snapshot().revision().value(), 1810 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 1811 &identities, 1812 &identities, 1813 &secrets, 1814 &operations, 1815 &FixedClock, 1816 ) 1817 .await 1818 .expect_err("unfinished replay must require recovery") 1819 .code(), 1820 SafeErrorCode::PendingOperationRecoveryRequired 1821 ); 1822 } 1823 1824 const ADMISSION_SECRET: &str = 1825 "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7"; 1826 1827 struct CompletedAdmissionFixture { 1828 core: AppCore, 1829 identities: InMemoryIdentityRepository, 1830 secrets: FailureSecretStore, 1831 operations: TestDurableRepository, 1832 original: NostrIdentity, 1833 candidate: NostrIdentity, 1834 request: DurableRequestId, 1835 expected_revision: u64, 1836 } 1837 1838 impl CompletedAdmissionFixture { 1839 async fn new() -> Self { 1840 let core = AppCore::in_memory(RelayConfiguration::default()); 1841 core.bootstrap().expect("bootstrap"); 1842 let identities = InMemoryIdentityRepository::default(); 1843 let secrets = FailureSecretStore::default(); 1844 let material = core 1845 .key_material() 1846 .import(SecretKeyInput::parse(ADMISSION_SECRET.to_owned()).expect("secret")) 1847 .expect("key material"); 1848 let (public_key, npub, secret) = material.into_parts(); 1849 let original = NostrIdentity::new( 1850 NostrIdentityReference::verify(public_key, npub.as_str().to_owned()) 1851 .expect("reference"), 1852 LocalKeyringBinding::new(public_key, SignerAvailability::Available), 1853 None, 1854 IdentityCreatedAt::new(FixedClock.now()), 1855 None, 1856 ) 1857 .expect("original identity"); 1858 let candidate = NostrIdentity::new( 1859 NostrIdentityReference::verify(public_key, npub.as_str().to_owned()) 1860 .expect("reference"), 1861 LocalKeyringBinding::new(public_key, SignerAvailability::Available), 1862 None, 1863 IdentityCreatedAt::new(LateClock.now()), 1864 None, 1865 ) 1866 .expect("candidate identity"); 1867 identities 1868 .insert_identity(&original) 1869 .await 1870 .expect("insert original identity"); 1871 identities 1872 .save_selected_identity(Some(public_key)) 1873 .await 1874 .expect("selection"); 1875 let request = DurableRequestId::new_v7(); 1876 secrets 1877 .put(&request, public_key, secret) 1878 .await 1879 .expect("original custody"); 1880 let expected_revision = core.snapshot().revision().value(); 1881 let operation = DurableIdentityOperation::new( 1882 request.clone(), 1883 DurableOperationKind::Import, 1884 public_key, 1885 Some(expected_revision), 1886 DurableOperationPhase::Finalized, 1887 OperationPriorState::new(Some(public_key), None), 1888 FixedClock.now(), 1889 None, 1890 Some(DurableOperationReceipt::new( 1891 request.clone(), 1892 public_key, 1893 DurableTerminalOutcome::Completed, 1894 Some(expected_revision + 1), 1895 FixedClock.now(), 1896 )), 1897 ); 1898 Self { 1899 core, 1900 identities, 1901 secrets, 1902 operations: TestDurableRepository::new(operation), 1903 original, 1904 candidate, 1905 request, 1906 expected_revision, 1907 } 1908 } 1909 1910 fn canonical_secret(&self) -> SecretKeyInput { 1911 self.core 1912 .key_material() 1913 .import(SecretKeyInput::parse(ADMISSION_SECRET.to_owned()).expect("secret")) 1914 .expect("key material") 1915 .into_parts() 1916 .2 1917 } 1918 1919 async fn attempt( 1920 &self, 1921 kind: DurableOperationKind, 1922 expected_revision: u64, 1923 secret: SecretKeyInput, 1924 ) -> Result<NostrIdentity, SafeError> { 1925 self.core 1926 .persist_identity_durable( 1927 &self.request, 1928 kind, 1929 expected_revision, 1930 &self.candidate, 1931 secret, 1932 None, 1933 &self.identities, 1934 &self.identities, 1935 &self.secrets, 1936 &self.operations, 1937 &FixedClock, 1938 ) 1939 .await 1940 } 1941 } 1942 1943 #[tokio::test] 1944 async fn completed_admission_race_verifies_original_binding_and_identity_without_mutation() { 1945 let fixture = CompletedAdmissionFixture::new().await; 1946 let before_operation = fixture.operations.operation().clone(); 1947 let before_state = fixture.core.snapshot(); 1948 let before_identities = fixture 1949 .identities 1950 .list_identities() 1951 .await 1952 .expect("identities"); 1953 let before_selection = fixture 1954 .identities 1955 .load_selected_identity() 1956 .await 1957 .expect("selection"); 1958 let exact = fixture 1959 .attempt( 1960 DurableOperationKind::Import, 1961 fixture.expected_revision, 1962 fixture.canonical_secret(), 1963 ) 1964 .await; 1965 let changed_kind = fixture 1966 .attempt( 1967 DurableOperationKind::Repair, 1968 fixture.expected_revision, 1969 fixture.canonical_secret(), 1970 ) 1971 .await 1972 .expect_err("original kind required"); 1973 let changed_revision = fixture 1974 .attempt( 1975 DurableOperationKind::Import, 1976 fixture.expected_revision + 1, 1977 fixture.canonical_secret(), 1978 ) 1979 .await 1980 .expect_err("original revision required"); 1981 let changed_secret = fixture 1982 .attempt( 1983 DurableOperationKind::Import, 1984 fixture.expected_revision, 1985 SecretKeyInput::parse( 1986 "0000000000000000000000000000000000000000000000000000000000000001".to_owned(), 1987 ) 1988 .expect("different secret"), 1989 ) 1990 .await 1991 .expect_err("full secret required"); 1992 let after_operation = fixture.operations.operation().clone(); 1993 let after_state = fixture.core.snapshot(); 1994 let after_identities = fixture 1995 .identities 1996 .list_identities() 1997 .await 1998 .expect("identities"); 1999 let after_selection = fixture 2000 .identities 2001 .load_selected_identity() 2002 .await 2003 .expect("selection"); 2004 let retained = fixture 2005 .secrets 2006 .load(fixture.original.public_key()) 2007 .await 2008 .expect("credential"); 2009 let mutations = fixture 2010 .secrets 2011 .calls() 2012 .into_iter() 2013 .filter(|call| { 2014 matches!( 2015 call.operation(), 2016 SecretStoreOperation::Put | SecretStoreOperation::Delete, 2017 ) 2018 }) 2019 .collect::<Vec<_>>(); 2020 assert_eq!(exact.expect("exact completed admission"), fixture.original); 2021 assert_ne!(fixture.candidate, fixture.original); 2022 assert_eq!(changed_kind.code(), SafeErrorCode::InvalidApplicationState); 2023 assert_eq!( 2024 changed_revision.code(), 2025 SafeErrorCode::InvalidApplicationState 2026 ); 2027 assert_eq!( 2028 changed_secret.code(), 2029 SafeErrorCode::InvalidApplicationState 2030 ); 2031 assert_eq!(before_operation, after_operation); 2032 assert_eq!(before_state, after_state); 2033 assert_eq!(before_identities, after_identities); 2034 assert_eq!(before_selection, after_selection); 2035 assert_eq!(mutations.len(), 1); 2036 assert_eq!(mutations[0].operation(), SecretStoreOperation::Put); 2037 assert!(retained.with_exposed_secret(|value| { 2038 fixture 2039 .canonical_secret() 2040 .with_exposed_secret(|expected| value == expected) 2041 })); 2042 assert!( 2043 !format!("{changed_kind:?} {changed_revision:?} {changed_secret:?}") 2044 .contains(ADMISSION_SECRET) 2045 ); 2046 } 2047 2048 #[tokio::test] 2049 async fn completed_admission_race_rejects_missing_and_rebound_custody_without_mutation() { 2050 for rebound in [false, true] { 2051 let fixture = CompletedAdmissionFixture::new().await; 2052 fixture 2053 .secrets 2054 .delete(&fixture.request, fixture.original.public_key()) 2055 .await 2056 .expect("remove custody"); 2057 let another_request = DurableRequestId::new_v7(); 2058 if rebound { 2059 fixture 2060 .secrets 2061 .put( 2062 &another_request, 2063 fixture.original.public_key(), 2064 fixture.canonical_secret(), 2065 ) 2066 .await 2067 .expect("replacement custody"); 2068 } 2069 let before_operation = fixture.operations.operation().clone(); 2070 let before_state = fixture.core.snapshot(); 2071 let before_identities = fixture 2072 .identities 2073 .list_identities() 2074 .await 2075 .expect("identities"); 2076 let before_selection = fixture 2077 .identities 2078 .load_selected_identity() 2079 .await 2080 .expect("selection"); 2081 let before_mutations = fixture 2082 .secrets 2083 .calls() 2084 .into_iter() 2085 .filter(|call| { 2086 matches!( 2087 call.operation(), 2088 SecretStoreOperation::Put | SecretStoreOperation::Delete, 2089 ) 2090 }) 2091 .collect::<Vec<_>>(); 2092 let error = fixture 2093 .attempt( 2094 DurableOperationKind::Import, 2095 fixture.expected_revision, 2096 fixture.canonical_secret(), 2097 ) 2098 .await 2099 .expect_err("unbound custody must fail"); 2100 let after_operation = fixture.operations.operation().clone(); 2101 let after_state = fixture.core.snapshot(); 2102 let after_identities = fixture 2103 .identities 2104 .list_identities() 2105 .await 2106 .expect("identities"); 2107 let after_selection = fixture 2108 .identities 2109 .load_selected_identity() 2110 .await 2111 .expect("selection"); 2112 let after_mutations = fixture 2113 .secrets 2114 .calls() 2115 .into_iter() 2116 .filter(|call| { 2117 matches!( 2118 call.operation(), 2119 SecretStoreOperation::Put | SecretStoreOperation::Delete, 2120 ) 2121 }) 2122 .collect::<Vec<_>>(); 2123 let present = fixture 2124 .secrets 2125 .contains(fixture.original.public_key()) 2126 .await 2127 .expect("availability"); 2128 let replacement_binding = if rebound { 2129 fixture 2130 .secrets 2131 .verify( 2132 &another_request, 2133 fixture.original.public_key(), 2134 fixture.canonical_secret(), 2135 ) 2136 .await 2137 } else { 2138 Ok(()) 2139 }; 2140 assert_eq!( 2141 error.code(), 2142 if rebound { 2143 SafeErrorCode::InvalidApplicationState 2144 } else { 2145 SafeErrorCode::CredentialMissing 2146 } 2147 ); 2148 assert_eq!(before_operation, after_operation); 2149 assert_eq!(before_state, after_state); 2150 assert_eq!(before_identities, after_identities); 2151 assert_eq!(before_selection, after_selection); 2152 assert_eq!(before_mutations, after_mutations); 2153 assert_eq!(present, rebound); 2154 assert!(replacement_binding.is_ok()); 2155 assert!(!format!("{error:?}").contains(ADMISSION_SECRET)); 2156 } 2157 } 2158 2159 #[tokio::test] 2160 async fn durable_import_covers_new_and_missing_credential_repair_paths() { 2161 const SECRET: &str = "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7"; 2162 for repair in [false, true] { 2163 let core = AppCore::in_memory(RelayConfiguration::default()); 2164 let identities = InMemoryIdentityRepository::default(); 2165 let secrets = InMemorySecretStore::default(); 2166 let material = core 2167 .key_material() 2168 .import(SecretKeyInput::parse(SECRET.to_owned()).expect("secret")) 2169 .expect("key material"); 2170 let (public_key, npub, secret) = material.into_parts(); 2171 drop(secret); 2172 if repair { 2173 let identity = NostrIdentity::new( 2174 NostrIdentityReference::verify(public_key, npub.as_str().to_owned()) 2175 .expect("identity"), 2176 LocalKeyringBinding::new(public_key, SignerAvailability::CredentialMissing), 2177 None, 2178 IdentityCreatedAt::new(FixedClock.now()), 2179 None, 2180 ) 2181 .expect("identity"); 2182 identities 2183 .insert_identity(&identity) 2184 .await 2185 .expect("insert identity"); 2186 identities 2187 .save_selected_identity(Some(public_key)) 2188 .await 2189 .expect("selection"); 2190 core.apply_transition(StateTransition::BootstrapRegistry { 2191 identities: vec![identity], 2192 selected: Some(public_key), 2193 }) 2194 .expect("registry"); 2195 } else { 2196 core.bootstrap().expect("bootstrap"); 2197 } 2198 let kind = if repair { 2199 DurableOperationKind::Repair 2200 } else { 2201 DurableOperationKind::Import 2202 }; 2203 let pending = durable_operation( 2204 kind, 2205 DurableOperationPhase::IntentRecorded, 2206 public_key, 2207 repair.then_some(SignerAvailability::CredentialMissing), 2208 ); 2209 let request_id = pending.request_id().clone(); 2210 let operations = TestDurableRepository::fresh(pending); 2211 let receipt = core 2212 .import_secret_key_durable( 2213 &request_id, 2214 core.snapshot().revision().value(), 2215 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 2216 &identities, 2217 &identities, 2218 &secrets, 2219 &operations, 2220 &FixedClock, 2221 ) 2222 .await 2223 .expect("durable import"); 2224 assert_eq!(receipt.identity().public_key(), public_key); 2225 assert_eq!( 2226 operations.operation().phase(), 2227 DurableOperationPhase::Finalized 2228 ); 2229 } 2230 } 2231 2232 #[tokio::test] 2233 async fn removal_of_unselected_identity_preserves_the_current_selection() { 2234 let core = AppCore::in_memory(RelayConfiguration::default()); 2235 let identities = InMemoryIdentityRepository::default(); 2236 let secrets = InMemorySecretStore::default(); 2237 let journal = InMemoryOperationJournal::default(); 2238 core.bootstrap().expect("bootstrap"); 2239 let first = core 2240 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 2241 .await 2242 .expect("first") 2243 .identity() 2244 .public_key(); 2245 let second = core 2246 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 2247 .await 2248 .expect("second") 2249 .identity() 2250 .public_key(); 2251 let token = core 2252 .request_identity_removal(first, &FixedClock) 2253 .expect("removal token"); 2254 let snapshot = core 2255 .confirm_identity_removal( 2256 token, 2257 &identities, 2258 &identities, 2259 &secrets, 2260 &journal, 2261 &FixedClock, 2262 ) 2263 .await 2264 .expect("remove unselected identity"); 2265 assert_eq!(snapshot.selected_identity(), Some(second)); 2266 2267 let missing = crate::test_support::valid_test_public_key(99).expect("missing key"); 2268 assert!( 2269 identities 2270 .insert_identity(&snapshot.identities()[0]) 2271 .await 2272 .is_err() 2273 ); 2274 assert!( 2275 identities 2276 .save_selected_identity(Some(missing)) 2277 .await 2278 .is_err() 2279 ); 2280 2281 let third = core 2282 .generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 2283 .await 2284 .expect("third") 2285 .identity() 2286 .public_key(); 2287 let token = core 2288 .request_identity_removal(second, &FixedClock) 2289 .expect("durable removal token"); 2290 let pending = durable_operation( 2291 DurableOperationKind::Remove, 2292 DurableOperationPhase::IntentRecorded, 2293 second, 2294 Some(SignerAvailability::Available), 2295 ); 2296 let request_id = pending.request_id().clone(); 2297 let operations = TestDurableRepository::fresh(pending); 2298 let snapshot = core 2299 .confirm_identity_removal_durable( 2300 &request_id, 2301 token, 2302 &identities, 2303 &identities, 2304 &secrets, 2305 &operations, 2306 &FixedClock, 2307 ) 2308 .await 2309 .expect("durable unselected removal"); 2310 assert_eq!(snapshot.selected_identity(), Some(third)); 2311 } 2312 2313 #[tokio::test] 2314 async fn duplicate_missing_binding_with_orphan_credential_fails_closed() { 2315 const SECRET: &str = "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7"; 2316 let core = AppCore::in_memory(RelayConfiguration::default()); 2317 let identities = InMemoryIdentityRepository::default(); 2318 let secrets = InMemorySecretStore::default(); 2319 let journal = InMemoryOperationJournal::default(); 2320 let material = core 2321 .key_material() 2322 .import(SecretKeyInput::parse(SECRET.to_owned()).expect("secret")) 2323 .expect("key material"); 2324 let (public_key, npub, secret) = material.into_parts(); 2325 let identity = NostrIdentity::new( 2326 NostrIdentityReference::verify(public_key, npub.as_str().to_owned()).expect("identity"), 2327 LocalKeyringBinding::new(public_key, SignerAvailability::CredentialMissing), 2328 None, 2329 IdentityCreatedAt::new(FixedClock.now()), 2330 None, 2331 ) 2332 .expect("identity"); 2333 identities 2334 .insert_identity(&identity) 2335 .await 2336 .expect("insert identity"); 2337 identities 2338 .save_selected_identity(Some(public_key)) 2339 .await 2340 .expect("selection"); 2341 secrets 2342 .put(&DurableRequestId::new_v7(), public_key, secret) 2343 .await 2344 .expect("credential"); 2345 core.apply_transition(StateTransition::BootstrapRegistry { 2346 identities: vec![identity], 2347 selected: Some(public_key), 2348 }) 2349 .expect("registry"); 2350 2351 assert_eq!( 2352 core.import_secret_key( 2353 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 2354 &identities, 2355 &identities, 2356 &secrets, 2357 &journal, 2358 &FixedClock, 2359 ) 2360 .await 2361 .expect_err("orphan credential must fail") 2362 .code(), 2363 SafeErrorCode::IdentityAlreadyExists 2364 ); 2365 let pending = durable_operation( 2366 DurableOperationKind::Repair, 2367 DurableOperationPhase::IntentRecorded, 2368 public_key, 2369 Some(SignerAvailability::CredentialMissing), 2370 ); 2371 let request_id = pending.request_id().clone(); 2372 let operations = TestDurableRepository::fresh(pending); 2373 assert_eq!( 2374 core.import_secret_key_durable( 2375 &request_id, 2376 core.snapshot().revision().value(), 2377 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 2378 &identities, 2379 &identities, 2380 &secrets, 2381 &operations, 2382 &FixedClock, 2383 ) 2384 .await 2385 .expect_err("orphan durable credential must fail") 2386 .code(), 2387 SafeErrorCode::IdentityAlreadyExists 2388 ); 2389 } 2390 2391 #[tokio::test] 2392 async fn removing_an_active_identity_signs_out_for_legacy_and_durable_requests() { 2393 for durable in [false, true] { 2394 let core = AppCore::in_memory(RelayConfiguration::default()); 2395 let identities = InMemoryIdentityRepository::default(); 2396 let secrets = InMemorySecretStore::default(); 2397 let journal = InMemoryOperationJournal::default(); 2398 core.bootstrap().expect("bootstrap"); 2399 let public_key = core 2400 .import_secret_key( 2401 SecretKeyInput::parse( 2402 "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7" 2403 .to_owned(), 2404 ) 2405 .expect("secret"), 2406 &identities, 2407 &identities, 2408 &secrets, 2409 &journal, 2410 &FixedClock, 2411 ) 2412 .await 2413 .expect("identity") 2414 .identity() 2415 .public_key(); 2416 core.activate_identity( 2417 public_key, 2418 &identities, 2419 &identities, 2420 &EmptyProfiles, 2421 &secrets, 2422 &FixedClock, 2423 ) 2424 .await 2425 .expect("activate identity"); 2426 let token = core 2427 .request_identity_removal(public_key, &FixedClock) 2428 .expect("removal token"); 2429 let snapshot = if durable { 2430 let pending = durable_operation( 2431 DurableOperationKind::Remove, 2432 DurableOperationPhase::IntentRecorded, 2433 public_key, 2434 Some(SignerAvailability::Available), 2435 ); 2436 let request_id = pending.request_id().clone(); 2437 let operations = TestDurableRepository::fresh(pending); 2438 core.confirm_identity_removal_durable( 2439 &request_id, 2440 token, 2441 &identities, 2442 &identities, 2443 &secrets, 2444 &operations, 2445 &FixedClock, 2446 ) 2447 .await 2448 .expect("durable removal") 2449 } else { 2450 core.confirm_identity_removal( 2451 token, 2452 &identities, 2453 &identities, 2454 &secrets, 2455 &journal, 2456 &FixedClock, 2457 ) 2458 .await 2459 .expect("removal") 2460 }; 2461 assert_eq!(snapshot.session(), SessionState::SignedOut); 2462 } 2463 } 2464 2465 #[tokio::test] 2466 async fn identity_transaction_compensates_a_journal_phase_failure() { 2467 let core = AppCore::in_memory(RelayConfiguration::default()); 2468 let identities = InMemoryIdentityRepository::default(); 2469 let secrets = InMemorySecretStore::default(); 2470 let journal = FailingUpdateJournal::default(); 2471 core.bootstrap().expect("bootstrap"); 2472 2473 assert_eq!( 2474 core.generate_identity(&identities, &identities, &secrets, &journal, &FixedClock) 2475 .await 2476 .err() 2477 .expect("journal failure must be returned") 2478 .code(), 2479 SafeErrorCode::StorageUnavailable 2480 ); 2481 assert!(identities.list_identities().await.unwrap().is_empty()); 2482 } 2483 }