feat: verify updater package and harden client reliability in 0.1.25
This commit is contained in:
@@ -3,6 +3,7 @@ use reqwest::StatusCode;
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::Serialize;
|
||||
use serde_json::{Map, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
@@ -11,9 +12,16 @@ use std::io::{Read, Seek, SeekFrom};
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use tauri::Emitter;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use windows_sys::Win32::Security::Cryptography::{
|
||||
CryptProtectData, CryptUnprotectData, CRYPTPROTECT_UI_FORBIDDEN, CRYPT_INTEGER_BLOB,
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
@@ -198,6 +206,153 @@ fn build_chunk_retry_delay(attempt: u32) -> Duration {
|
||||
Duration::from_millis(ms)
|
||||
}
|
||||
|
||||
fn to_hex_string(bytes: &[u8]) -> String {
|
||||
let mut output = String::with_capacity(bytes.len() * 2);
|
||||
for b in bytes {
|
||||
output.push_str(&format!("{:02x}", b));
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn compute_file_sha256_hex(file_path: &Path) -> Result<String, String> {
|
||||
let mut file = fs::File::open(file_path).map_err(|err| format!("打开文件失败: {}", err))?;
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buf = vec![0_u8; 1024 * 256];
|
||||
loop {
|
||||
let read = file
|
||||
.read(&mut buf)
|
||||
.map_err(|err| format!("读取文件失败: {}", err))?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buf[..read]);
|
||||
}
|
||||
Ok(to_hex_string(&hasher.finalize()))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn dpapi_protect_bytes(input: &[u8]) -> Result<Vec<u8>, String> {
|
||||
if input.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut in_blob = CRYPT_INTEGER_BLOB {
|
||||
cbData: input.len() as u32,
|
||||
pbData: input.as_ptr() as *mut u8,
|
||||
};
|
||||
let mut out_blob = CRYPT_INTEGER_BLOB {
|
||||
cbData: 0,
|
||||
pbData: std::ptr::null_mut(),
|
||||
};
|
||||
|
||||
let ok = unsafe {
|
||||
CryptProtectData(
|
||||
&mut in_blob,
|
||||
std::ptr::null(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
CRYPTPROTECT_UI_FORBIDDEN,
|
||||
&mut out_blob,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
return Err(format!("加密登录状态失败: {}", std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
let data = unsafe {
|
||||
std::slice::from_raw_parts(out_blob.pbData as *const u8, out_blob.cbData as usize).to_vec()
|
||||
};
|
||||
unsafe {
|
||||
LocalFree(out_blob.pbData as *mut core::ffi::c_void);
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn dpapi_unprotect_bytes(input: &[u8]) -> Result<Vec<u8>, String> {
|
||||
if input.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut in_blob = CRYPT_INTEGER_BLOB {
|
||||
cbData: input.len() as u32,
|
||||
pbData: input.as_ptr() as *mut u8,
|
||||
};
|
||||
let mut out_blob = CRYPT_INTEGER_BLOB {
|
||||
cbData: 0,
|
||||
pbData: std::ptr::null_mut(),
|
||||
};
|
||||
|
||||
let ok = unsafe {
|
||||
CryptUnprotectData(
|
||||
&mut in_blob,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
CRYPTPROTECT_UI_FORBIDDEN,
|
||||
&mut out_blob,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
return Err(format!("解密登录状态失败: {}", std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
let data = unsafe {
|
||||
std::slice::from_raw_parts(out_blob.pbData as *const u8, out_blob.cbData as usize).to_vec()
|
||||
};
|
||||
unsafe {
|
||||
LocalFree(out_blob.pbData as *mut core::ffi::c_void);
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn encode_login_password(raw_password: &str) -> Result<String, String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let protected = dpapi_protect_bytes(raw_password.as_bytes())?;
|
||||
return Ok(format!("dpapi:{}", to_hex_string(&protected)));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Ok(raw_password.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_login_password(stored_password: &str) -> Result<String, String> {
|
||||
let raw = stored_password.trim();
|
||||
if let Some(hex_body) = raw.strip_prefix("dpapi:") {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if hex_body.len() % 2 != 0 {
|
||||
return Err("登录状态密文格式无效".to_string());
|
||||
}
|
||||
let mut encrypted = Vec::with_capacity(hex_body.len() / 2);
|
||||
let bytes = hex_body.as_bytes();
|
||||
let mut index = 0;
|
||||
while index < bytes.len() {
|
||||
let part = std::str::from_utf8(&bytes[index..index + 2])
|
||||
.map_err(|_| "登录状态密文格式无效".to_string())?;
|
||||
let value = u8::from_str_radix(part, 16)
|
||||
.map_err(|_| "登录状态密文格式无效".to_string())?;
|
||||
encrypted.push(value);
|
||||
index += 2;
|
||||
}
|
||||
let plain = dpapi_unprotect_bytes(&encrypted)?;
|
||||
return String::from_utf8(plain).map_err(|_| "登录状态密文解码失败".to_string());
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = hex_body;
|
||||
return Err("当前系统不支持读取该登录状态密文".to_string());
|
||||
}
|
||||
}
|
||||
Ok(raw.to_string())
|
||||
}
|
||||
|
||||
fn fallback_json(status: StatusCode, text: &str) -> Value {
|
||||
let mut data = Map::new();
|
||||
data.insert("success".to_string(), Value::Bool(status.is_success()));
|
||||
@@ -316,6 +471,7 @@ fn open_local_state_db() -> Result<Connection, String> {
|
||||
|
||||
fn save_login_state_record(base_url: &str, username: &str, password: &str) -> Result<(), String> {
|
||||
let conn = open_local_state_db()?;
|
||||
let encoded_password = encode_login_password(password)?;
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_secs() as i64)
|
||||
@@ -328,7 +484,7 @@ fn save_login_state_record(base_url: &str, username: &str, password: &str) -> Re
|
||||
username = excluded.username,
|
||||
password = excluded.password,
|
||||
updated_at = excluded.updated_at",
|
||||
params![base_url, username, password, now],
|
||||
params![base_url, username, encoded_password, now],
|
||||
)
|
||||
.map_err(|err| format!("保存登录状态失败: {}", err))?;
|
||||
Ok(())
|
||||
@@ -347,7 +503,10 @@ fn load_login_state_record() -> Result<Option<(String, String, String)>, String>
|
||||
))
|
||||
});
|
||||
match row {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Ok((base_url, username, password)) => {
|
||||
let decoded_password = decode_login_password(&password)?;
|
||||
Ok(Some((base_url, username, decoded_password)))
|
||||
}
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(err) => Err(format!("读取登录状态失败: {}", err)),
|
||||
}
|
||||
@@ -1107,6 +1266,40 @@ async fn api_native_download(
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn api_compute_file_sha256(file_path: String) -> Result<BridgeResponse, String> {
|
||||
let normalized = file_path.trim().to_string();
|
||||
if normalized.is_empty() {
|
||||
return Err("文件路径不能为空".to_string());
|
||||
}
|
||||
let target = PathBuf::from(&normalized);
|
||||
if !target.exists() {
|
||||
return Err("文件不存在".to_string());
|
||||
}
|
||||
if !target.is_file() {
|
||||
return Err("无效的文件路径".to_string());
|
||||
}
|
||||
let file_size = fs::metadata(&target)
|
||||
.map(|meta| meta.len())
|
||||
.map_err(|err| format!("读取文件大小失败: {}", err))?;
|
||||
let sha256 = compute_file_sha256_hex(&target)?;
|
||||
|
||||
let mut data = Map::new();
|
||||
data.insert("success".to_string(), Value::Bool(true));
|
||||
data.insert("filePath".to_string(), Value::String(normalized));
|
||||
data.insert("sha256".to_string(), Value::String(sha256));
|
||||
data.insert(
|
||||
"fileSize".to_string(),
|
||||
Value::Number(serde_json::Number::from(file_size)),
|
||||
);
|
||||
|
||||
Ok(BridgeResponse {
|
||||
ok: true,
|
||||
status: 200,
|
||||
data: Value::Object(data),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn api_launch_installer(installer_path: String) -> Result<BridgeResponse, String> {
|
||||
let path_text = installer_path.trim().to_string();
|
||||
@@ -1548,7 +1741,7 @@ async fn api_upload_file_resumable(
|
||||
Ok(chunk_resp) => parse_response_as_bridge(chunk_resp).await?,
|
||||
Err(err) => {
|
||||
if attempt < RESUMABLE_CHUNK_MAX_RETRIES && is_retryable_transport_error(&err) {
|
||||
thread::sleep(build_chunk_retry_delay(attempt));
|
||||
sleep(build_chunk_retry_delay(attempt)).await;
|
||||
continue;
|
||||
}
|
||||
return Err(format!("上传分片失败: {}", err));
|
||||
@@ -1577,7 +1770,7 @@ async fn api_upload_file_resumable(
|
||||
|| message.to_lowercase().contains("timeout")
|
||||
|| message.contains("稍后重试");
|
||||
if attempt < RESUMABLE_CHUNK_MAX_RETRIES && (retryable_status || retryable_message) {
|
||||
thread::sleep(build_chunk_retry_delay(attempt));
|
||||
sleep(build_chunk_retry_delay(attempt)).await;
|
||||
continue;
|
||||
}
|
||||
return Ok(chunk_bridge);
|
||||
@@ -1753,6 +1946,7 @@ pub fn run() {
|
||||
api_delete_share,
|
||||
api_create_direct_link,
|
||||
api_native_download,
|
||||
api_compute_file_sha256,
|
||||
api_launch_installer,
|
||||
api_silent_install_and_restart,
|
||||
api_check_client_update,
|
||||
|
||||
Reference in New Issue
Block a user