use alloc::borrow::Cow;
use alloc::format;
use alloc::string::{String, ToString};
use super::{FetchContext, KeyFetch, RawKey};
use crate::Result;
use crate::error::Error;
#[derive(Debug, Clone)]
pub struct KeychainFetch {
service: String,
account: String,
}
impl KeychainFetch {
#[must_use]
pub fn new(service: impl Into<String>, account: impl Into<String>) -> Self {
Self {
service: service.into(),
account: account.into(),
}
}
}
impl KeyFetch for KeychainFetch {
fn fetch(&self, _ctx: &FetchContext) -> Result<RawKey> {
let entry =
keyring::Entry::new(&self.service, &self.account).map_err(|e| Error::Acquisition {
source: Cow::Borrowed("keychain"),
reason: format!(
"could not open keychain entry {}/{}: {}",
self.service,
self.account,
redact_keyring_error(&e),
),
})?;
let value = entry.get_password().map_err(|e| Error::Acquisition {
source: Cow::Borrowed("keychain"),
reason: format!(
"could not read keychain entry {}/{}: {}",
self.service,
self.account,
redact_keyring_error(&e),
),
})?;
Ok(RawKey::new(value.into_bytes()))
}
fn describe(&self) -> Cow<'_, str> {
Cow::Borrowed("keychain")
}
}
fn redact_keyring_error(e: &keyring::Error) -> String {
use keyring::Error;
match e {
Error::NoEntry => "no such entry".to_string(),
Error::BadEncoding(_) => "stored value is not UTF-8".to_string(),
Error::TooLong(field, _) => format!("{field} too long for platform"),
Error::Invalid(field, _) => format!("invalid {field}"),
Error::PlatformFailure(_) => "platform-specific keyring failure".to_string(),
Error::NoStorageAccess(_) => "keyring service inaccessible".to_string(),
Error::Ambiguous(_) => "multiple matching entries (ambiguous)".to_string(),
_ => "keyring error".to_string(),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn describe_returns_keychain() {
let f = KeychainFetch::new("svc", "acct");
assert_eq!(f.describe(), "keychain");
}
#[test]
fn construction_holds_service_and_account() {
let f = KeychainFetch::new("test-service", "test-account");
assert_eq!(f.service, "test-service");
assert_eq!(f.account, "test-account");
}
#[test]
fn live_keychain_test_skipped_when_not_opted_in() {
if std::env::var("KEY_VAULT_KEYCHAIN_TEST").ok().as_deref() != Some("1") {
return; }
let fetcher = KeychainFetch::new("key-vault-test", "ci-key");
let raw = fetcher.fetch(&FetchContext::new("k")).unwrap();
assert!(!raw.is_empty());
}
}