diff --git a/src/github/client.rs b/src/github/client.rs index 7f98169..1df9ec1 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,22 @@ 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<()> {