mattrguerra/cortexmainPrivate on GitHub, published here in full

secrets.rs

Language Rust5.5 KB167 lines

crates/cortex-core/src/secrets.rs

  1. //! Where API keys are kept: the OS keychain, or memory in tests, behind a
  2. //! per-session cache.
  3. use std::collections::HashMap;
  4. use std::sync::{Mutex, MutexGuard, PoisonError};
  5. pub const SERVICE: &str = "com.mattrguerra.cortex";
  6. /// A panic cannot leave these maps half-written, so a poisoned lock is still safe to use.
  7. fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
  8. m.lock().unwrap_or_else(PoisonError::into_inner)
  9. }
  10. /// What people paste is not always just the key: a trailing newline, quotes,
  11. /// a `Bearer ` prefix, or an invisible character copied from a web page.
  12. pub fn clean_key(raw: &str) -> String {
  13. let s: String = raw
  14. .chars()
  15. .filter(|c| !c.is_whitespace() && !matches!(c, '\u{200b}'..='\u{200d}' | '\u{2060}' | '\u{feff}'))
  16. .collect();
  17. let s = s.trim_matches(|c| c == '"' || c == '\'' || c == '`');
  18. let s = s.strip_prefix("Bearer").or_else(|| s.strip_prefix("bearer")).unwrap_or(s);
  19. s.to_string()
  20. }
  21. pub fn account_for(provider_id: &str) -> String {
  22. format!("provider:{provider_id}")
  23. }
  24. pub trait SecretStore: Send + Sync {
  25. fn get(&self, account: &str) -> Result<Option<String>, String>;
  26. fn set(&self, account: &str, secret: &str) -> Result<(), String>;
  27. fn delete(&self, account: &str) -> Result<(), String>;
  28. }
  29. /// For tests and the end-to-end run.
  30. #[derive(Default)]
  31. pub struct MemoryStore(Mutex<HashMap<String, String>>);
  32. impl SecretStore for MemoryStore {
  33. fn get(&self, account: &str) -> Result<Option<String>, String> {
  34. Ok(lock(&self.0).get(account).cloned())
  35. }
  36. fn set(&self, account: &str, secret: &str) -> Result<(), String> {
  37. lock(&self.0).insert(account.into(), secret.into());
  38. Ok(())
  39. }
  40. fn delete(&self, account: &str) -> Result<(), String> {
  41. lock(&self.0).remove(account);
  42. Ok(())
  43. }
  44. }
  45. /// The OS keychain.
  46. #[cfg(feature = "keychain")]
  47. pub struct KeychainStore;
  48. #[cfg(feature = "keychain")]
  49. impl SecretStore for KeychainStore {
  50. fn get(&self, account: &str) -> Result<Option<String>, String> {
  51. let e = keyring::Entry::new(SERVICE, account).map_err(|e| e.to_string())?;
  52. match e.get_password() {
  53. Ok(s) => Ok(Some(s)),
  54. Err(keyring::Error::NoEntry) => Ok(None),
  55. Err(e) => Err(e.to_string()),
  56. }
  57. }
  58. fn set(&self, account: &str, secret: &str) -> Result<(), String> {
  59. keyring::Entry::new(SERVICE, account).and_then(|e| e.set_password(secret)).map_err(|e| e.to_string())
  60. }
  61. fn delete(&self, account: &str) -> Result<(), String> {
  62. let e = keyring::Entry::new(SERVICE, account).map_err(|e| e.to_string())?;
  63. match e.delete_credential() {
  64. Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
  65. Err(e) => Err(e.to_string()),
  66. }
  67. }
  68. }
  69. /// Reads each key from the store once per session (so macOS asks once) and
  70. /// keeps it in memory after that.
  71. pub struct KeyCache {
  72. store: Box<dyn SecretStore>,
  73. cache: Mutex<HashMap<String, Option<String>>>,
  74. }
  75. impl KeyCache {
  76. pub fn new(store: Box<dyn SecretStore>) -> Self {
  77. Self { store, cache: Mutex::new(HashMap::new()) }
  78. }
  79. pub fn get(&self, provider_id: &str) -> Result<Option<String>, String> {
  80. let acct = account_for(provider_id);
  81. if let Some(v) = lock(&self.cache).get(&acct) {
  82. return Ok(v.clone());
  83. }
  84. let v = self.store.get(&acct)?;
  85. lock(&self.cache).insert(acct, v.clone());
  86. Ok(v)
  87. }
  88. pub fn set(&self, provider_id: &str, key: &str) -> Result<(), String> {
  89. let key = clean_key(key);
  90. let key = key.as_str();
  91. let acct = account_for(provider_id);
  92. self.store.set(&acct, key)?;
  93. lock(&self.cache).insert(acct, Some(key.to_string()));
  94. Ok(())
  95. }
  96. pub fn clear(&self, provider_id: &str) -> Result<(), String> {
  97. let acct = account_for(provider_id);
  98. self.store.delete(&acct)?;
  99. lock(&self.cache).insert(acct, None);
  100. Ok(())
  101. }
  102. pub fn has(&self, provider_id: &str) -> Result<bool, String> {
  103. Ok(self.get(provider_id)?.is_some_and(|k| !k.is_empty()))
  104. }
  105. }
  106. #[cfg(test)]
  107. mod tests {
  108. use std::sync::atomic::{AtomicUsize, Ordering};
  109. use std::sync::Arc;
  110. use super::*;
  111. #[test]
  112. fn pasted_keys_are_cleaned() {
  113. for raw in
  114. ["abc123", " abc123\n", "\"abc123\"", "Bearer abc123", "abc\u{200b}123", "'abc123'\r\n", "\u{feff}abc123"]
  115. {
  116. assert_eq!(clean_key(raw), "abc123", "{raw:?}");
  117. }
  118. assert_eq!(clean_key(" \n"), "");
  119. }
  120. struct Counting(MemoryStore, Arc<AtomicUsize>);
  121. impl SecretStore for Counting {
  122. fn get(&self, a: &str) -> Result<Option<String>, String> {
  123. self.1.fetch_add(1, Ordering::SeqCst);
  124. self.0.get(a)
  125. }
  126. fn set(&self, a: &str, s: &str) -> Result<(), String> {
  127. self.0.set(a, s)
  128. }
  129. fn delete(&self, a: &str) -> Result<(), String> {
  130. self.0.delete(a)
  131. }
  132. }
  133. #[test]
  134. fn reads_the_store_once_per_provider() {
  135. let reads = Arc::new(AtomicUsize::new(0));
  136. let c = KeyCache::new(Box::new(Counting(MemoryStore::default(), reads.clone())));
  137. assert!(!c.has("deepinfra").unwrap());
  138. assert!(!c.has("deepinfra").unwrap());
  139. assert_eq!(reads.load(Ordering::SeqCst), 1);
  140. c.set("deepinfra", " key-1 \n").unwrap();
  141. assert_eq!(c.get("deepinfra").unwrap().as_deref(), Some("key-1"));
  142. c.clear("deepinfra").unwrap();
  143. assert!(!c.has("deepinfra").unwrap());
  144. assert_eq!(reads.load(Ordering::SeqCst), 1);
  145. }
  146. }