secrets.rs (23348B)
1 use std::collections::BTreeMap; 2 use std::sync::{Mutex, MutexGuard}; 3 4 use harvestcircle_domain::{PublicKey, SafeError, SafeErrorCode, SafeMessage, SecretKeyInput}; 5 use secrecy::{ExposeSecret, SecretString}; 6 7 use crate::{BoxFuture, DurableRequestId}; 8 9 pub trait SecretStore: Send + Sync { 10 /// Stores a credential under its canonical public key without overwriting. 11 /// 12 /// # Errors 13 /// 14 /// Returns a safe duplicate or keyring error without exposing the credential. 15 fn put<'a>( 16 &'a self, 17 request_id: &'a DurableRequestId, 18 public_key: PublicKey, 19 secret: SecretKeyInput, 20 ) -> BoxFuture<'a, Result<(), SafeError>>; 21 /// Verifies the original request and full canonical secret without changing custody. 22 /// 23 /// # Errors 24 /// 25 /// Returns a safe conflict, missing-credential, or keyring error. Adapters without 26 /// request-bound verification fail closed rather than loading an unbound credential. 27 fn verify<'a>( 28 &'a self, 29 _request_id: &'a DurableRequestId, 30 _public_key: PublicKey, 31 secret: SecretKeyInput, 32 ) -> BoxFuture<'a, Result<(), SafeError>> { 33 Box::pin(async move { 34 drop(secret); 35 Err(keyring_unavailable()) 36 }) 37 } 38 /// Loads a credential into a non-cloneable redacted boundary value. 39 /// 40 /// # Errors 41 /// 42 /// Returns a safe missing-credential or keyring error. 43 fn load(&self, public_key: PublicKey) -> BoxFuture<'_, Result<SecretKeyInput, SafeError>>; 44 /// Reports whether a credential exists without exposing it. 45 /// 46 /// # Errors 47 /// 48 /// Returns a safe keyring error when availability cannot be determined. 49 fn contains(&self, public_key: PublicKey) -> BoxFuture<'_, Result<bool, SafeError>>; 50 /// Deletes a credential without affecting public identity metadata. 51 /// 52 /// # Errors 53 /// 54 /// Returns a safe missing-credential or keyring error. 55 fn delete<'a>( 56 &'a self, 57 request_id: &'a DurableRequestId, 58 public_key: PublicKey, 59 ) -> BoxFuture<'a, Result<(), SafeError>>; 60 } 61 62 #[derive(Default)] 63 pub struct InMemorySecretStore { 64 credentials: Mutex<BTreeMap<PublicKey, StoredCredential>>, 65 } 66 67 struct StoredCredential { 68 request_id: DurableRequestId, 69 secret: SecretString, 70 } 71 72 #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] 73 pub enum SecretStoreOperation { 74 Put, 75 Verify, 76 Load, 77 Contains, 78 Delete, 79 } 80 81 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 82 pub struct SecretStoreCall { 83 operation: SecretStoreOperation, 84 public_key: PublicKey, 85 } 86 87 impl SecretStoreCall { 88 #[must_use] 89 pub const fn operation(self) -> SecretStoreOperation { 90 self.operation 91 } 92 93 #[must_use] 94 pub const fn public_key(self) -> PublicKey { 95 self.public_key 96 } 97 } 98 99 #[derive(Default)] 100 pub struct FailureSecretStore { 101 inner: InMemorySecretStore, 102 remaining_failures: Mutex<BTreeMap<SecretStoreOperation, usize>>, 103 calls: Mutex<Vec<SecretStoreCall>>, 104 } 105 106 impl FailureSecretStore { 107 pub fn fail_next(&self, operation: SecretStoreOperation) { 108 if let Ok(mut failures) = self.remaining_failures.lock() { 109 *failures.entry(operation).or_default() += 1; 110 } 111 } 112 113 #[must_use] 114 pub fn calls(&self) -> Vec<SecretStoreCall> { 115 self.calls 116 .lock() 117 .map(|calls| calls.clone()) 118 .unwrap_or_default() 119 } 120 121 fn record_and_should_fail( 122 &self, 123 operation: SecretStoreOperation, 124 public_key: PublicKey, 125 ) -> bool { 126 let Ok(mut calls) = self.calls.lock() else { 127 return true; 128 }; 129 calls.push(SecretStoreCall { 130 operation, 131 public_key, 132 }); 133 drop(calls); 134 let Ok(mut failures) = self.remaining_failures.lock() else { 135 return true; 136 }; 137 let remaining = failures.entry(operation).or_default(); 138 let should_fail = *remaining > 0; 139 *remaining = remaining.saturating_sub(1); 140 should_fail 141 } 142 } 143 144 impl SecretStore for FailureSecretStore { 145 fn put<'a>( 146 &'a self, 147 request_id: &'a DurableRequestId, 148 public_key: PublicKey, 149 secret: SecretKeyInput, 150 ) -> BoxFuture<'a, Result<(), SafeError>> { 151 Box::pin(async move { 152 if self.record_and_should_fail(SecretStoreOperation::Put, public_key) { 153 return Err(keyring_unavailable()); 154 } 155 self.inner.put(request_id, public_key, secret).await 156 }) 157 } 158 159 fn verify<'a>( 160 &'a self, 161 request_id: &'a DurableRequestId, 162 public_key: PublicKey, 163 secret: SecretKeyInput, 164 ) -> BoxFuture<'a, Result<(), SafeError>> { 165 Box::pin(async move { 166 if self.record_and_should_fail(SecretStoreOperation::Verify, public_key) { 167 return Err(keyring_unavailable()); 168 } 169 self.inner.verify(request_id, public_key, secret).await 170 }) 171 } 172 173 fn load(&self, public_key: PublicKey) -> BoxFuture<'_, Result<SecretKeyInput, SafeError>> { 174 Box::pin(async move { 175 if self.record_and_should_fail(SecretStoreOperation::Load, public_key) { 176 return Err(keyring_unavailable()); 177 } 178 self.inner.load(public_key).await 179 }) 180 } 181 182 fn contains(&self, public_key: PublicKey) -> BoxFuture<'_, Result<bool, SafeError>> { 183 Box::pin(async move { 184 if self.record_and_should_fail(SecretStoreOperation::Contains, public_key) { 185 return Err(keyring_unavailable()); 186 } 187 self.inner.contains(public_key).await 188 }) 189 } 190 191 fn delete<'a>( 192 &'a self, 193 request_id: &'a DurableRequestId, 194 public_key: PublicKey, 195 ) -> BoxFuture<'a, Result<(), SafeError>> { 196 Box::pin(async move { 197 if self.record_and_should_fail(SecretStoreOperation::Delete, public_key) { 198 return Err(keyring_unavailable()); 199 } 200 self.inner.delete(request_id, public_key).await 201 }) 202 } 203 } 204 205 impl InMemorySecretStore { 206 fn credentials( 207 &self, 208 ) -> Result<MutexGuard<'_, BTreeMap<PublicKey, StoredCredential>>, SafeError> { 209 self.credentials.lock().map_err(|_| keyring_unavailable()) 210 } 211 } 212 213 impl SecretStore for InMemorySecretStore { 214 fn put<'a>( 215 &'a self, 216 request_id: &'a DurableRequestId, 217 public_key: PublicKey, 218 secret: SecretKeyInput, 219 ) -> BoxFuture<'a, Result<(), SafeError>> { 220 Box::pin(async move { 221 let mut credentials = self.credentials()?; 222 if credentials.contains_key(&public_key) { 223 return Err(credential_exists()); 224 } 225 let value = secret.with_exposed_secret(ToOwned::to_owned); 226 credentials.insert( 227 public_key, 228 StoredCredential { 229 request_id: request_id.clone(), 230 secret: SecretString::from(value), 231 }, 232 ); 233 Ok(()) 234 }) 235 } 236 237 fn verify<'a>( 238 &'a self, 239 request_id: &'a DurableRequestId, 240 public_key: PublicKey, 241 secret: SecretKeyInput, 242 ) -> BoxFuture<'a, Result<(), SafeError>> { 243 Box::pin(async move { 244 let credentials = self.credentials()?; 245 let existing = credentials 246 .get(&public_key) 247 .ok_or_else(credential_missing)?; 248 if existing.request_id != *request_id 249 || !secret 250 .with_exposed_secret(|expected| existing.secret.expose_secret() == expected) 251 { 252 return Err(replay_conflict()); 253 } 254 Ok(()) 255 }) 256 } 257 258 fn load(&self, public_key: PublicKey) -> BoxFuture<'_, Result<SecretKeyInput, SafeError>> { 259 Box::pin(async move { 260 let credentials = self.credentials()?; 261 let secret = credentials 262 .get(&public_key) 263 .ok_or_else(credential_missing)?; 264 SecretKeyInput::parse(secret.secret.expose_secret().to_owned()) 265 .map_err(|_| credential_missing()) 266 }) 267 } 268 269 fn contains(&self, public_key: PublicKey) -> BoxFuture<'_, Result<bool, SafeError>> { 270 Box::pin(async move { Ok(self.credentials()?.contains_key(&public_key)) }) 271 } 272 273 fn delete<'a>( 274 &'a self, 275 _request_id: &'a DurableRequestId, 276 public_key: PublicKey, 277 ) -> BoxFuture<'a, Result<(), SafeError>> { 278 Box::pin(async move { 279 self.credentials()? 280 .remove(&public_key) 281 .map(|_| ()) 282 .ok_or_else(credential_missing) 283 }) 284 } 285 } 286 287 const fn replay_conflict() -> SafeError { 288 SafeError::new( 289 SafeErrorCode::InvalidApplicationState, 290 SafeMessage::new("The identity operation conflicts with the stored credential."), 291 ) 292 } 293 294 const fn credential_exists() -> SafeError { 295 SafeError::new( 296 SafeErrorCode::IdentityAlreadyExists, 297 SafeMessage::new("The Nostr identity credential already exists."), 298 ) 299 } 300 301 const fn credential_missing() -> SafeError { 302 SafeError::new( 303 SafeErrorCode::CredentialMissing, 304 SafeMessage::new("The Nostr identity credential is missing."), 305 ) 306 } 307 308 const fn keyring_unavailable() -> SafeError { 309 SafeError::new( 310 SafeErrorCode::KeyringUnavailable, 311 SafeMessage::new("The operating system credential store is unavailable."), 312 ) 313 } 314 315 #[cfg(test)] 316 mod tests { 317 use crate::{BoxFuture, DurableRequestId}; 318 319 use harvestcircle_domain::{PublicKey, SafeError, SafeErrorCode, SecretKeyInput}; 320 321 use super::{FailureSecretStore, InMemorySecretStore, SecretStore, SecretStoreOperation}; 322 323 const SECRET: &str = "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7"; 324 325 fn request_id() -> DurableRequestId { 326 DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000000301").expect("request") 327 } 328 329 struct UnverifiedSecretStore(InMemorySecretStore); 330 331 impl SecretStore for UnverifiedSecretStore { 332 fn put<'a>( 333 &'a self, 334 request_id: &'a DurableRequestId, 335 public_key: PublicKey, 336 secret: SecretKeyInput, 337 ) -> BoxFuture<'a, Result<(), SafeError>> { 338 self.0.put(request_id, public_key, secret) 339 } 340 341 fn load(&self, public_key: PublicKey) -> BoxFuture<'_, Result<SecretKeyInput, SafeError>> { 342 self.0.load(public_key) 343 } 344 345 fn contains(&self, public_key: PublicKey) -> BoxFuture<'_, Result<bool, SafeError>> { 346 self.0.contains(public_key) 347 } 348 349 fn delete<'a>( 350 &'a self, 351 request_id: &'a DurableRequestId, 352 public_key: PublicKey, 353 ) -> BoxFuture<'a, Result<(), SafeError>> { 354 self.0.delete(request_id, public_key) 355 } 356 } 357 358 #[tokio::test] 359 async fn default_secret_verification_fails_closed_through_object_safe_port() { 360 let store = UnverifiedSecretStore(InMemorySecretStore::default()); 361 let port: &dyn SecretStore = &store; 362 let public_key = PublicKey::from_bytes([7; 32]).expect("public key"); 363 port.put( 364 &request_id(), 365 public_key, 366 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 367 ) 368 .await 369 .expect("put"); 370 let error = port 371 .verify( 372 &request_id(), 373 public_key, 374 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 375 ) 376 .await 377 .expect_err("unbound adapter must fail closed"); 378 let retained = port.load(public_key).await.expect("retained credential"); 379 assert_eq!(error.code(), SafeErrorCode::KeyringUnavailable); 380 assert!(retained.with_exposed_secret(|value| value == SECRET)); 381 assert!(!format!("{error:?}").contains(SECRET)); 382 } 383 384 #[tokio::test] 385 async fn memory_secret_verification_binds_request_and_full_secret_without_mutation() { 386 let store = InMemorySecretStore::default(); 387 let request = request_id(); 388 let another_request = DurableRequestId::new_v7(); 389 let public_key = PublicKey::from_bytes([7; 32]).expect("public key"); 390 store 391 .put( 392 &request, 393 public_key, 394 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 395 ) 396 .await 397 .expect("put"); 398 let exact = store 399 .verify( 400 &request, 401 public_key, 402 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 403 ) 404 .await; 405 let changed_request = store 406 .verify( 407 &another_request, 408 public_key, 409 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 410 ) 411 .await 412 .expect_err("original request required"); 413 let changed_secret = store 414 .verify( 415 &request, 416 public_key, 417 SecretKeyInput::parse( 418 "0000000000000000000000000000000000000000000000000000000000000001".to_owned(), 419 ) 420 .expect("different secret"), 421 ) 422 .await 423 .expect_err("full secret required"); 424 let missing = store 425 .verify( 426 &request, 427 PublicKey::from_bytes([8; 32]).expect("other public key"), 428 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 429 ) 430 .await 431 .expect_err("missing credential"); 432 let retained = store.load(public_key).await.expect("retained credential"); 433 let still_exact = store 434 .verify( 435 &request, 436 public_key, 437 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 438 ) 439 .await; 440 assert!(exact.is_ok()); 441 assert!(still_exact.is_ok()); 442 assert_eq!( 443 changed_request.code(), 444 SafeErrorCode::InvalidApplicationState 445 ); 446 assert_eq!( 447 changed_secret.code(), 448 SafeErrorCode::InvalidApplicationState 449 ); 450 assert_eq!(missing.code(), SafeErrorCode::CredentialMissing); 451 assert!(retained.with_exposed_secret(|value| value == SECRET)); 452 assert_eq!(store.credentials().expect("credentials").len(), 1); 453 assert!(!format!("{changed_request:?} {changed_secret:?} {missing:?}").contains(SECRET)); 454 } 455 456 #[tokio::test] 457 async fn memory_secret_verification_fails_closed_on_poison() { 458 let store = InMemorySecretStore::default(); 459 let public_key = PublicKey::from_bytes([7; 32]).expect("public key"); 460 store 461 .put( 462 &request_id(), 463 public_key, 464 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 465 ) 466 .await 467 .expect("put"); 468 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 469 let _credentials = store.credentials.lock().expect("credentials lock"); 470 panic!("injected custody failure"); 471 })); 472 let error = store 473 .verify( 474 &request_id(), 475 public_key, 476 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 477 ) 478 .await 479 .expect_err("poison must fail closed"); 480 assert!(panic.is_err()); 481 assert_eq!(error.code(), SafeErrorCode::KeyringUnavailable); 482 assert!(!format!("{error:?}").contains(SECRET)); 483 } 484 485 #[tokio::test] 486 async fn failure_secret_verification_audits_only_public_identity_and_preserves_custody() { 487 let store = FailureSecretStore::default(); 488 let request = request_id(); 489 let public_key = PublicKey::from_bytes([7; 32]).expect("public key"); 490 store 491 .put( 492 &request, 493 public_key, 494 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 495 ) 496 .await 497 .expect("put"); 498 store.fail_next(SecretStoreOperation::Verify); 499 let unavailable = store 500 .verify( 501 &request, 502 public_key, 503 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 504 ) 505 .await 506 .expect_err("injected verification failure"); 507 let exact = store 508 .verify( 509 &request, 510 public_key, 511 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 512 ) 513 .await; 514 let conflict = store 515 .verify( 516 &DurableRequestId::new_v7(), 517 public_key, 518 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 519 ) 520 .await 521 .expect_err("request mismatch"); 522 let retained = store 523 .inner 524 .load(public_key) 525 .await 526 .expect("retained credential"); 527 let calls = store.calls(); 528 assert_eq!(unavailable.code(), SafeErrorCode::KeyringUnavailable); 529 assert!(exact.is_ok()); 530 assert_eq!(conflict.code(), SafeErrorCode::InvalidApplicationState); 531 assert!(retained.with_exposed_secret(|value| value == SECRET)); 532 assert_eq!( 533 calls 534 .iter() 535 .map(|call| call.operation()) 536 .collect::<Vec<_>>(), 537 vec![ 538 SecretStoreOperation::Put, 539 SecretStoreOperation::Verify, 540 SecretStoreOperation::Verify, 541 SecretStoreOperation::Verify, 542 ] 543 ); 544 assert!(calls.iter().all(|call| call.public_key() == public_key)); 545 let public_evidence = format!("{calls:?} {unavailable:?} {conflict:?}"); 546 assert!(!public_evidence.contains(SECRET)); 547 assert!(!public_evidence.contains(request.as_str())); 548 } 549 550 #[tokio::test] 551 async fn secret_store_puts_loads_checks_and_deletes_redacted_credentials() { 552 let store = InMemorySecretStore::default(); 553 let public_key = PublicKey::from_bytes([7; 32]).expect("valid public key"); 554 assert!(!store.contains(public_key).await.expect("contains")); 555 store 556 .put( 557 &request_id(), 558 public_key, 559 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 560 ) 561 .await 562 .expect("put"); 563 assert!(store.contains(public_key).await.expect("contains")); 564 let loaded = store.load(public_key).await.expect("load"); 565 assert_eq!(loaded.with_exposed_secret(str::len), 64); 566 store 567 .delete(&request_id(), public_key) 568 .await 569 .expect("delete"); 570 assert!(!store.contains(public_key).await.expect("contains")); 571 } 572 573 #[tokio::test] 574 async fn secret_store_rejects_duplicates_and_reports_missing_credentials() { 575 let store = InMemorySecretStore::default(); 576 let public_key = PublicKey::from_bytes([7; 32]).expect("valid public key"); 577 let Err(missing) = store.load(public_key).await else { 578 panic!("missing credential was returned"); 579 }; 580 assert_eq!(missing.code(), SafeErrorCode::CredentialMissing); 581 store 582 .put( 583 &request_id(), 584 public_key, 585 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 586 ) 587 .await 588 .expect("put"); 589 let duplicate = store 590 .put( 591 &request_id(), 592 public_key, 593 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 594 ) 595 .await 596 .expect_err("duplicate"); 597 assert_eq!(duplicate.code(), SafeErrorCode::IdentityAlreadyExists); 598 store 599 .delete(&request_id(), public_key) 600 .await 601 .expect("delete"); 602 let missing = store 603 .delete(&request_id(), public_key) 604 .await 605 .expect_err("missing delete"); 606 assert_eq!(missing.code(), SafeErrorCode::CredentialMissing); 607 } 608 609 #[tokio::test] 610 async fn failure_secret_store_injects_each_boundary_without_mutating_state() { 611 let store = FailureSecretStore::default(); 612 let public_key = PublicKey::from_bytes([7; 32]).expect("valid public key"); 613 store.fail_next(SecretStoreOperation::Put); 614 let error = store 615 .put( 616 &request_id(), 617 public_key, 618 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 619 ) 620 .await 621 .expect_err("put failure"); 622 assert_eq!(error.code(), SafeErrorCode::KeyringUnavailable); 623 assert!(!store.contains(public_key).await.expect("not written")); 624 625 store 626 .put( 627 &request_id(), 628 public_key, 629 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 630 ) 631 .await 632 .expect("put"); 633 for operation in [ 634 SecretStoreOperation::Verify, 635 SecretStoreOperation::Load, 636 SecretStoreOperation::Contains, 637 SecretStoreOperation::Delete, 638 ] { 639 store.fail_next(operation); 640 let error = match operation { 641 SecretStoreOperation::Verify => { 642 store 643 .verify( 644 &request_id(), 645 public_key, 646 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 647 ) 648 .await 649 } 650 SecretStoreOperation::Load => store.load(public_key).await.map(|_| ()), 651 SecretStoreOperation::Contains => store.contains(public_key).await.map(|_| ()), 652 SecretStoreOperation::Delete => store.delete(&request_id(), public_key).await, 653 SecretStoreOperation::Put => unreachable!("put tested separately"), 654 } 655 .expect_err("injected failure"); 656 assert_eq!(error.code(), SafeErrorCode::KeyringUnavailable); 657 } 658 assert!( 659 store 660 .contains(public_key) 661 .await 662 .expect("credential retained") 663 ); 664 } 665 666 #[tokio::test] 667 async fn failure_secret_store_call_log_contains_only_public_identity() { 668 let store = FailureSecretStore::default(); 669 let public_key = PublicKey::from_bytes([7; 32]).expect("valid public key"); 670 store 671 .put( 672 &request_id(), 673 public_key, 674 SecretKeyInput::parse(SECRET.to_owned()).expect("secret"), 675 ) 676 .await 677 .expect("put"); 678 let calls = store.calls(); 679 assert_eq!(calls[0].operation(), SecretStoreOperation::Put); 680 assert_eq!(calls[0].public_key(), public_key); 681 assert!(!format!("{calls:?}").contains(SECRET)); 682 } 683 }