app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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 }