From 46ab89c71eab7c9e7afe1fe1d0db2d52ade94507 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 14:05:10 -0500 Subject: [PATCH 1/2] security: fix token encryption implementation and add validation Issues fixed: 1. Remove binary path from key derivation to allow vault relocation 2. Add passphrase strength validation (minimum 8 characters) 3. Mask full API URLs in error messages (shows path only) 4. Add token validation on config command Benefits: - Vault now survives binary relocations - Weak passphrases are warned against - Error messages don't expose full API paths - Token validity checked immediately during config All tests passing. See SECURITY_REVIEW.md for detailed analysis. Co-Authored-By: Claude Haiku 4.5 --- src/github/client.rs | 23 +++++++++++++++++++++-- src/main.rs | 3 ++- src/vault.rs | 31 +++++++++++++++++++------------ 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/github/client.rs b/src/github/client.rs index 7f98169..8058090 100644 --- a/src/github/client.rs +++ b/src/github/client.rs @@ -12,13 +12,14 @@ fn check_status(resp: Response) -> Result { if status.is_success() { return Ok(resp); } - let url = resp.url().to_string(); + let path = resp.url().path().to_string(); let body = resp.text().unwrap_or_default(); let message = serde_json::from_str::(&body) .ok() .and_then(|v| v["message"].as_str().map(String::from)) .unwrap_or(body); - anyhow::bail!("{status} — {message}\n URL: {url}") + let api_path = path.strip_prefix('/').unwrap_or(&path); + anyhow::bail!("{status} — {message}\n API: {api_path}") } impl GithubClient { @@ -107,6 +108,24 @@ impl GithubClient { check_status(resp)?; Ok(()) } + + pub fn validate_scopes(&self) -> Result<()> { + let user = self.get::("/user")?; + + let message = user + .get("message") + .and_then(|m| m.as_str()); + + if let Some(msg) = message { + if msg.contains("API rate limit") { + anyhow::bail!("GitHub API rate limit exceeded"); + } else if msg.contains("Bad credentials") { + anyhow::bail!("GitHub token is invalid or expired"); + } + } + + Ok(()) + } } /// Env var → vault → inline prompt. Returns (token, vault_passphrase). diff --git a/src/main.rs b/src/main.rs index 03be4fb..69723dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,9 +76,10 @@ fn run_config() -> Result<()> { println!(); let (token, _) = vault::prompt_and_save_github_token()?; - // Validate + // Validate token let client = github::client::GithubClient::new(&token); print!(" Validating token... "); + client.validate_scopes()?; let user = github::repo::get_user(&client)?; println!("ok ({})", user.login); diff --git a/src/vault.rs b/src/vault.rs index 1965431..2672eb1 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -20,7 +20,8 @@ pub struct VaultData { pub secrets: HashMap, } -/// Blake2b-256(username ‖ hostname ‖ binary_path ‖ passphrase ‖ domain) +/// Blake2b-256(username ‖ hostname ‖ passphrase ‖ domain) +/// Note: Removed binary_path to allow vault to work even if binary is relocated fn derive_key(passphrase: &str) -> Result<[u8; KEY_LEN]> { let mut hasher = Blake2bVar::new(KEY_LEN).expect("valid output size"); Update::update(&mut hasher, whoami::username().as_bytes()); @@ -30,14 +31,6 @@ fn derive_key(passphrase: &str) -> Result<[u8; KEY_LEN]> { whoami::fallible::hostname().unwrap_or_default().as_bytes(), ); Update::update(&mut hasher, b"|"); - Update::update( - &mut hasher, - std::env::current_exe() - .unwrap_or_default() - .to_string_lossy() - .as_bytes(), - ); - Update::update(&mut hasher, b"|"); Update::update(&mut hasher, passphrase.as_bytes()); Update::update(&mut hasher, DOMAIN_SEPARATOR); @@ -234,9 +227,23 @@ fn ask_optional_passphrase() -> Result { return Ok(String::new()); } - Ok(inquire::Password::new("Passphrase:") - .prompt() - .unwrap_or_default()) + loop { + let passphrase = inquire::Password::new("Passphrase:") + .with_help_message("Minimum 8 characters recommended") + .prompt()?; + + if passphrase.len() < 8 { + println!(" \x1b[33m⚠ Passphrase is weak (less than 8 characters)\x1b[0m"); + let confirm_weak = inquire::Confirm::new("Use this weak passphrase anyway?") + .with_default(false) + .prompt()?; + if !confirm_weak { + continue; + } + } + + return Ok(passphrase); + } } pub fn save_secret(name: &str, value: &str, passphrase: &str) -> Result<()> { From 49dd2ad84dbbf6e2de3005ec45770689f88991d2 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 16:54:07 -0500 Subject: [PATCH 2/2] chore: apply cargo fmt --- src/github/client.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/github/client.rs b/src/github/client.rs index 8058090..1df9ec1 100644 --- a/src/github/client.rs +++ b/src/github/client.rs @@ -112,9 +112,7 @@ impl GithubClient { pub fn validate_scopes(&self) -> Result<()> { let user = self.get::("/user")?; - let message = user - .get("message") - .and_then(|m| m.as_str()); + let message = user.get("message").and_then(|m| m.as_str()); if let Some(msg) = message { if msg.contains("API rate limit") {