Skip to main content

pawan/
credentials.rs

1//! Secure credential storage using OS-native keyring.
2//!
3//! This module provides a secure way to store and retrieve API keys
4//! using the operating system's native credential store:
5//! - Linux: libsecret or KWallet
6//! - macOS: Keychain
7//! - Windows: Credential Manager
8
9use keyring::{Entry, Error};
10use thiserror::Error;
11use tracing::warn;
12
13const SERVICE_NAME: &str = "pawan";
14const USER: &str = "api_keys";
15
16/// Errors that can occur during credential operations.
17#[derive(Error, Debug)]
18pub enum CredentialError {
19    #[error("Failed to access credential store: {0}")]
20    StoreError(String),
21
22    #[error("Credential not found")]
23    NotFound,
24
25    #[error("Invalid credential data")]
26    InvalidData,
27}
28
29impl From<Error> for CredentialError {
30    fn from(err: Error) -> Self {
31        match err {
32            Error::PlatformFailure(e) => CredentialError::StoreError(format!("Platform error: {}", e)),
33            Error::NoEntry => CredentialError::NotFound,
34            _ => CredentialError::StoreError(format!("Credential store error: {}", err)),
35        }
36    }
37}
38
39/// Securely stores an API key in the OS-native credential store.
40///
41/// # Arguments
42/// * `key_name` - The name of the key (e.g., "nvidia_api_key", "openai_api_key")
43/// * `api_key` - The API key to store
44///
45/// # Returns
46/// * `Ok(())` if the key was stored successfully
47/// * `Err(CredentialError)` if storage failed
48pub fn store_api_key(key_name: &str, api_key: &str) -> Result<(), CredentialError> {
49    let entry = Entry::new(SERVICE_NAME, &format!("{}_{}", USER, key_name))?;
50    entry.set_password(api_key)?;
51    warn!("API key '{}' stored securely", key_name);
52    Ok(())
53}
54
55/// Retrieves an API key from the OS-native credential store.
56///
57/// # Arguments
58/// * `key_name` - The name of the key (e.g., "nvidia_api_key", "openai_api_key")
59///
60/// # Returns
61/// * `Ok(Some(String))` if the key was found
62/// * `Ok(None)` if the key was not found
63/// * `Err(CredentialError)` if retrieval failed
64pub fn get_api_key(key_name: &str) -> Result<Option<String>, CredentialError> {
65    let entry = Entry::new(SERVICE_NAME, &format!("{}_{}", USER, key_name))?;
66    
67    match entry.get_password() {
68        Ok(key) => Ok(Some(key)),
69        Err(Error::NoEntry) => Ok(None),
70        Err(e) => Err(e.into()),
71    }
72}
73
74/// Deletes an API key from the OS-native credential store.
75///
76/// # Arguments
77/// * `key_name` - The name of the key to delete
78///
79/// # Returns
80/// * `Ok(())` if the key was deleted successfully or didn't exist
81/// * `Err(CredentialError)` if deletion failed
82pub fn delete_api_key(key_name: &str) -> Result<(), CredentialError> {
83    let entry = Entry::new(SERVICE_NAME, &format!("{}_{}", USER, key_name))?;
84    
85    match entry.delete_credential() {
86        Ok(()) => {
87            warn!("API key '{}' deleted from secure store", key_name);
88            Ok(())
89        }
90        Err(Error::NoEntry) => Ok(()),
91        Err(e) => Err(e.into()),
92    }
93}
94
95/// Checks if a secure credential store is available on this system.
96///
97/// # Returns
98/// * `true` if a credential store is available
99/// * `false` if no credential store is available
100pub fn is_secure_store_available() -> bool {
101    let test_entry = Entry::new(SERVICE_NAME, "test_check");
102    test_entry.is_ok()
103}
104
105/// Convenience function for NVIDIA API key operations.
106pub fn store_nvidia_api_key(key: &str) -> Result<(), CredentialError> {
107    store_api_key("nvidia_api_key", key)
108}
109
110pub fn get_nvidia_api_key() -> Result<Option<String>, CredentialError> {
111    get_api_key("nvidia_api_key")
112}
113
114pub fn delete_nvidia_api_key() -> Result<(), CredentialError> {
115    delete_api_key("nvidia_api_key")
116}
117
118/// Convenience function for OpenAI API key operations.
119pub fn store_openai_api_key(key: &str) -> Result<(), CredentialError> {
120    store_api_key("openai_api_key", key)
121}
122
123pub fn get_openai_api_key() -> Result<Option<String>, CredentialError> {
124    get_api_key("openai_api_key")
125}
126
127pub fn delete_openai_api_key() -> Result<(), CredentialError> {
128    delete_api_key("openai_api_key")
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    #[ignore] // Requires a working credential store
137    fn test_store_and_get_key() {
138        let key_name = "test_key_12345";
139        let test_key = "test_api_key_value";
140
141        // Store the key
142        store_api_key(key_name, test_key).expect("Failed to store key");
143
144        // Retrieve the key
145        let retrieved = get_api_key(key_name).expect("Failed to retrieve key");
146        assert_eq!(retrieved, Some(test_key.to_string()));
147
148        // Clean up
149        delete_api_key(key_name).expect("Failed to delete key");
150    }
151
152    #[test]
153    #[ignore] // Requires a working credential store
154    fn test_get_nonexistent_key() {
155        let key_name = "nonexistent_key_12345";
156        
157        // Delete to ensure clean state
158        let _ = delete_api_key(key_name);
159
160        // Try to retrieve
161        let retrieved = get_api_key(key_name).expect("Failed to retrieve key");
162        assert_eq!(retrieved, None);
163    }
164
165    #[test]
166    #[ignore] // Requires a working credential store
167    fn test_delete_key() {
168        let key_name = "test_delete_key_12345";
169        let test_key = "test_key_value";
170
171        // Store and verify
172        store_api_key(key_name, test_key).expect("Failed to store");
173        assert!(get_api_key(key_name).expect("Failed to get") == Some(test_key.to_string()));
174
175        // Delete and verify
176        delete_api_key(key_name).expect("Failed to delete");
177        assert_eq!(get_api_key(key_name).expect("Failed to get"), None);
178    }
179}