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 = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
"base64 0.22.1",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"hostname",
|
"hostname",
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue