Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ async-stream = "0.3"
rfd = "0.15"
if-addrs = "0.13"
zip = { version = "0.6", default-features = false, features = ["deflate"] }
constant_time_eq = "0.3"
5 changes: 2 additions & 3 deletions src-tauri/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use time::OffsetDateTime;

use crate::models::ExtractedAuth;
use crate::models::PreparedOauthLogin;
use crate::utils::private_create_new_options;
use crate::utils::set_private_permissions;
use crate::utils::truncate_for_error;

Expand Down Expand Up @@ -656,9 +657,7 @@ fn write_auth_file_atomically(path: &Path, contents: &[u8]) -> Result<(), String
));

let write_result = (|| -> Result<(), String> {
let mut temp_file = fs::OpenOptions::new()
.create_new(true)
.write(true)
let mut temp_file = private_create_new_options()
.open(&temp_path)
.map_err(|e| format!("创建临时 auth.json 失败 {}: {e}", temp_path.display()))?;
temp_file
Expand Down
4 changes: 3 additions & 1 deletion src-tauri/src/proxy_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ pub async fn run_proxy_daemon(options: ProxyDaemonOptions) -> Result<(), String>
println!("data_dir={}", options.data_dir.display());
println!("listen=http://{}:{port}/v1", options.host);
if let Some(api_key) = status.api_key.as_deref() {
println!("api_key={api_key}");
// 仅打印前 8 位,避免完整 API Key 写入 systemd journal / 日志文件
let preview = &api_key[..api_key.len().min(8)];
println!("api_key_preview={preview}... (full key stored in data_dir/api-proxy.key)");
}
println!("upstream=codex");

Expand Down
11 changes: 6 additions & 5 deletions src-tauri/src/proxy_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ use crate::store::load_store_from_path;
use crate::store::save_store_to_path;
use crate::usage::resolve_chatgpt_base_origin;
use crate::utils::now_unix_seconds;
use crate::utils::private_create_new_options;
use crate::utils::set_private_permissions;
use crate::utils::truncate_for_error;

Expand Down Expand Up @@ -1634,7 +1635,7 @@ fn is_authorized(headers: &HeaderMap, api_key: &str) -> bool {
.get("x-api-key")
.and_then(|value| value.to_str().ok())
{
if value == api_key {
if constant_time_eq::constant_time_eq(value.as_bytes(), api_key.as_bytes()) {
return true;
}
}
Expand All @@ -1644,7 +1645,9 @@ fn is_authorized(headers: &HeaderMap, api_key: &str) -> bool {
.and_then(|value| value.to_str().ok())
{
if let Some(token) = value.strip_prefix("Bearer ") {
return token == api_key;
if constant_time_eq::constant_time_eq(token.as_bytes(), api_key.as_bytes()) {
return true;
}
}
}

Expand Down Expand Up @@ -1776,9 +1779,7 @@ fn write_private_file_atomically(path: &Path, contents: &[u8]) -> Result<(), Str
));

let write_result = (|| -> Result<(), String> {
let mut temp_file = fs::OpenOptions::new()
.create_new(true)
.write(true)
let mut temp_file = private_create_new_options()
.open(&temp_path)
.map_err(|error| {
format!("创建 API Key 临时文件失败 {}: {error}", temp_path.display())
Expand Down
22 changes: 11 additions & 11 deletions src-tauri/src/remote_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1494,18 +1494,18 @@ impl PreparedAuth {
sanitize_service_fragment(&server.id),
now_unix_seconds()
));
fs::write(&temp_path, private_key).map_err(|error| {
format!("写入临时 SSH 私钥文件失败 {}: {error}", temp_path.display())
})?;
#[cfg(unix)]
// 使用 private_create_new_options 确保文件从创建时就是 0o600 权限,
// 消除 fs::write + set_permissions 两步之间的 TOCTOU 窗口
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&temp_path)
.map_err(|error| format!("读取临时 SSH 私钥文件权限失败: {error}"))?
.permissions();
perms.set_mode(0o600);
fs::set_permissions(&temp_path, perms)
.map_err(|error| format!("设置临时 SSH 私钥文件权限失败: {error}"))?;
use std::io::Write as _;
let mut f = crate::utils::private_create_new_options()
.open(&temp_path)
.map_err(|error| {
format!("写入临时 SSH 私钥文件失败 {}: {error}", temp_path.display())
})?;
f.write_all(private_key.as_bytes()).map_err(|error| {
format!("写入临时 SSH 私钥文件失败 {}: {error}", temp_path.display())
})?;
}
Ok(Self {
temp_identity_file: Some(temp_path),
Expand Down
36 changes: 27 additions & 9 deletions src-tauri/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::models::dedupe_account_variants;
use crate::models::AccountsStore;
use crate::models::StoredAccount;
use crate::utils::now_unix_seconds;
use crate::utils::private_create_new_options;
use crate::utils::set_private_permissions;
use crate::utils::short_account;

Expand Down Expand Up @@ -245,9 +246,7 @@ fn write_file_atomically(path: &Path, contents: &[u8]) -> Result<(), String> {
));

let write_result = (|| -> Result<(), String> {
let mut temp_file = fs::OpenOptions::new()
.create_new(true)
.write(true)
let mut temp_file = private_create_new_options()
.open(&temp_path)
.map_err(|e| format!("创建临时存储文件失败 {}: {e}", temp_path.display()))?;
temp_file
Expand Down Expand Up @@ -315,14 +314,12 @@ fn write_store_shadow_backups(path: &Path, contents: &[u8]) -> Result<(), String
if latest_backup.exists() {
let latest_contents = fs::read(&latest_backup)
.map_err(|e| format!("读取最新备份失败 {}: {e}", latest_backup.display()))?;
fs::write(&previous_backup, latest_contents)
write_private_file(&previous_backup, &latest_contents)
.map_err(|e| format!("写入上一个备份失败 {}: {e}", previous_backup.display()))?;
set_private_permissions(&previous_backup);
}

fs::write(&latest_backup, contents)
write_private_file(&latest_backup, contents)
.map_err(|e| format!("写入最新备份失败 {}: {e}", latest_backup.display()))?;
set_private_permissions(&latest_backup);
Ok(())
}

Expand Down Expand Up @@ -457,6 +454,28 @@ fn is_store_backup_candidate(path: &Path) -> bool {
|| name.starts_with(".accounts.json.tmp-")
}

/// 以 0o600 权限(Unix)原子写入文件,替换 fs::write + set_private_permissions 的两步操作,
/// 消除文件短暂对其他用户可读的窗口。
fn write_private_file(path: &Path, contents: &[u8]) -> Result<(), std::io::Error> {
use std::io::Write as _;

// 若文件已存在,先截断再写;不存在则新建,均以受限权限打开
let mut opts = std::fs::OpenOptions::new();
opts.create(true).write(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut f = opts.open(path)?;
f.write_all(contents)?;

#[cfg(windows)]
set_private_permissions(path);

Ok(())
}

fn file_modified_at(path: &Path) -> i64 {
fs::metadata(path)
.ok()
Expand All @@ -477,9 +496,8 @@ fn backup_corrupted_store_file(path: &Path, raw: &str) -> Result<PathBuf, String
.map_err(|e| format!("创建存储目录失败 {}: {e}", parent.display()))?;

let backup_path = parent.join(format!("accounts.corrupt-{}.json", now_unix_seconds()));
fs::write(&backup_path, raw)
write_private_file(&backup_path, raw.as_bytes())
.map_err(|e| format!("写入损坏备份文件失败 {}: {e}", backup_path.display()))?;
set_private_permissions(&backup_path);
Ok(backup_path)
}

Expand Down
36 changes: 35 additions & 1 deletion src-tauri/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,44 @@ pub(crate) fn set_private_permissions(path: &Path) {
}
}

#[cfg(not(unix))]
#[cfg(windows)]
{
// 在 Windows 上尝试通过 icacls 移除 Everyone 的读权限,仅保留当前用户
if let Some(path_str) = path.to_str() {
let current_user = std::env::var("USERNAME").unwrap_or_default();
if !current_user.is_empty() {
let _ = std::process::Command::new("icacls")
.args([
path_str,
"/inheritance:r",
"/grant:r",
&format!("{current_user}:(R,W)"),
])
.output();
}
}
}

#[cfg(not(any(unix, windows)))]
let _ = path;
}

/// 返回用于创建私有临时文件的 OpenOptions:
/// - Unix:以 0o600 权限创建(从一开始就限制访问,消除 TOCTOU)
/// - 其他平台:使用默认 OpenOptions,之后再调用 set_private_permissions
pub(crate) fn private_create_new_options() -> fs::OpenOptions {
let mut opts = fs::OpenOptions::new();
opts.create_new(true).write(true);

#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}

opts
}

pub(crate) fn prepare_process_path() {
let mut merged = preferred_executable_dirs();
if let Some(current_path) = env::var_os("PATH") {
Expand Down