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) <noreply@anthropic.com>
This commit is contained in:
parent
be5f6a55c5
commit
9e26ad58d1
3 changed files with 32 additions and 41 deletions
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
|
|
@ -4284,6 +4284,7 @@ version = "0.6.7"
|
|||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
"encoding_rs",
|
||||
"hostname",
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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<PathBuf, String> {
|
|||
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<AccountInfo, St
|
|||
|
||||
let tokens_json = serde_json::to_string_pretty(&new_tokens)
|
||||
.map_err(|e| format!("Serialize error: {}", e))?;
|
||||
fs::write(&tokens_path, tokens_json)
|
||||
.map_err(|e| format!("Cannot write tokens: {}", e))?;
|
||||
write_restricted(&tokens_path, &tokens_json)?;
|
||||
|
||||
let account = fetch_userinfo(&endpoint, &new_access).await?;
|
||||
let account_json =
|
||||
|
|
@ -400,6 +389,6 @@ async fn fetch_userinfo(endpoint: &str, access_token: &str) -> Result<AccountInf
|
|||
fn chrono_now() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue