hyprcorrect_core/secrets.rs
1//! Secret storage via the OS keychain.
2//!
3//! API keys (today only LLM providers) live in libsecret / kwallet on
4//! Linux, the macOS Keychain on macOS, and the Credential Manager on
5//! Windows — never on disk in `config.toml`.
6//!
7//! Entries are keyed by `(service = "hyprcorrect", account = name)`.
8//! `name` is something descriptive like `"llm.anthropic"`.
9
10/// An error talking to the OS keychain.
11#[derive(Debug, thiserror::Error)]
12pub enum SecretError {
13 /// The OS keychain rejected the request — typically a missing
14 /// daemon (libsecret service not running) or a user lock.
15 #[error("keychain: {0}")]
16 Keychain(String),
17}
18
19const SERVICE: &str = "hyprcorrect";
20
21/// Fetch a stored secret. `Ok(None)` means "no entry" (not an error).
22///
23/// # Errors
24///
25/// Returns [`SecretError`] if the OS keychain is unreachable.
26pub fn get(name: &str) -> Result<Option<String>, SecretError> {
27 let entry = entry(name)?;
28 match entry.get_password() {
29 Ok(value) => Ok(Some(value)),
30 Err(keyring::Error::NoEntry) => Ok(None),
31 Err(e) => Err(SecretError::Keychain(e.to_string())),
32 }
33}
34
35/// Store (or overwrite) a secret.
36///
37/// # Errors
38///
39/// Returns [`SecretError`] if the OS keychain is unreachable.
40pub fn set(name: &str, value: &str) -> Result<(), SecretError> {
41 let entry = entry(name)?;
42 entry
43 .set_password(value)
44 .map_err(|e| SecretError::Keychain(e.to_string()))
45}
46
47/// Remove a stored secret. Removing a non-existent entry is not an
48/// error.
49///
50/// # Errors
51///
52/// Returns [`SecretError`] if the OS keychain is unreachable.
53pub fn delete(name: &str) -> Result<(), SecretError> {
54 let entry = entry(name)?;
55 match entry.delete_credential() {
56 Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
57 Err(e) => Err(SecretError::Keychain(e.to_string())),
58 }
59}
60
61fn entry(name: &str) -> Result<keyring::Entry, SecretError> {
62 keyring::Entry::new(SERVICE, name).map_err(|e| SecretError::Keychain(e.to_string()))
63}