Skip to main content

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}