Remove Act, AutoUpdater, and Book IPC modules alongside associated database logic.

This commit is contained in:
natreex
2026-04-05 19:18:42 -04:00
parent d4765e6576
commit 687c1d582c
99 changed files with 500 additions and 28269 deletions

View File

@@ -1,243 +1,274 @@
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use crate::error::{AppError, AppResult};
const SERVICE_NAME: &str = "com.eritors.scribe.desktop";
const IV_LENGTH: usize = 16;
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
// ===== Secure vault: encrypted JSON file (like Electron's safeStorage) =====
#[derive(Serialize, Deserialize, Default)]
struct SecureVault {
token: Option<String>,
encryption_keys: HashMap<String, String>,
last_user_id: Option<String>,
pin_hashes: HashMap<String, String>,
#[serde(default)]
pin_failed_attempts: i32,
#[serde(default)]
pin_locked_until: Option<i64>,
}
fn vault_path() -> PathBuf {
dirs_next::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(SERVICE_NAME)
.join("secure-config.json")
}
const KEYRING_USER: &str = "vault-key";
/// Retrieves (or generates and stores) the vault encryption key via the OS keyring
/// (macOS Keychain, Windows DPAPI, Linux Secret Service).
/// Falls back to the old derivation method if the keyring is unavailable,
/// and attempts to migrate the key into the keyring for next time.
fn get_vault_key() -> [u8; 32] {
let entry = keyring::Entry::new(SERVICE_NAME, KEYRING_USER);
if let Ok(entry) = &entry {
if let Ok(stored) = entry.get_password() {
if let Ok(decoded) = BASE64.decode(stored.trim()) {
if decoded.len() == 32 {
let mut key = [0u8; 32];
key.copy_from_slice(&decoded);
return key;
}
}
}
}
let mut key = [0u8; 32];
rand::rng().fill_bytes(&mut key);
let encoded = BASE64.encode(key);
if let Ok(entry) = &entry {
let _ = entry.set_password(&encoded);
}
key
}
fn encrypt_vault(data: &[u8], key: &[u8; 32]) -> AppResult<Vec<u8>> {
let mut iv = [0u8; IV_LENGTH];
rand::rng().fill_bytes(&mut iv);
let block_size = 16;
let padding_len = block_size - (data.len() % block_size);
let mut buf = Vec::with_capacity(data.len() + padding_len);
buf.extend_from_slice(data);
buf.extend(std::iter::repeat(padding_len as u8).take(padding_len));
let padded_len = buf.len();
let cipher = Aes256CbcEnc::new(key.into(), &iv.into());
cipher.encrypt_padded_mut::<aes::cipher::block_padding::NoPadding>(&mut buf, padded_len)
.map_err(|e| AppError::Encryption(format!("Vault encrypt failed: {}", e)))?;
let mut result = Vec::with_capacity(IV_LENGTH + buf.len());
result.extend_from_slice(&iv);
result.extend_from_slice(&buf);
Ok(result)
}
fn decrypt_vault(data: &[u8], key: &[u8; 32]) -> AppResult<Vec<u8>> {
if data.len() < IV_LENGTH + 1 {
return Err(AppError::Encryption("Vault data too short".into()));
}
let iv: [u8; 16] = data[..IV_LENGTH].try_into()
.map_err(|_| AppError::Encryption("Invalid IV".into()))?;
let mut buf = data[IV_LENGTH..].to_vec();
let cipher = Aes256CbcDec::new(key.into(), &iv.into());
let decrypted = cipher.decrypt_padded_mut::<aes::cipher::block_padding::Pkcs7>(&mut buf)
.map_err(|e| AppError::Encryption(format!("Vault decrypt failed: {}", e)))?;
Ok(decrypted.to_vec())
}
fn read_vault() -> AppResult<SecureVault> {
let path = vault_path();
if !path.exists() {
return Ok(SecureVault::default());
}
let content = fs::read_to_string(&path)
.map_err(|e| AppError::Keyring(format!("Failed to read vault: {}", e)))?;
if cfg!(debug_assertions) {
return serde_json::from_str::<SecureVault>(&content)
.map_err(|e| AppError::Keyring(format!("Vault JSON parse error: {}", e)));
}
let raw = BASE64.decode(content.trim())
.map_err(|e| AppError::Keyring(format!("Vault corrupted (base64): {}", e)))?;
let key = get_vault_key();
if let Ok(decrypted) = decrypt_vault(&raw, &key) {
if let Ok(vault) = serde_json::from_slice::<SecureVault>(&decrypted) {
return Ok(vault);
}
}
Err(AppError::Keyring("Vault decryption failed — cannot read existing vault".into()))
}
fn write_vault(vault: &SecureVault) -> AppResult<()> {
let path = vault_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create vault dir: {}", e)))?;
}
if cfg!(debug_assertions) {
let json = serde_json::to_string_pretty(vault)
.map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?;
return fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e)));
}
let key = get_vault_key();
let json = serde_json::to_string(vault)
.map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?;
let encrypted = encrypt_vault(json.as_bytes(), &key)?;
let encoded = BASE64.encode(&encrypted);
fs::write(&path, encoded).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e)))
}
// ===== Public API (same signatures as before) =====
pub fn get_user_encryption_key(user_id: &str) -> AppResult<String> {
let vault = read_vault()?;
vault.encryption_keys.get(user_id).cloned()
.ok_or_else(|| AppError::Keyring(format!("No encryption key for user {}", user_id)))
}
pub fn set_user_encryption_key(user_id: &str, encryption_key: &str) -> AppResult<()> {
let mut vault = read_vault()?;
vault.encryption_keys.insert(user_id.to_string(), encryption_key.to_string());
write_vault(&vault)
}
pub fn has_user_encryption_key(user_id: &str) -> AppResult<bool> {
let vault = read_vault()?;
Ok(vault.encryption_keys.contains_key(user_id))
}
pub fn get_token() -> AppResult<Option<String>> {
let vault = read_vault()?;
Ok(vault.token)
}
pub fn set_token(token: &str) -> AppResult<()> {
let mut vault = read_vault()?;
vault.token = Some(token.to_string());
write_vault(&vault)
}
pub fn remove_token() -> AppResult<()> {
let mut vault = read_vault()?;
vault.token = None;
write_vault(&vault)
}
pub fn set_pin_hash(user_id: &str, pin_hash: &str) -> AppResult<()> {
let mut vault = read_vault()?;
vault.pin_hashes.insert(user_id.to_string(), pin_hash.to_string());
write_vault(&vault)
}
pub fn get_pin_hash(user_id: &str) -> AppResult<Option<String>> {
let vault = read_vault()?;
Ok(vault.pin_hashes.get(user_id).cloned())
}
pub fn set_last_user_id(user_id: &str) -> AppResult<()> {
let mut vault = read_vault()?;
vault.last_user_id = Some(user_id.to_string());
write_vault(&vault)
}
pub fn get_last_user_id() -> AppResult<Option<String>> {
let vault = read_vault()?;
Ok(vault.last_user_id)
}
pub fn clear_vault() -> AppResult<()> {
write_vault(&SecureVault::default())
}
const MAX_PIN_ATTEMPTS: i32 = 5;
const PIN_LOCKOUT_SECONDS: i64 = 300; // 5 minutes
pub fn check_pin_rate_limit() -> AppResult<()> {
let vault = read_vault()?;
if let Some(locked_until) = vault.pin_locked_until {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
if now < locked_until {
let remaining = locked_until - now;
return Err(AppError::Auth(format!("Too many attempts. Try again in {} seconds.", remaining)));
}
}
Ok(())
}
pub fn record_pin_failure() -> AppResult<()> {
let mut vault = read_vault()?;
vault.pin_failed_attempts += 1;
if vault.pin_failed_attempts >= MAX_PIN_ATTEMPTS {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
vault.pin_locked_until = Some(now + PIN_LOCKOUT_SECONDS);
}
write_vault(&vault)
}
pub fn reset_pin_attempts() -> AppResult<()> {
let mut vault = read_vault()?;
vault.pin_failed_attempts = 0;
vault.pin_locked_until = None;
write_vault(&vault)
}
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::error::{AppError, AppResult};
const SERVICE_NAME: &str = "com.eritors.scribe.desktop";
// ===== DEV: plain JSON vault (everything in one file, no encryption) =====
#[derive(Serialize, Deserialize, Default)]
struct SecureVault {
token: Option<String>,
encryption_keys: HashMap<String, String>,
last_user_id: Option<String>,
pin_hashes: HashMap<String, String>,
#[serde(default)]
pin_failed_attempts: i32,
#[serde(default)]
pin_locked_until: Option<i64>,
}
// ===== PROD: plain JSON for non-sensitive config only =====
#[derive(Serialize, Deserialize, Default)]
struct AppConfig {
last_user_id: Option<String>,
#[serde(default)]
pin_failed_attempts: i32,
#[serde(default)]
pin_locked_until: Option<i64>,
}
fn config_path() -> PathBuf {
dirs_next::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(SERVICE_NAME)
.join(if cfg!(debug_assertions) { "secure-config.json" } else { "app-config.json" })
}
// ── DEV helpers ──────────────────────────────────────────────────────────
fn read_vault() -> AppResult<SecureVault> {
let path = config_path();
if !path.exists() { return Ok(SecureVault::default()); }
let content = fs::read_to_string(&path)
.map_err(|e| AppError::Keyring(format!("Failed to read vault: {}", e)))?;
serde_json::from_str::<SecureVault>(&content)
.map_err(|e| AppError::Keyring(format!("Vault JSON parse error: {}", e)))
}
fn write_vault(vault: &SecureVault) -> AppResult<()> {
let path = config_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create dir: {}", e)))?;
}
let json = serde_json::to_string_pretty(vault)
.map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?;
fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e)))
}
// ── PROD helpers ─────────────────────────────────────────────────────────
fn read_config() -> AppResult<AppConfig> {
let path = config_path();
if !path.exists() { return Ok(AppConfig::default()); }
let content = fs::read_to_string(&path)
.map_err(|e| AppError::Keyring(format!("Failed to read config: {}", e)))?;
serde_json::from_str::<AppConfig>(&content)
.map_err(|e| AppError::Keyring(format!("Config JSON parse error: {}", e)))
}
fn write_config(config: &AppConfig) -> AppResult<()> {
let path = config_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create dir: {}", e)))?;
}
let json = serde_json::to_string_pretty(config)
.map_err(|e| AppError::Internal(format!("Failed to serialize config: {}", e)))?;
fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write config: {}", e)))
}
fn keyring_entry(account: &str) -> AppResult<keyring::Entry> {
keyring::Entry::new(SERVICE_NAME, account)
.map_err(|e| AppError::Keyring(format!("Keyring entry error: {}", e)))
}
fn keyring_get(account: &str) -> AppResult<Option<String>> {
let entry = keyring_entry(account)?;
match entry.get_password() {
Ok(val) => Ok(Some(val)),
Err(keyring::Error::NoEntry) => Ok(None),
Err(e) => Err(AppError::Keyring(format!("Keyring read error: {}", e))),
}
}
fn keyring_set(account: &str, value: &str) -> AppResult<()> {
keyring_entry(account)?.set_password(value)
.map_err(|e| AppError::Keyring(format!("Keyring write error: {}", e)))
}
fn keyring_delete(account: &str) -> AppResult<()> {
let entry = keyring_entry(account)?;
match entry.delete_credential() {
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
Err(e) => Err(AppError::Keyring(format!("Keyring delete error: {}", e))),
}
}
// ===== Public API (signatures unchanged) =====
pub fn get_user_encryption_key(user_id: &str) -> AppResult<String> {
if cfg!(debug_assertions) {
let vault = read_vault()?;
return vault.encryption_keys.get(user_id).cloned()
.ok_or_else(|| AppError::Keyring(format!("No encryption key for user {}", user_id)));
}
keyring_get(&format!("encryption_key:{}", user_id))?
.ok_or_else(|| AppError::Keyring(format!("No encryption key for user {}", user_id)))
}
pub fn set_user_encryption_key(user_id: &str, encryption_key: &str) -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.encryption_keys.insert(user_id.to_string(), encryption_key.to_string());
return write_vault(&vault);
}
keyring_set(&format!("encryption_key:{}", user_id), encryption_key)
}
pub fn has_user_encryption_key(user_id: &str) -> AppResult<bool> {
if cfg!(debug_assertions) {
let vault = read_vault()?;
return Ok(vault.encryption_keys.contains_key(user_id));
}
Ok(keyring_get(&format!("encryption_key:{}", user_id))?.is_some())
}
pub fn get_token() -> AppResult<Option<String>> {
if cfg!(debug_assertions) {
let vault = read_vault()?;
return Ok(vault.token);
}
keyring_get("token")
}
pub fn set_token(token: &str) -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.token = Some(token.to_string());
return write_vault(&vault);
}
keyring_set("token", token)
}
pub fn remove_token() -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.token = None;
return write_vault(&vault);
}
keyring_delete("token")
}
pub fn set_pin_hash(user_id: &str, pin_hash: &str) -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.pin_hashes.insert(user_id.to_string(), pin_hash.to_string());
return write_vault(&vault);
}
keyring_set(&format!("pin_hash:{}", user_id), pin_hash)
}
pub fn get_pin_hash(user_id: &str) -> AppResult<Option<String>> {
if cfg!(debug_assertions) {
let vault = read_vault()?;
return Ok(vault.pin_hashes.get(user_id).cloned());
}
keyring_get(&format!("pin_hash:{}", user_id))
}
pub fn set_last_user_id(user_id: &str) -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.last_user_id = Some(user_id.to_string());
return write_vault(&vault);
}
let mut config = read_config()?;
config.last_user_id = Some(user_id.to_string());
write_config(&config)
}
pub fn get_last_user_id() -> AppResult<Option<String>> {
if cfg!(debug_assertions) {
let vault = read_vault()?;
return Ok(vault.last_user_id);
}
let config = read_config()?;
Ok(config.last_user_id)
}
pub fn clear_vault() -> AppResult<()> {
if cfg!(debug_assertions) {
return write_vault(&SecureVault::default());
}
// Prod: delete all known keyring entries + reset config
keyring_delete("token")?;
if let Ok(config) = read_config() {
if let Some(uid) = &config.last_user_id {
keyring_delete(&format!("encryption_key:{}", uid))?;
keyring_delete(&format!("pin_hash:{}", uid))?;
}
}
write_config(&AppConfig::default())
}
const MAX_PIN_ATTEMPTS: i32 = 5;
const PIN_LOCKOUT_SECONDS: i64 = 300;
pub fn check_pin_rate_limit() -> AppResult<()> {
let (locked_until, _) = if cfg!(debug_assertions) {
let vault = read_vault()?;
(vault.pin_locked_until, vault.pin_failed_attempts)
} else {
let config = read_config()?;
(config.pin_locked_until, config.pin_failed_attempts)
};
if let Some(locked_until) = locked_until {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
if now < locked_until {
return Err(AppError::Auth(format!("Too many attempts. Try again in {} seconds.", locked_until - now)));
}
}
Ok(())
}
pub fn record_pin_failure() -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.pin_failed_attempts += 1;
if vault.pin_failed_attempts >= MAX_PIN_ATTEMPTS {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64).unwrap_or(0);
vault.pin_locked_until = Some(now + PIN_LOCKOUT_SECONDS);
}
return write_vault(&vault);
}
let mut config = read_config()?;
config.pin_failed_attempts += 1;
if config.pin_failed_attempts >= MAX_PIN_ATTEMPTS {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64).unwrap_or(0);
config.pin_locked_until = Some(now + PIN_LOCKOUT_SECONDS);
}
write_config(&config)
}
pub fn reset_pin_attempts() -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.pin_failed_attempts = 0;
vault.pin_locked_until = None;
return write_vault(&vault);
}
let mut config = read_config()?;
config.pin_failed_attempts = 0;
config.pin_locked_until = None;
write_config(&config)
}

View File

@@ -22,6 +22,7 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_http::init())
.manage(db_manager)
.manage(session)
.invoke_handler(tauri::generate_handler![