在桌面应用开发中,安全性至关重要。相比 Electron,Tauri 2.0 提供了更严格的安全模型和更完善的权限系统。本文将帮助你理解和实践 Tauri 的安全特性。
权限系统对比
Electron 的安全模型
在 Electron 中,我们通常这样处理安全:
javascript
// main.js
const { app, BrowserWindow } = require('electron')
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true
}
})
// 加载本地文件
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})
主要特点:
- 上下文隔离
- 沙箱机制
- CSP 策略
- 手动配置
Tauri 的安全模型
Tauri 采用了更严格的安全策略:
rust
// main.rs
use tauri::Manager;
use tauri::api::path::app_dir;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Permissions {
fs_access: bool,
network_access: bool,
shell_access: bool,
}
#[tauri::command]
async fn check_permissions(
app: tauri::AppHandle
) -> Result<Permissions, String> {
let perms = Permissions {
fs_access: app.fs_scope().is_allowed("path/to/check"),
network_access: app.runtime_handle().is_allowed("network"),
shell_access: app.runtime_handle().is_allowed("shell")
};
Ok(perms)
}
fn main() {
tauri::Builder::default()
.setup(|app| {
// 配置安全策略
app.manage(tauri::SecurityConfig::default()
.with_csp("default-src 'self'")
.with_dangerous_allow_asset_csp_modification(false)
);
Ok(())
})
.invoke_handler(tauri::generate_handler![
check_permissions
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
typescript
// security.ts
import { invoke } from '@tauri-apps/api/tauri'
interface Permissions {
fs_access: boolean
network_access: boolean
shell_access: boolean
}
export const checkPermissions = async (): Promise<Permissions> => {
try {
return await invoke('check_permissions')
} catch (error) {
console.error('Failed to check permissions:', error)
throw error
}
}
实战案例:安全的密码管理器
让我们通过一个实际的案例来演示 Tauri 的安全特性:
rust
// main.rs
use argon2::{
password_hash::{
rand_core::OsRng,
PasswordHash, PasswordHasher, PasswordVerifier, SaltString
},
Argon2
};
use chacha20poly1305::{
aead::{Aead, KeyInit},
ChaCha20Poly1305, Nonce
};
use serde::{Deserialize, Serialize};
use std::fs;
#[derive(Debug, Serialize, Deserialize)]
struct PasswordEntry {
site: String,
username: String,
encrypted_password: Vec<u8>,
nonce: Vec<u8>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Vault {
entries: Vec<PasswordEntry>,
master_hash: String,
}
#[tauri::command]
async fn create_vault(
master_password: String
) -> Result<(), String> {
// 生成 master password hash
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let master_hash = argon2
.hash_password(master_password.as_bytes(), &salt)
.map_err(|e| e.to_string())?
.to_string();
let vault = Vault {
entries: Vec::new(),
master_hash,
};
// 保存空保险库
save_vault(&vault)?;
Ok(())
}
#[tauri::command]
async fn add_password(
master_password: String,
site: String,
username: String,
password: String
) -> Result<(), String> {
// 验证 master password
let vault = load_vault()?;
let parsed_hash = PasswordHash::new(&vault.master_hash)
.map_err(|e| e.to_string())?;
Argon2::default()
.verify_password(master_password.as_bytes(), &parsed_hash)
.map_err(|_| "Invalid master password")?;
// 加密密码
let key = derive_key(&master_password);
let cipher = ChaCha20Poly1305::new(&key.into());
let nonce = Nonce::from_slice(&rand::random::<[u8; 12]>());
let encrypted_password = cipher
.encrypt(nonce, password.as_bytes())
.map_err(|e| e.to_string())?;
// 添加新条目
let mut vault = load_vault()?;
vault.entries.push(PasswordEntry {
site,
username,
encrypted_password,
nonce: nonce.to_vec(),
});
save_vault(&vault)?;
Ok(())
}
#[tauri::command]
async fn get_password(
master_password: String,
site: String,
username: String
) -> Result<String, String> {
// 验证 master password
let vault = load_vault()?;
let parsed_hash = PasswordHash::new(&vault.master_hash)
.map_err(|e| e.to_string())?;
Argon2::default()
.verify_password(master_password.as_bytes(), &parsed_hash)
.map_err(|_| "Invalid master password")?;
// 查找并解密密码
let entry = vault.entries
.iter()
.find(|e| e.site == site && e.username == username)
.ok_or("Password not found")?;
let key = derive_key(&master_password);
let cipher = ChaCha20Poly1305::new(&key.into());
let nonce = Nonce::from_slice(&entry.nonce);
let password = cipher
.decrypt(nonce, entry.encrypted_password.as_ref())
.map_err(|e| e.to_string())?;
String::from_utf8(password)
.map_err(|e| e.to_string())
}
fn derive_key(password: &str) -> [u8; 32] {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.finalize().into()
}
fn load_vault() -> Result<Vault, String> {
let vault_path = get_vault_path()?;
let json = fs::read_to_string(vault_path)
.map_err(|e| e.to_string())?;
serde_json::from_str(&json)
.map_err(|e| e.to_string())
}
fn save_vault(vault: &Vault) -> Result<(), String> {
let vault_path = get_vault_path()?;
let json = serde_json::to_string_pretty(vault)
.map_err(|e| e.to_string())?;
fs::write(vault_path, json)
.map_err(|e| e.to_string())
}
fn get_vault_path() -> Result<String, String> {
let app_dir = app_dir(&tauri::Config::default())
.ok_or("Failed to get app directory")?;
Ok(app_dir.join("vault.json").to_string_lossy().into_owned())
}
typescript
// App.tsx
import { useState, useEffect } from 'react'
import { invoke } from '@tauri-apps/api/tauri'
interface PasswordEntry {
site: string
username: string
}
function App() {
const [masterPassword, setMasterPassword] = useState('')
const [site, setSite] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [status, setStatus] = useState('')
const [entries, setEntries] = useState<PasswordEntry[]>([])
const handleCreateVault = async () => {
try {
await invoke('create_vault', { masterPassword })
setStatus('Vault created successfully')
} catch (error) {
setStatus(`Error: ${error}`)
}
}
const handleAddPassword = async () => {
try {
await invoke('add_password', {
masterPassword,
site,
username,
password
})
setStatus('Password added successfully')
setPassword('')
} catch (error) {
setStatus(`Error: ${error}`)
}
}
const handleGetPassword = async () => {
try {
const password = await invoke('get_password', {
masterPassword,
site,
username
})
setPassword(password as string)
setStatus('Password retrieved successfully')
} catch (error) {
setStatus(`Error: ${error}`)
}
}
return (
<div className="container">
<h1>Secure Password Manager</h1>
<div className="form-group">
<input
type="password"
placeholder="Master Password"
value={masterPassword}
onChange={(e) => setMasterPassword(e.target.value)}
/>
<button onClick={handleCreateVault}>Create Vault</button>
</div>
<div className="form-group">
<input
type="text"
placeholder="Site"
value={site}
onChange={(e) => setSite(e.target.value)}
/>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button onClick={handleAddPassword}>Add Password</button>
<button onClick={handleGetPassword}>Get Password</button>
</div>
<div className="status">
{status}
</div>
</div>
)
}
export default App
css
/* styles.css */
.container {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.form-group {
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 10px;
}
input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
.status {
margin-top: 20px;
padding: 10px;
border-radius: 4px;
background: #f8f9fa;
}
安全最佳实践
-
权限控制
- 最小权限原则
- 精确的权限范围
- 动态权限请求
- 权限审计日志
-
数据加密
- 使用强加密算法
- 安全的密钥管理
- 加密传输数据
- 安全存储凭证
-
输入验证
- 验证所有输入
- 防止注入攻击
- 限制输入长度
- 过滤特殊字符
-
安全配置
- 严格的 CSP
- 禁用危险特性
- 更新依赖包
- 安全的默认值
调试与审计
-
安全日志
rustuse log::{info, warn, error}; #[tauri::command] async fn security_audit(action: String) -> Result<(), String> { info!("Security audit: {}", action); Ok(()) }
-
权���检查
rust#[tauri::command] async fn check_security_config( app: tauri::AppHandle ) -> Result<String, String> { let config = app.security_config(); Ok(format!("Security config: {:?}", config)) }
-
性能监控
rustuse std::time::Instant; #[tauri::command] async fn measure_security_operation() -> Result<String, String> { let start = Instant::now(); // 执行安全操作 let duration = start.elapsed(); Ok(format!("Operation took: {:?}", duration)) }
小结
-
Tauri 安全优势:
- 更严格的权限模型
- 内置的安全特性
- 更好的隔离机制
- 更现代的加密方案
-
安全策略:
- 实施权限控制
- 加密敏感数据
- 验证所有输入
- 记录安全日志
-
最佳实践:
- 遵循最小权限
- 使用安全配置
- 定期安全审计
- 及时更新依赖
下一篇文章,我们将探讨 Tauri 2.0 的性能优化,帮助你构建更快速、更高效的桌面应用。
如果觉得这篇文章对你有帮助,别忘了点个赞 👍