From 9e26ad58d1f552b80b41c01e28d2f5b46440058a Mon Sep 17 00:00:00 2001 From: le king fu Date: Fri, 10 Apr 2026 14:58:10 -0400 Subject: [PATCH] fix: use base64 crate, restrict token file perms, safer chrono_now - Replace hand-rolled base64 encoder with base64::URL_SAFE_NO_PAD crate - Set 0600 permissions on tokens.json via write_restricted() helper (Unix) - Replace chrono_now() .unwrap() with .unwrap_or_default() Co-Authored-By: Claude Opus 4.6 (1M context) --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/commands/auth_commands.rs | 71 +++++++++++-------------- 3 files changed, 32 insertions(+), 41 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 53bac46..a9dda2f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4284,6 +4284,7 @@ version = "0.6.7" dependencies = [ "aes-gcm", "argon2", + "base64 0.22.1", "ed25519-dalek", "encoding_rs", "hostname", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 77ccd85..41fda00 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -42,6 +42,7 @@ reqwest = { version = "0.12", features = ["json"] } tokio = { version = "1", features = ["macros"] } hostname = "0.4" urlencoding = "2" +base64 = "0.22" [dev-dependencies] # Used in license_commands.rs tests to sign test JWTs. We avoid the `pem` diff --git a/src-tauri/src/commands/auth_commands.rs b/src-tauri/src/commands/auth_commands.rs index 67c1152..de5988f 100644 --- a/src-tauri/src/commands/auth_commands.rs +++ b/src-tauri/src/commands/auth_commands.rs @@ -15,7 +15,8 @@ use serde::{Deserialize, Serialize}; use std::fs; -use std::path::PathBuf; +use std::io::Write; +use std::path::{Path, PathBuf}; use std::sync::Mutex; use tauri::Manager; @@ -72,6 +73,29 @@ fn auth_dir(app: &tauri::AppHandle) -> Result { Ok(dir) } +/// Write a file with restricted permissions (0600 on Unix) for sensitive data like tokens. +fn write_restricted(path: &Path, contents: &str) -> Result<(), String> { + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + let mut file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(path) + .map_err(|e| format!("Cannot write {}: {}", path.display(), e))?; + file.write_all(contents.as_bytes()) + .map_err(|e| format!("Cannot write {}: {}", path.display(), e))?; + } + #[cfg(not(unix))] + { + fs::write(path, contents) + .map_err(|e| format!("Cannot write {}: {}", path.display(), e))?; + } + Ok(()) +} + fn generate_pkce() -> (String, String) { use rand::Rng; let mut rng = rand::thread_rng(); @@ -91,41 +115,8 @@ fn generate_pkce() -> (String, String) { } fn base64_url_encode(data: &[u8]) -> String { - use base64_encode::encode; - encode(data) - .replace('+', "-") - .replace('/', "_") - .trim_end_matches('=') - .to_string() -} - -// Simple base64 encoding without external dependency -mod base64_encode { - const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - - pub fn encode(data: &[u8]) -> String { - let mut result = String::new(); - for chunk in data.chunks(3) { - let b0 = chunk[0] as u32; - let b1 = chunk.get(1).copied().unwrap_or(0) as u32; - let b2 = chunk.get(2).copied().unwrap_or(0) as u32; - let triple = (b0 << 16) | (b1 << 8) | b2; - - result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char); - result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char); - if chunk.len() > 1 { - result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char); - } else { - result.push('='); - } - if chunk.len() > 2 { - result.push(CHARS[(triple & 0x3F) as usize] as char); - } else { - result.push('='); - } - } - result - } + use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; + URL_SAFE_NO_PAD.encode(data) } /// Start the OAuth2 PKCE flow. Generates a code verifier/challenge, stores the verifier @@ -208,8 +199,7 @@ pub async fn handle_auth_callback(app: tauri::AppHandle, code: String) -> Result let dir = auth_dir(&app)?; let tokens_json = serde_json::to_string_pretty(&tokens).map_err(|e| format!("Serialize error: {}", e))?; - fs::write(dir.join(TOKENS_FILE), tokens_json) - .map_err(|e| format!("Cannot write tokens: {}", e))?; + write_restricted(&dir.join(TOKENS_FILE), &tokens_json)?; // Fetch user info let account = fetch_userinfo(&endpoint, &access_token).await?; @@ -285,8 +275,7 @@ pub async fn refresh_auth_token(app: tauri::AppHandle) -> Result Result i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .unwrap_or_default() .as_secs() as i64 }