use keyring::{Entry, Error};
use thiserror::Error;
use tracing::warn;
const SERVICE_NAME: &str = "pawan";
const USER: &str = "api_keys";
#[derive(Error, Debug)]
pub enum CredentialError {
#[error("Failed to access credential store: {0}")]
StoreError(String),
#[error("Credential not found")]
NotFound,
#[error("Invalid credential data")]
InvalidData,
}
impl From<Error> for CredentialError {
fn from(err: Error) -> Self {
match err {
Error::PlatformFailure(e) => CredentialError::StoreError(format!("Platform error: {}", e)),
Error::NoEntry => CredentialError::NotFound,
_ => CredentialError::StoreError(format!("Credential store error: {}", err)),
}
}
}
pub fn store_api_key(key_name: &str, api_key: &str) -> Result<(), CredentialError> {
let entry = Entry::new(SERVICE_NAME, &format!("{}_{}", USER, key_name))?;
entry.set_password(api_key)?;
warn!("API key '{}' stored securely", key_name);
Ok(())
}
pub fn get_api_key(key_name: &str) -> Result<Option<String>, CredentialError> {
let entry = Entry::new(SERVICE_NAME, &format!("{}_{}", USER, key_name))?;
match entry.get_password() {
Ok(key) => Ok(Some(key)),
Err(Error::NoEntry) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn delete_api_key(key_name: &str) -> Result<(), CredentialError> {
let entry = Entry::new(SERVICE_NAME, &format!("{}_{}", USER, key_name))?;
match entry.delete_credential() {
Ok(()) => {
warn!("API key '{}' deleted from secure store", key_name);
Ok(())
}
Err(Error::NoEntry) => Ok(()),
Err(e) => Err(e.into()),
}
}
pub fn is_secure_store_available() -> bool {
let test_entry = Entry::new(SERVICE_NAME, "test_check");
test_entry.is_ok()
}
pub fn store_nvidia_api_key(key: &str) -> Result<(), CredentialError> {
store_api_key("nvidia_api_key", key)
}
pub fn get_nvidia_api_key() -> Result<Option<String>, CredentialError> {
get_api_key("nvidia_api_key")
}
pub fn delete_nvidia_api_key() -> Result<(), CredentialError> {
delete_api_key("nvidia_api_key")
}
pub fn store_openai_api_key(key: &str) -> Result<(), CredentialError> {
store_api_key("openai_api_key", key)
}
pub fn get_openai_api_key() -> Result<Option<String>, CredentialError> {
get_api_key("openai_api_key")
}
pub fn delete_openai_api_key() -> Result<(), CredentialError> {
delete_api_key("openai_api_key")
}
#[cfg(test)]
mod tests {
use super::*;
use keyring::Error;
#[test]
fn test_credential_error_store_error() {
let err = CredentialError::StoreError("test error".to_string());
assert_eq!(err.to_string(), "Failed to access credential store: test error");
}
#[test]
fn test_credential_error_not_found() {
let err = CredentialError::NotFound;
assert_eq!(err.to_string(), "Credential not found");
}
#[test]
fn test_credential_error_invalid_data() {
let err = CredentialError::InvalidData;
assert_eq!(err.to_string(), "Invalid credential data");
}
#[test]
fn test_from_error_platform_failure() {
let platform_err = Error::PlatformFailure(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"DBus error"
)));
let cred_err: CredentialError = platform_err.into();
match cred_err {
CredentialError::StoreError(msg) => {
assert!(msg.contains("Platform error:"));
assert!(msg.contains("DBus error"));
}
_ => panic!("Expected StoreError variant"),
}
}
#[test]
fn test_from_error_no_entry() {
let no_entry_err = Error::NoEntry;
let cred_err: CredentialError = no_entry_err.into();
match cred_err {
CredentialError::NotFound => {}
_ => panic!("Expected NotFound variant"),
}
}
#[test]
fn test_from_error_invalid_credential() {
let bad_encoding_err = Error::BadEncoding(vec![0xFF, 0xFE]);
let cred_err: CredentialError = bad_encoding_err.into();
match cred_err {
CredentialError::StoreError(msg) => {
assert!(msg.contains("Credential store error:"));
}
_ => panic!("Expected StoreError variant"),
}
}
#[test]
fn test_from_error_no_storage_access() {
let no_storage_err = Error::NoStorageAccess(Box::new(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"Access denied"
)));
let cred_err: CredentialError = no_storage_err.into();
match cred_err {
CredentialError::StoreError(msg) => {
assert!(msg.contains("Credential store error:"));
}
_ => panic!("Expected StoreError variant"),
}
}
#[test]
fn test_from_error_too_long() {
let too_long_err = Error::TooLong("username".to_string(), 255);
let cred_err: CredentialError = too_long_err.into();
match cred_err {
CredentialError::StoreError(msg) => {
assert!(msg.contains("Credential store error:"));
}
_ => panic!("Expected StoreError variant"),
}
}
#[test]
fn test_is_secure_store_available_returns_bool() {
let result = is_secure_store_available();
assert!(result == true || result == false);
}
#[test]
fn test_nvidia_key_names_are_consistent() {
let nvidia_key = "nvidia_api_key";
assert_eq!(nvidia_key, "nvidia_api_key");
}
#[test]
fn test_openai_key_names_are_consistent() {
let openai_key = "openai_api_key";
assert_eq!(openai_key, "openai_api_key");
}
#[test]
fn test_service_and_user_constants() {
assert_eq!(SERVICE_NAME, "pawan");
assert_eq!(USER, "api_keys");
}
#[test]
fn test_key_name_formatting() {
let key_name = "test_key";
let formatted = format!("{}_{}", USER, key_name);
assert_eq!(formatted, "api_keys_test_key");
}
#[test]
#[ignore] fn test_store_and_get_key() {
let key_name = "test_key_12345";
let test_key = "test_api_key_value";
store_api_key(key_name, test_key).expect("Failed to store key");
let retrieved = get_api_key(key_name).expect("Failed to retrieve key");
assert_eq!(retrieved, Some(test_key.to_string()));
delete_api_key(key_name).expect("Failed to delete key");
}
#[test]
#[ignore] fn test_get_nonexistent_key() {
let key_name = "nonexistent_key_12345";
let _ = delete_api_key(key_name);
let retrieved = get_api_key(key_name).expect("Failed to retrieve key");
assert_eq!(retrieved, None);
}
#[test]
#[ignore] fn test_delete_key() {
let key_name = "test_delete_key_12345";
let test_key = "test_key_value";
store_api_key(key_name, test_key).expect("Failed to store");
assert!(get_api_key(key_name).expect("Failed to get") == Some(test_key.to_string()));
delete_api_key(key_name).expect("Failed to delete");
assert_eq!(get_api_key(key_name).expect("Failed to get"), None);
}
#[test]
#[ignore] fn test_delete_nonexistent_key_succeeds() {
let key_name = "nonexistent_delete_key_12345";
let result = delete_api_key(key_name);
assert!(result.is_ok());
}
#[test]
#[ignore] fn test_nvidia_convenience_functions() {
let test_key = "nv-test-key-12345";
store_nvidia_api_key(test_key).expect("Failed to store nvidia key");
let retrieved = get_nvidia_api_key().expect("Failed to get nvidia key");
assert_eq!(retrieved, Some(test_key.to_string()));
delete_nvidia_api_key().expect("Failed to delete nvidia key");
assert_eq!(get_nvidia_api_key().expect("Failed to get"), None);
}
#[test]
#[ignore] fn test_openai_convenience_functions() {
let test_key = "oa-test-key-12345";
store_openai_api_key(test_key).expect("Failed to store openai key");
let retrieved = get_openai_api_key().expect("Failed to get openai key");
assert_eq!(retrieved, Some(test_key.to_string()));
delete_openai_api_key().expect("Failed to delete openai key");
assert_eq!(get_openai_api_key().expect("Failed to get"), None);
}
}