1use tracing::{error, info};
2
3use super::core::{
4 public_key_hex, CloudHomeCredentials, DeviceIdentityCustody, KeyError, UserKeypair,
5 SIGN_SECRETKEYBYTES,
6};
7
8#[derive(Debug, thiserror::Error)]
10pub enum MasterKeyError {
11 #[error("a master key is already established for this store")]
14 AlreadyEstablished,
15 #[error("cannot import a master key while a cloud home is connected")]
16 CloudHomeConnected,
17 #[error("key error: {0}")]
18 Key(#[from] KeyError),
19 #[error("invalid master key material: {0}")]
20 Encryption(#[from] crate::encryption::EncryptionError),
21}
22
23#[derive(Debug, thiserror::Error)]
25pub enum IdentityError {
26 #[error("an identity is already established for this store")]
31 AlreadyEstablished,
32 #[error("key error: {0}")]
33 Key(#[from] KeyError),
34}
35
36struct KeyringService {
37 name: String,
38 worker: KeyringWorker,
39}
40
41struct KeyringWorker {
45 operations: Option<std::sync::mpsc::Sender<KeyringOperation>>,
46 thread: Option<std::thread::JoinHandle<()>>,
47}
48
49enum KeyringOperation {
50 Read {
51 account: String,
52 reply: std::sync::mpsc::SyncSender<Result<Option<String>, KeyError>>,
53 },
54 Write {
55 account: String,
56 value: String,
57 reply: std::sync::mpsc::SyncSender<Result<(), KeyError>>,
58 },
59 Delete {
60 account: String,
61 reply: std::sync::mpsc::SyncSender<Result<bool, KeyError>>,
62 },
63 #[cfg(all(
64 any(test, feature = "test-utils"),
65 any(target_os = "macos", target_os = "ios")
66 ))]
67 AppleEntryFacts {
68 account: String,
69 reply: std::sync::mpsc::SyncSender<Result<AppleKeyringEntryFacts, KeyError>>,
70 },
71 #[cfg(any(test, feature = "test-utils"))]
72 SetNextError {
73 account: String,
74 error: keyring_core::Error,
75 reply: std::sync::mpsc::SyncSender<Result<(), KeyError>>,
76 },
77}
78
79struct KeyringBackend {
80 name: String,
81 store: std::sync::Arc<keyring_core::CredentialStore>,
82}
83
84impl KeyringWorker {
85 const STACK_SIZE: usize = 16 * 1024 * 1024;
88
89 fn start(backend: KeyringBackend) -> Result<Self, KeyError> {
90 let (operations, receiver) = std::sync::mpsc::channel();
91 let thread = std::thread::Builder::new()
92 .name("coven-keyring".to_string())
93 .stack_size(Self::STACK_SIZE)
94 .spawn(move || {
95 while let Ok(operation) = receiver.recv() {
96 backend.execute(operation);
97 }
98 })
99 .map_err(KeyError::KeyringWorkerStart)?;
100 Ok(Self {
101 operations: Some(operations),
102 thread: Some(thread),
103 })
104 }
105
106 fn execute<T: Send + 'static>(
107 &self,
108 operation_name: &'static str,
109 operation: impl FnOnce(std::sync::mpsc::SyncSender<Result<T, KeyError>>) -> KeyringOperation,
110 ) -> Result<T, KeyError> {
111 let (reply, receiver) = std::sync::mpsc::sync_channel(1);
112 self.operations
113 .as_ref()
114 .expect("the keyring worker sender exists until drop")
115 .send(operation(reply))
116 .map_err(|_| KeyError::KeyringWorkerStopped {
117 operation: operation_name,
118 })?;
119 receiver
120 .recv()
121 .map_err(|_| KeyError::KeyringWorkerStopped {
122 operation: operation_name,
123 })?
124 }
125}
126
127impl Drop for KeyringWorker {
128 fn drop(&mut self) {
129 self.operations.take();
130 if let Some(thread) = self.thread.take() {
131 if thread.join().is_err() {
132 error!("Keyring worker terminated with a panic");
133 }
134 }
135 }
136}
137
138#[derive(Clone, Copy)]
139enum KeyringBinding {
140 Registered(&'static KeyringService),
141 Unregistered,
142}
143
144impl KeyringBinding {
145 fn service(self) -> Result<&'static KeyringService, KeyError> {
146 match self {
147 Self::Registered(service) => Ok(service),
148 Self::Unregistered => Err(KeyError::ServiceNotRegistered),
149 }
150 }
151}
152
153static KEYRING_SERVICE: std::sync::OnceLock<KeyringService> = std::sync::OnceLock::new();
154
155pub fn set_keyring_service(name: impl Into<String>) -> Result<(), KeyError> {
163 crate::keyring_backend::install_platform_store()?;
164 let name = name.into();
165 let store = keyring_core::get_default_store().ok_or(KeyError::StoreNotInstalled)?;
166 let service = KeyringService::new(name.clone(), store)?;
167 if KEYRING_SERVICE.set(service).is_err() {
168 let registered = KEYRING_SERVICE
169 .get()
170 .map(|service| service.name.as_str())
171 .expect("a keyring service is registered when set() fails");
172 if registered != name {
173 return Err(KeyError::ServiceAlreadyRegistered {
174 registered: registered.to_string(),
175 requested: name,
176 });
177 }
178 }
179 Ok(())
180}
181
182pub fn keyring_service() -> Result<&'static str, KeyError> {
186 KEYRING_SERVICE
187 .get()
188 .map(|service| service.name.as_str())
189 .ok_or(KeyError::ServiceNotRegistered)
190}
191
192fn registered_keyring() -> Result<&'static KeyringService, KeyError> {
193 KEYRING_SERVICE.get().ok_or(KeyError::ServiceNotRegistered)
194}
195
196#[cfg(any(target_os = "macos", target_os = "ios"))]
199const ERR_SEC_MISSING_ENTITLEMENT: i32 = -34018;
200#[cfg(any(target_os = "macos", target_os = "ios"))]
204const ERR_SEC_INTERACTION_NOT_ALLOWED: i32 = -25308;
205
206fn map_keyring_error(e: keyring_core::Error) -> KeyError {
207 #[cfg(any(target_os = "macos", target_os = "ios"))]
208 match keychain_os_status(&e) {
209 Some(ERR_SEC_MISSING_ENTITLEMENT) => return KeyError::MissingKeychainEntitlement,
210 Some(ERR_SEC_INTERACTION_NOT_ALLOWED) => return KeyError::KeychainTemporarilyUnavailable,
211 _ => {}
212 }
213 match e {
214 keyring_core::Error::NoDefaultStore => KeyError::StoreNotInstalled,
215 other => KeyError::Keyring(other),
216 }
217}
218
219#[cfg(any(target_os = "macos", target_os = "ios"))]
232fn keychain_os_status(e: &keyring_core::Error) -> Option<i32> {
233 let keyring_core::Error::PlatformFailure(inner) = e else {
234 return None;
235 };
236 inner
237 .downcast_ref::<security_framework::base::Error>()
238 .map(|err| err.code())
239}
240
241const DEVICE_SIGNING_KEY_BASE: &str = "coven_user_signing_key";
244const ENCRYPTION_MASTER_KEY_BASE: &str = "encryption_master_key";
247const CLOUD_HOME_CREDENTIALS_BASE: &str = "cloud_home_credentials";
250const PENDING_IDENTITY_BASE: &str = "coven_pending_identity";
253
254pub(crate) const RESERVED_HOST_SECRET_NAMES: &[&str] = &[
260 DEVICE_SIGNING_KEY_BASE,
261 ENCRYPTION_MASTER_KEY_BASE,
262 CLOUD_HOME_CREDENTIALS_BASE,
263 PENDING_IDENTITY_BASE,
264];
265
266pub(crate) enum KeyringSlot {
275 DeviceSigningKey(String),
277 EncryptionMasterKey(String),
279 CloudHomeCredentials(String),
281 PendingIdentity(String),
284 HostSecret { name: String, store_id: String },
288}
289
290impl KeyringSlot {
291 pub(crate) fn account(&self) -> String {
298 match self {
299 KeyringSlot::DeviceSigningKey(store_id) => {
300 format!("{DEVICE_SIGNING_KEY_BASE}:{store_id}")
301 }
302 KeyringSlot::EncryptionMasterKey(store_id) => {
303 format!("{ENCRYPTION_MASTER_KEY_BASE}:{store_id}")
304 }
305 KeyringSlot::CloudHomeCredentials(store_id) => {
306 format!("{CLOUD_HOME_CREDENTIALS_BASE}:{store_id}")
307 }
308 KeyringSlot::PendingIdentity(pending_public_key_hex) => {
309 format!("{PENDING_IDENTITY_BASE}:{pending_public_key_hex}")
310 }
311 KeyringSlot::HostSecret { name, store_id } => format!("{name}:{store_id}"),
312 }
313 }
314}
315
316pub(crate) fn validate_host_secret_name(name: &str) -> Result<(), KeyError> {
322 if name.is_empty() {
323 return Err(KeyError::InvalidSecretName {
324 name: name.to_string(),
325 reason: "a host secret name must not be empty".to_string(),
326 });
327 }
328 if name.contains(':') {
329 return Err(KeyError::InvalidSecretName {
330 name: name.to_string(),
331 reason: "a host secret name must not contain ':', the keyring account scheme's \
332 separator"
333 .to_string(),
334 });
335 }
336 if RESERVED_HOST_SECRET_NAMES.contains(&name) {
337 return Err(KeyError::InvalidSecretName {
338 name: name.to_string(),
339 reason: "reserved for coven's own keyring entries".to_string(),
340 });
341 }
342 Ok(())
343}
344
345impl KeyringBackend {
350 fn execute(&self, operation: KeyringOperation) {
351 match operation {
352 KeyringOperation::Read { account, reply } => {
353 drop(reply.send(self.read(account)));
354 }
355 KeyringOperation::Write {
356 account,
357 value,
358 reply,
359 } => {
360 drop(reply.send(self.write(account, value)));
361 }
362 KeyringOperation::Delete { account, reply } => {
363 drop(reply.send(self.delete(account)));
364 }
365 #[cfg(all(
366 any(test, feature = "test-utils"),
367 any(target_os = "macos", target_os = "ios")
368 ))]
369 KeyringOperation::AppleEntryFacts { account, reply } => {
370 drop(reply.send(self.apple_entry_facts(account)));
371 }
372 #[cfg(any(test, feature = "test-utils"))]
373 KeyringOperation::SetNextError {
374 account,
375 error,
376 reply,
377 } => {
378 drop(reply.send(self.set_next_error(account, error)));
379 }
380 }
381 }
382
383 fn entry(&self, account: &str) -> Result<keyring_core::Entry, KeyError> {
384 self.store
385 .build(&self.name, account, None)
386 .map_err(map_keyring_error)
387 }
388
389 fn read(&self, account: String) -> Result<Option<String>, KeyError> {
390 let entry = self.entry(&account)?;
391 match entry.get_password() {
392 Ok(password) if password.is_empty() => Err(KeyError::EmptyKeyringEntry { account }),
393 Ok(password) => Ok(Some(password)),
394 Err(keyring_core::Error::NoEntry) => Ok(None),
395 Err(error) => Err(map_keyring_error(error)),
396 }
397 }
398
399 fn write(&self, account: String, value: String) -> Result<(), KeyError> {
400 self.entry(&account)?
401 .set_password(&value)
402 .map_err(map_keyring_error)
403 }
404
405 fn delete(&self, account: String) -> Result<bool, KeyError> {
406 match self.entry(&account)?.delete_credential() {
407 Ok(()) => Ok(true),
408 Err(keyring_core::Error::NoEntry) => Ok(false),
409 Err(error) => Err(map_keyring_error(error)),
410 }
411 }
412
413 #[cfg(all(
414 any(test, feature = "test-utils"),
415 any(target_os = "macos", target_os = "ios")
416 ))]
417 fn apple_entry_facts(&self, account: String) -> Result<AppleKeyringEntryFacts, KeyError> {
418 let entry = self.entry(&account)?;
419 let credential = entry
420 .as_any()
421 .downcast_ref::<apple_native_keyring_store::protected::Cred>()
422 .ok_or(KeyError::UnexpectedAppleKeyringEntry)?;
423 Ok(AppleKeyringEntryFacts {
424 access_policy: credential.access_policy.clone(),
425 cloud_synchronize: credential.cloud_synchronize,
426 service: credential.service.clone(),
427 account: credential.account.clone(),
428 })
429 }
430
431 #[cfg(any(test, feature = "test-utils"))]
432 fn set_next_error(&self, account: String, error: keyring_core::Error) -> Result<(), KeyError> {
433 let entry = self.entry(&account)?;
434 let credential = entry
435 .as_any()
436 .downcast_ref::<keyring_core::mock::Cred>()
437 .ok_or(KeyError::UnexpectedTestKeyringEntry)?;
438 credential.set_error(error);
439 Ok(())
440 }
441}
442
443impl KeyringService {
444 fn new(
445 name: String,
446 store: std::sync::Arc<keyring_core::CredentialStore>,
447 ) -> Result<Self, KeyError> {
448 Ok(Self {
449 name: name.clone(),
450 worker: KeyringWorker::start(KeyringBackend { name, store })?,
451 })
452 }
453
454 fn read(&self, slot: &KeyringSlot) -> Result<Option<String>, KeyError> {
455 let account = slot.account();
456 self.worker.execute("read a keyring entry", move |reply| {
457 KeyringOperation::Read { account, reply }
458 })
459 }
460
461 fn write(&self, slot: &KeyringSlot, value: &str) -> Result<(), KeyError> {
462 let account = slot.account();
463 let value = value.to_string();
464 self.worker.execute("write a keyring entry", move |reply| {
465 KeyringOperation::Write {
466 account,
467 value,
468 reply,
469 }
470 })
471 }
472
473 fn delete(&self, slot: &KeyringSlot) -> Result<bool, KeyError> {
474 let account = slot.account();
475 self.worker.execute("delete a keyring entry", move |reply| {
476 KeyringOperation::Delete { account, reply }
477 })
478 }
479
480 #[cfg(all(
481 any(test, feature = "test-utils"),
482 any(target_os = "macos", target_os = "ios")
483 ))]
484 fn apple_entry_facts(&self, account: String) -> Result<AppleKeyringEntryFacts, KeyError> {
485 self.worker
486 .execute("inspect an Apple keyring entry", move |reply| {
487 KeyringOperation::AppleEntryFacts { account, reply }
488 })
489 }
490
491 #[cfg(any(test, feature = "test-utils"))]
492 fn set_next_error(
493 &self,
494 slot: &KeyringSlot,
495 error: keyring_core::Error,
496 ) -> Result<(), KeyError> {
497 let account = slot.account();
498 self.worker
499 .execute("configure a test keyring entry", move |reply| {
500 KeyringOperation::SetNextError {
501 account,
502 error,
503 reply,
504 }
505 })
506 }
507}
508
509#[cfg(all(
510 any(test, feature = "test-utils"),
511 any(target_os = "macos", target_os = "ios")
512))]
513pub struct AppleKeyringEntryFacts {
514 pub access_policy: apple_native_keyring_store::protected::AccessPolicy,
515 pub cloud_synchronize: bool,
516 pub service: String,
517 pub account: String,
518}
519
520#[cfg(all(
523 any(test, feature = "test-utils"),
524 any(target_os = "macos", target_os = "ios")
525))]
526pub fn apple_keyring_entry_facts_for_test(
527 account: &str,
528) -> Result<AppleKeyringEntryFacts, KeyError> {
529 registered_keyring()?.apple_entry_facts(account.to_string())
530}
531
532pub fn require_identity(custody: &dyn DeviceIdentityCustody) -> Result<UserKeypair, KeyError> {
537 custody.unlock()?.ok_or(KeyError::NoDeviceIdentity)
538}
539
540pub fn mint_pending_identity() -> Result<UserKeypair, KeyError> {
552 let keypair = UserKeypair::generate();
553 registered_keyring()?.write(
554 &KeyringSlot::PendingIdentity(public_key_hex(&keypair)),
555 &hex::encode(keypair.to_keypair_bytes()),
556 )?;
557 info!("Minted a pending identity for device pairing");
558 Ok(keypair)
559}
560
561pub fn peek_pending_identity(pending_public_key_hex: &str) -> Result<UserKeypair, KeyError> {
567 read_pending_identity_slot(&KeyringSlot::PendingIdentity(
568 pending_public_key_hex.to_string(),
569 ))
570}
571
572fn read_pending_identity_slot(slot: &KeyringSlot) -> Result<UserKeypair, KeyError> {
573 let KeyringSlot::PendingIdentity(pending_public_key_hex) = slot else {
574 unreachable!("read_pending_identity_slot is only ever called with a PendingIdentity slot");
575 };
576 let sk_hex = registered_keyring()?
577 .read(slot)?
578 .ok_or_else(|| KeyError::NoPendingIdentity {
579 pending_public_key_hex: pending_public_key_hex.clone(),
580 })?;
581 let signing_key = hex::decode(&sk_hex).map_err(|source| KeyError::Hex {
582 subject: "pending identity",
583 source,
584 })?;
585 let actual = signing_key.len();
586 let signing_key: [u8; SIGN_SECRETKEYBYTES] =
587 signing_key
588 .try_into()
589 .map_err(|_| KeyError::InvalidLength {
590 subject: "pending identity",
591 expected: SIGN_SECRETKEYBYTES,
592 actual,
593 })?;
594 UserKeypair::from_signing_key_bytes(&signing_key)
595}
596
597pub fn discard_pending_identity(pending_public_key_hex: &str) -> Result<(), KeyError> {
602 registered_keyring()?
603 .delete(&KeyringSlot::PendingIdentity(
604 pending_public_key_hex.to_string(),
605 ))
606 .map(|_| ())
607}
608
609#[derive(Clone)]
615pub struct StoreKeys {
616 keyring: KeyringBinding,
617 store_id: String,
618}
619
620impl StoreKeys {
621 pub fn bind(store_id: String) -> Self {
622 let keyring = match KEYRING_SERVICE.get() {
623 Some(service) => KeyringBinding::Registered(service),
624 None => KeyringBinding::Unregistered,
625 };
626 Self { keyring, store_id }
627 }
628
629 pub fn store_id(&self) -> &str {
630 &self.store_id
631 }
632
633 pub fn get_encryption_key(&self) -> Result<Option<String>, KeyError> {
634 self.keyring
635 .service()?
636 .read(&KeyringSlot::EncryptionMasterKey(self.store_id.clone()))
637 }
638
639 pub fn set_encryption_key(&self, value: &str) -> Result<(), KeyError> {
640 self.keyring.service()?.write(
641 &KeyringSlot::EncryptionMasterKey(self.store_id.clone()),
642 value,
643 )?;
644 info!("Encryption key saved to keyring");
645 Ok(())
646 }
647
648 pub fn delete_encryption_key(&self) -> Result<(), KeyError> {
649 if self
650 .keyring
651 .service()?
652 .delete(&KeyringSlot::EncryptionMasterKey(self.store_id.clone()))?
653 {
654 info!("Encryption key deleted from keyring");
655 }
656 Ok(())
657 }
658
659 pub fn get_cloud_home_credentials(&self) -> Result<Option<CloudHomeCredentials>, KeyError> {
660 match self
661 .keyring
662 .service()?
663 .read(&KeyringSlot::CloudHomeCredentials(self.store_id.clone()))?
664 {
665 None => Ok(None),
666 Some(j) => serde_json::from_str(&j)
667 .map(Some)
668 .map_err(|source| KeyError::Json {
669 operation: "parse cloud home credentials JSON",
670 source,
671 }),
672 }
673 }
674
675 pub fn set_cloud_home_credentials(&self, creds: &CloudHomeCredentials) -> Result<(), KeyError> {
676 let json = serde_json::to_string(creds).map_err(|source| KeyError::Json {
677 operation: "serialize cloud home credentials",
678 source,
679 })?;
680 self.keyring.service()?.write(
681 &KeyringSlot::CloudHomeCredentials(self.store_id.clone()),
682 &json,
683 )?;
684 info!("Cloud home credentials saved to keyring");
685 Ok(())
686 }
687
688 #[cfg(feature = "oauth-providers")]
689 pub fn get_cloud_home_oauth_tokens(
690 &self,
691 ) -> Result<Option<crate::keys::OAuthTokens>, KeyError> {
692 Ok(match self.get_cloud_home_credentials()? {
693 Some(CloudHomeCredentials::OAuth { tokens }) => Some(tokens),
694 _ => None,
695 })
696 }
697
698 #[cfg(feature = "oauth-providers")]
699 pub fn set_cloud_home_oauth_tokens(
700 &self,
701 tokens: &crate::keys::OAuthTokens,
702 ) -> Result<(), KeyError> {
703 self.set_cloud_home_credentials(&CloudHomeCredentials::OAuth {
704 tokens: tokens.clone(),
705 })
706 }
707
708 pub fn delete_cloud_home_credentials(&self) -> Result<(), KeyError> {
709 if self
710 .keyring
711 .service()?
712 .delete(&KeyringSlot::CloudHomeCredentials(self.store_id.clone()))?
713 {
714 info!("Cloud home credentials deleted from keyring");
715 }
716 Ok(())
717 }
718
719 fn host_secret_slot(&self, name: &str) -> KeyringSlot {
720 KeyringSlot::HostSecret {
721 name: name.to_string(),
722 store_id: self.store_id.clone(),
723 }
724 }
725
726 pub fn get_host_secret(&self, name: &str) -> Result<Option<String>, KeyError> {
732 validate_host_secret_name(name)?;
733 self.keyring.service()?.read(&self.host_secret_slot(name))
734 }
735
736 pub fn set_host_secret(&self, name: &str, value: &str) -> Result<(), KeyError> {
739 validate_host_secret_name(name)?;
740 self.keyring
741 .service()?
742 .write(&self.host_secret_slot(name), value)?;
743 info!("Host secret {name:?} saved to keyring");
744 Ok(())
745 }
746
747 pub fn delete_host_secret(&self, name: &str) -> Result<(), KeyError> {
750 validate_host_secret_name(name)?;
751 if self
752 .keyring
753 .service()?
754 .delete(&self.host_secret_slot(name))?
755 {
756 info!("Host secret {name:?} deleted from keyring");
757 }
758 Ok(())
759 }
760
761 #[cfg(test)]
762 pub(crate) fn write_empty_encryption_key_for_test(&self) -> Result<(), KeyError> {
763 self.keyring
764 .service()?
765 .write(&KeyringSlot::EncryptionMasterKey(self.store_id.clone()), "")
766 }
767
768 #[cfg(any(test, feature = "test-utils"))]
769 pub fn write_cloud_home_credentials_json_for_test(&self, json: &str) -> Result<(), KeyError> {
770 self.keyring.service()?.write(
771 &KeyringSlot::CloudHomeCredentials(self.store_id.clone()),
772 json,
773 )
774 }
775
776 #[cfg(any(test, feature = "test-utils"))]
777 pub fn fail_next_cloud_home_credentials_operation_for_test(
778 &self,
779 error: keyring_core::Error,
780 ) -> Result<(), KeyError> {
781 self.keyring.service()?.set_next_error(
782 &KeyringSlot::CloudHomeCredentials(self.store_id.clone()),
783 error,
784 )
785 }
786}
787
788impl DeviceIdentityCustody for StoreKeys {
789 fn unlock(&self) -> Result<Option<UserKeypair>, KeyError> {
790 let slot = KeyringSlot::DeviceSigningKey(self.store_id.clone());
791 let Some(signing_key_hex) = self.keyring.service()?.read(&slot)? else {
792 return Ok(None);
793 };
794 let signing_key = hex::decode(&signing_key_hex).map_err(|source| KeyError::Hex {
795 subject: "signing key",
796 source,
797 })?;
798 let actual = signing_key.len();
799 let signing_key: [u8; SIGN_SECRETKEYBYTES] =
800 signing_key
801 .try_into()
802 .map_err(|_| KeyError::InvalidLength {
803 subject: "signing key",
804 expected: SIGN_SECRETKEYBYTES,
805 actual,
806 })?;
807 Ok(Some(UserKeypair::from_signing_key_bytes(&signing_key)?))
808 }
809
810 fn persist(&self, keypair: &UserKeypair) -> Result<(), KeyError> {
811 self.keyring.service()?.write(
812 &KeyringSlot::DeviceSigningKey(self.store_id.clone()),
813 &hex::encode(keypair.to_keypair_bytes()),
814 )
815 }
816
817 fn forget(&self) -> Result<(), KeyError> {
818 self.keyring
819 .service()?
820 .delete(&KeyringSlot::DeviceSigningKey(self.store_id.clone()))
821 .map(|_| ())
822 }
823}
824
825#[cfg(any(test, feature = "test-utils", debug_assertions))]
830pub mod test_keyring {
831 use std::sync::Once;
832
833 static INSTALL: Once = Once::new();
834
835 pub fn install() {
836 install_for_service("coven-tests").expect("register test keyring service");
837 }
838
839 pub fn install_for_service(service_name: &str) -> Result<(), super::KeyError> {
840 INSTALL.call_once(|| {
841 keyring_core::set_default_store(
845 keyring_core::mock::Store::new().expect("create mock keyring store"),
846 );
847 });
848 super::set_keyring_service(service_name)
849 }
850}
851
852#[cfg(test)]
853#[path = "platform_tests.rs"]
854mod tests;