fix: use base64 crate, restrict token file perms, safer chrono_now
Some checks are pending
PR Check / rust (push) Waiting to run
PR Check / frontend (push) Waiting to run
PR Check / rust (pull_request) Successful in 17m32s
PR Check / frontend (pull_request) Successful in 2m15s

- 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) <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-04-10 14:58:10 -04:00
parent be5f6a55c5
commit 9e26ad58d1
3 changed files with 32 additions and 41 deletions

1
src-tauri/Cargo.lock generated
View file

@ -4284,6 +4284,7 @@ version = "0.6.7"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"argon2", "argon2",
"base64 0.22.1",
"ed25519-dalek", "ed25519-dalek",
"encoding_rs", "encoding_rs",
"hostname", "hostname",

View file

@ -42,6 +42,7 @@ reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["macros"] } tokio = { version = "1", features = ["macros"] }
hostname = "0.4" hostname = "0.4"
urlencoding = "2" urlencoding = "2"
base64 = "0.22"
[dev-dependencies] [dev-dependencies]
# Used in license_commands.rs tests to sign test JWTs. We avoid the `pem` # Used in license_commands.rs tests to sign test JWTs. We avoid the `pem`

View file

@ -15,7 +15,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::PathBuf; use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Mutex; use std::sync::Mutex;
use tauri::Manager; use tauri::Manager;
@ -72,6 +73,29 @@ fn auth_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
Ok(dir) 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) { fn generate_pkce() -> (String, String) {
use rand::Rng; use rand::Rng;
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@ -91,41 +115,8 @@ fn generate_pkce() -> (String, String) {
} }
fn base64_url_encode(data: &[u8]) -> String { fn base64_url_encode(data: &[u8]) -> String {
use base64_encode::encode; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
encode(data) URL_SAFE_NO_PAD.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
}
} }
/// Start the OAuth2 PKCE flow. Generates a code verifier/challenge, stores the verifier /// 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 dir = auth_dir(&app)?;
let tokens_json = let tokens_json =
serde_json::to_string_pretty(&tokens).map_err(|e| format!("Serialize error: {}", e))?; serde_json::to_string_pretty(&tokens).map_err(|e| format!("Serialize error: {}", e))?;
fs::write(dir.join(TOKENS_FILE), tokens_json) write_restricted(&dir.join(TOKENS_FILE), &tokens_json)?;
.map_err(|e| format!("Cannot write tokens: {}", e))?;
// Fetch user info // Fetch user info
let account = fetch_userinfo(&endpoint, &access_token).await?; let account = fetch_userinfo(&endpoint, &access_token).await?;
@ -285,8 +275,7 @@ pub async fn refresh_auth_token(app: tauri::AppHandle) -> Result<AccountInfo, St
let tokens_json = serde_json::to_string_pretty(&new_tokens) let tokens_json = serde_json::to_string_pretty(&new_tokens)
.map_err(|e| format!("Serialize error: {}", e))?; .map_err(|e| format!("Serialize error: {}", e))?;
fs::write(&tokens_path, tokens_json) write_restricted(&tokens_path, &tokens_json)?;
.map_err(|e| format!("Cannot write tokens: {}", e))?;
let account = fetch_userinfo(&endpoint, &new_access).await?; let account = fetch_userinfo(&endpoint, &new_access).await?;
let account_json = let account_json =
@ -400,6 +389,6 @@ async fn fetch_userinfo(endpoint: &str, access_token: &str) -> Result<AccountInf
fn chrono_now() -> i64 { fn chrono_now() -> i64 {
std::time::SystemTime::now() std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap() .unwrap_or_default()
.as_secs() as i64 .as_secs() as i64
} }