fix: credential masking panic and silent token write errors (#81)
Two bugs fixed: 1. `auth_commands.rs` — `gws auth export` masking used `s[..4]` which panics on strings shorter than 4 characters, and `s[s.len().min(4)..]` which evaluates to `s[4..]` on long strings — showing the entire secret instead of masking it. Replaced with a `mask_secret()` helper that safely shows only the first 4 and last 4 characters, or "***" for short strings. 2. `token_storage.rs` — `save_to_disk` silently discarded the return value of `atomic_write_async` with `let _`, causing the function to return `Ok(())` even when the write failed. Token persistence failures now properly propagate via `?`.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"gws": patch
|
||||
---
|
||||
|
||||
fix: credential masking panic and silent token write errors
|
||||
|
||||
Fixed `gws auth export` masking which panicked on short strings and showed
|
||||
the entire secret instead of masking it. Also fixed silent token cache write
|
||||
failures in `save_to_disk` that returned `Ok(())` even when the write failed.
|
||||
+24
-11
@@ -19,6 +19,24 @@ use serde_json::json;
|
||||
use crate::credential_store;
|
||||
use crate::error::GwsError;
|
||||
|
||||
/// Mask a secret string by showing only the first 4 and last 4 characters.
|
||||
/// Strings with 8 or fewer characters are fully replaced with "***".
|
||||
fn mask_secret(s: &str) -> String {
|
||||
const MASK_PREFIX_LEN: usize = 4;
|
||||
const MASK_SUFFIX_LEN: usize = 4;
|
||||
const MIN_LEN_FOR_PARTIAL_MASK: usize = MASK_PREFIX_LEN + MASK_SUFFIX_LEN;
|
||||
|
||||
if s.len() > MIN_LEN_FOR_PARTIAL_MASK {
|
||||
format!(
|
||||
"{}...{}",
|
||||
&s[..MASK_PREFIX_LEN],
|
||||
&s[s.len() - MASK_SUFFIX_LEN..]
|
||||
)
|
||||
} else {
|
||||
"***".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal scopes for first-run login — only core Workspace APIs that never
|
||||
/// trigger Google's `restricted_client` / unverified-app block.
|
||||
///
|
||||
@@ -279,17 +297,12 @@ async fn handle_export(unmasked: bool) -> Result<(), GwsError> {
|
||||
println!("{contents}");
|
||||
} else if let Ok(mut creds) = serde_json::from_str::<serde_json::Value>(&contents) {
|
||||
if let Some(obj) = creds.as_object_mut() {
|
||||
if let Some(serde_json::Value::String(s)) = obj.get("client_secret") {
|
||||
obj.insert(
|
||||
"client_secret".to_string(),
|
||||
json!(format!("{}...{}", &s[..4], &s[s.len().min(4)..])),
|
||||
);
|
||||
}
|
||||
if let Some(serde_json::Value::String(s)) = obj.get("refresh_token") {
|
||||
obj.insert(
|
||||
"refresh_token".to_string(),
|
||||
json!(format!("{}...{}", &s[..4], &s[s.len().min(4)..])),
|
||||
);
|
||||
for key in ["client_secret", "refresh_token"] {
|
||||
if let Some(val) = obj.get_mut(key) {
|
||||
if let Some(s) = val.as_str() {
|
||||
*val = json!(mask_secret(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{}", serde_json::to_string_pretty(&creds).unwrap());
|
||||
|
||||
@@ -61,7 +61,7 @@ impl EncryptedTokenStorage {
|
||||
}
|
||||
|
||||
// Write atomically via a sibling .tmp file + rename.
|
||||
let _ = crate::fs_util::atomic_write_async(&self.file_path, encrypted.as_slice()).await;
|
||||
crate::fs_util::atomic_write_async(&self.file_path, encrypted.as_slice()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user