Skip to content

Commit b03f0a1

Browse files
committed
refactor(services): use SecretString for api_key in Config and providers (#3 F-004)
Replace plain String with SecretString to prevent accidental exposure of sensitive API keys in logs or memory dumps. Update AnthropicProvider and OpenAiProvider to use expose_secret() when accessing keys for API requests.
1 parent 536b84a commit b03f0a1

5 files changed

Lines changed: 32 additions & 11 deletions

File tree

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ indicatif = "0.18"
8383
tracing = "0.1"
8484
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
8585

86+
# Secret management
87+
secrecy = { version = "0.10", features = ["serde"] }
88+
8689
# Utilities
8790
regex = "1.12"
8891
globset = "0.4"

src/config.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use directories::ProjectDirs;
66
use figment::Figment;
77
use figment::providers::{Env, Format, Serialized, Toml};
8+
use secrecy::SecretString;
89
use serde::{Deserialize, Serialize};
910
use std::fs;
1011
use std::path::PathBuf;
@@ -74,8 +75,8 @@ pub struct Config {
7475
#[serde(default = "default_ollama_host")]
7576
pub ollama_host: String,
7677

77-
#[serde(default)]
78-
pub api_key: Option<String>,
78+
#[serde(default, skip_serializing)]
79+
pub api_key: Option<SecretString>,
7980

8081
#[serde(default = "default_max_diff_lines")]
8182
pub max_diff_lines: usize,
@@ -316,8 +317,10 @@ impl Config {
316317
// Provider-specific API key fallback (after CLI overrides set the provider)
317318
if config.api_key.is_none() {
318319
config.api_key = match config.provider {
319-
Provider::OpenAI => std::env::var("OPENAI_API_KEY").ok(),
320-
Provider::Anthropic => std::env::var("ANTHROPIC_API_KEY").ok(),
320+
Provider::OpenAI => std::env::var("OPENAI_API_KEY").ok().map(SecretString::from),
321+
Provider::Anthropic => {
322+
std::env::var("ANTHROPIC_API_KEY").ok().map(SecretString::from)
323+
}
321324
Provider::Ollama => None,
322325
};
323326
}
@@ -329,7 +332,7 @@ impl Config {
329332
if let Ok(entry) = keyring::Entry::new("commitbee", &provider_name)
330333
&& let Ok(key) = entry.get_password()
331334
{
332-
config.api_key = Some(key);
335+
config.api_key = Some(SecretString::from(key));
333336
}
334337
}
335338

src/services/llm/anthropic.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use tokio::sync::mpsc;
1010
use tokio_stream::StreamExt;
1111
use tokio_util::sync::CancellationToken;
1212

13+
use secrecy::{ExposeSecret, SecretString};
14+
1315
use crate::config::Config;
1416
use crate::error::{Error, Result};
1517

@@ -22,7 +24,7 @@ pub struct AnthropicProvider {
2224
client: Client,
2325
base_url: String,
2426
model: String,
25-
api_key: String,
27+
api_key: SecretString,
2628
temperature: f32,
2729
max_tokens: u32,
2830
}
@@ -83,7 +85,7 @@ impl AnthropicProvider {
8385
pub async fn verify_connection(&self) -> Result<()> {
8486
// Anthropic doesn't have a lightweight endpoint for verification,
8587
// so we just validate that the key looks plausible
86-
if self.api_key.is_empty() {
88+
if self.api_key.expose_secret().is_empty() {
8789
return Err(Error::Provider {
8890
provider: "anthropic".into(),
8991
message: "API key not configured".into(),
@@ -104,7 +106,7 @@ impl AnthropicProvider {
104106
let response = self
105107
.client
106108
.post(&url)
107-
.header("x-api-key", &self.api_key)
109+
.header("x-api-key", self.api_key.expose_secret())
108110
.header("anthropic-version", API_VERSION)
109111
.header("content-type", "application/json")
110112
.json(&MessagesRequest {

src/services/llm/openai.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use tokio::sync::mpsc;
1010
use tokio_stream::StreamExt;
1111
use tokio_util::sync::CancellationToken;
1212

13+
use secrecy::{ExposeSecret, SecretString};
14+
1315
use crate::config::Config;
1416
use crate::error::{Error, Result};
1517

@@ -21,7 +23,7 @@ pub struct OpenAiProvider {
2123
client: Client,
2224
base_url: String,
2325
model: String,
24-
api_key: String,
26+
api_key: SecretString,
2527
temperature: f32,
2628
max_tokens: u32,
2729
}
@@ -88,7 +90,7 @@ impl OpenAiProvider {
8890
let response = self
8991
.client
9092
.get(&url)
91-
.header("Authorization", format!("Bearer {}", self.api_key))
93+
.header("Authorization", format!("Bearer {}", self.api_key.expose_secret()))
9294
.send()
9395
.await
9496
.map_err(|e| Error::Provider {
@@ -118,7 +120,7 @@ impl OpenAiProvider {
118120
let response = self
119121
.client
120122
.post(&url)
121-
.header("Authorization", format!("Bearer {}", self.api_key))
123+
.header("Authorization", format!("Bearer {}", self.api_key.expose_secret()))
122124
.json(&ChatRequest {
123125
model: self.model.clone(),
124126
messages: vec![

0 commit comments

Comments
 (0)