origin_secrets_system/
lib.rs1use async_trait::async_trait;
13use keyring::Entry;
14use origin_domain::{AppError, Result};
15use origin_secrets::{Secret, SecretKey, SecretStore};
16
17#[derive(Debug, Clone)]
22pub struct SystemSecretStore {
23 service_prefix: String,
24}
25
26impl SystemSecretStore {
27 pub fn new(service_prefix: impl Into<String>) -> Self {
28 Self {
29 service_prefix: service_prefix.into(),
30 }
31 }
32
33 fn entry(&self, key: &SecretKey) -> Result<Entry> {
34 let service = format!("{}.{}", self.service_prefix, key.namespace());
35 Entry::new(&service, key.name()).map_err(to_app_error)
36 }
37}
38
39#[async_trait]
40impl SecretStore for SystemSecretStore {
41 async fn get(&self, key: &SecretKey) -> Result<Option<Secret>> {
42 let entry = self.entry(key)?;
43 blocking(move || match entry.get_password() {
44 Ok(password) => Ok(Some(Secret::new(password))),
45 Err(keyring::Error::NoEntry) => Ok(None),
47 Err(error) => Err(to_app_error(error)),
48 })
49 .await
50 }
51
52 async fn set(&self, key: &SecretKey, value: Secret) -> Result<()> {
53 let entry = self.entry(key)?;
54 blocking(move || entry.set_password(value.expose()).map_err(to_app_error)).await
55 }
56
57 async fn delete(&self, key: &SecretKey) -> Result<()> {
58 let entry = self.entry(key)?;
59 blocking(move || match entry.delete_credential() {
60 Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
61 Err(error) => Err(to_app_error(error)),
62 })
63 .await
64 }
65}
66
67async fn blocking<T, F>(operation: F) -> Result<T>
70where
71 T: Send + 'static,
72 F: FnOnce() -> Result<T> + Send + 'static,
73{
74 tokio::task::spawn_blocking(operation)
75 .await
76 .map_err(|error| AppError::internal(format!("credential task failed: {error}")))?
77}
78
79fn to_app_error(error: keyring::Error) -> AppError {
84 match error {
85 keyring::Error::NoEntry => AppError::Authentication("no credential stored".to_owned()),
86 keyring::Error::NoDefaultStore => {
87 AppError::Configuration("no credential store available on this system".to_owned())
88 }
89 keyring::Error::Ambiguous(_) => AppError::Storage(
90 "several credentials matched — the credential store needs manual cleanup".to_owned(),
91 ),
92 keyring::Error::PlatformFailure(inner) => {
93 AppError::Storage(format!("credential store unavailable: {inner}"))
94 }
95 keyring::Error::NoStorageAccess(inner) => {
96 AppError::Permission(format!("credential store access denied: {inner}"))
97 }
98 other => AppError::Storage(other.to_string()),
99 }
100}