use anyhow::{Context, Result};
use keyring::Entry;
use tracing::{debug, warn};
use zeroize::Zeroizing;
use crate::traits::KeyStore;
const SERVICE_NAME: &str = "brainwires-cli";
const API_KEY_ACCOUNT: &str = "api_key";
pub struct KeyringKeyStore;
impl KeyringKeyStore {
pub fn new() -> Self {
Self
}
}
impl Default for KeyringKeyStore {
fn default() -> Self {
Self::new()
}
}
impl KeyStore for KeyringKeyStore {
fn store_key(&self, user_id: &str, key: &str) -> Result<()> {
store_api_key(user_id, key)
}
fn get_key(&self, user_id: &str) -> Result<Option<Zeroizing<String>>> {
get_api_key(user_id)
}
fn delete_key(&self, user_id: &str) -> Result<()> {
delete_api_key(user_id)
}
fn is_available(&self) -> bool {
is_keyring_available()
}
}
pub fn store_api_key(user_id: &str, api_key: &str) -> Result<()> {
let account = format!("{}:{}", API_KEY_ACCOUNT, user_id);
let entry = Entry::new(SERVICE_NAME, &account).context("Failed to create keyring entry")?;
entry
.set_password(api_key)
.context("Failed to store API key in keyring")?;
debug!("API key stored in system keyring for user {}", user_id);
Ok(())
}
pub fn get_api_key(user_id: &str) -> Result<Option<Zeroizing<String>>> {
let account = format!("{}:{}", API_KEY_ACCOUNT, user_id);
let entry = Entry::new(SERVICE_NAME, &account).context("Failed to create keyring entry")?;
match entry.get_password() {
Ok(key) => {
debug!("API key retrieved from system keyring for user {}", user_id);
Ok(Some(Zeroizing::new(key)))
}
Err(keyring::Error::NoEntry) => {
debug!("No API key found in keyring for user {}", user_id);
Ok(None)
}
Err(e) => {
warn!("Failed to retrieve API key from keyring: {}", e);
Err(e).context("Failed to retrieve API key from keyring")
}
}
}
pub fn delete_api_key(user_id: &str) -> Result<()> {
let account = format!("{}:{}", API_KEY_ACCOUNT, user_id);
let entry = Entry::new(SERVICE_NAME, &account).context("Failed to create keyring entry")?;
match entry.delete_credential() {
Ok(()) => {
debug!("API key deleted from system keyring for user {}", user_id);
Ok(())
}
Err(keyring::Error::NoEntry) => {
Ok(())
}
Err(e) => {
warn!("Failed to delete API key from keyring: {}", e);
Err(e).context("Failed to delete API key from keyring")
}
}
}
pub fn is_keyring_available() -> bool {
match Entry::new(SERVICE_NAME, "test_availability") {
Ok(_) => true,
Err(e) => {
debug!("Keyring not available: {}", e);
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "requires system keyring"]
fn test_store_and_retrieve_api_key() {
let test_user = "test-user-keyring-bridge-1";
let test_key = "bw_test_12345678901234567890123456789012";
let _ = delete_api_key(test_user);
store_api_key(test_user, test_key).expect("Failed to store key");
let retrieved = get_api_key(test_user)
.expect("Failed to retrieve key")
.expect("Key not found");
assert_eq!(retrieved.as_str(), test_key);
delete_api_key(test_user).expect("Failed to delete key");
}
#[test]
#[ignore = "requires system keyring"]
fn test_keystore_trait() {
let store = KeyringKeyStore::new();
let test_user = "test-user-keyring-bridge-trait";
let test_key = "bw_test_00000000000000000000000000000000";
let _ = store.delete_key(test_user);
store.store_key(test_user, test_key).unwrap();
let retrieved = store.get_key(test_user).unwrap().unwrap();
assert_eq!(retrieved.as_str(), test_key);
store.delete_key(test_user).unwrap();
let gone = store.get_key(test_user).unwrap();
assert!(gone.is_none());
}
#[test]
fn test_is_keyring_available() {
let _available = is_keyring_available();
}
}