diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cc60f97..645fcec 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -17,6 +17,41 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -508,6 +543,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "cocoa" version = "0.26.0" @@ -653,6 +698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -693,6 +739,15 @@ dependencies = [ "syn 2.0.92", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.20.10" @@ -1275,6 +1330,16 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1317,10 +1382,13 @@ dependencies = [ name = "github-profile-selector" version = "0.1.0" dependencies = [ + "aes-gcm", + "base64 0.21.7", "chrono", "dirs", "once_cell", "opener", + "rand 0.8.5", "serde", "serde_json", "tauri", @@ -1781,6 +1849,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -2441,6 +2518,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "open" version = "5.3.1" @@ -2748,6 +2831,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3439,6 +3534,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -4164,6 +4265,16 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "url" version = "2.5.4" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f92ded5..3ada14d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,3 +26,6 @@ dirs = "5.0" chrono = "0.4" once_cell = "1.8" opener = "0.5" +aes-gcm = "0.10.3" +base64 = "0.21.7" +rand = "0.8.5" diff --git a/src-tauri/src/encryption.rs b/src-tauri/src/encryption.rs new file mode 100644 index 0000000..06bef55 --- /dev/null +++ b/src-tauri/src/encryption.rs @@ -0,0 +1,101 @@ +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Key, Nonce, +}; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use std::fs; +use once_cell::sync::Lazy; +use std::sync::Mutex; + +static ENCRYPTION_KEY: Lazy>> = Lazy::new(|| { + Mutex::new(vec![0; 32]) // Initialize with zeros +}); + +pub fn set_encryption_key(key: &str) -> Result<(), String> { + let mut key_bytes = [0u8; 32]; + let input_bytes = key.as_bytes(); + + // Pad or truncate the input key to exactly 32 bytes + let len = std::cmp::min(input_bytes.len(), 32); + key_bytes[..len].copy_from_slice(&input_bytes[..len]); + + let mut current_key = ENCRYPTION_KEY.lock().map_err(|e| e.to_string())?; + *current_key = key_bytes.to_vec(); + Ok(()) +} + +pub fn get_encryption_key() -> Result { + let key = ENCRYPTION_KEY.lock().map_err(|e| e.to_string())?; + String::from_utf8(key.clone()).map_err(|e| e.to_string()) +} + +#[derive(Serialize, Deserialize)] +pub struct EncryptedData { + pub nonce: String, + pub ciphertext: String, +} + +pub fn encrypt_string(data: &str) -> Result { + let key_data = ENCRYPTION_KEY.lock().map_err(|e| e.to_string())?; + let key = Key::::from_slice(&key_data); + let cipher = Aes256Gcm::new(key); + + let mut rng = rand::thread_rng(); + let mut nonce_bytes = [0u8; 12]; + rng.fill(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher + .encrypt(nonce, data.as_bytes()) + .map_err(|e| format!("Encryption failed: {}", e))?; + + let encrypted_data = EncryptedData { + nonce: BASE64.encode(nonce), + ciphertext: BASE64.encode(ciphertext), + }; + + serde_json::to_string(&encrypted_data) + .map_err(|e| format!("Failed to serialize encrypted data: {}", e)) +} + +pub fn decrypt_string(encrypted_json: &str) -> Result { + // Parse encrypted data + let encrypted_data: EncryptedData = serde_json::from_str(encrypted_json) + .map_err(|e| format!("Failed to parse encrypted data: {}", e))?; + + // Decode base64 + let nonce_bytes = BASE64 + .decode(encrypted_data.nonce) + .map_err(|e| format!("Failed to decode nonce: {}", e))?; + let ciphertext = BASE64 + .decode(encrypted_data.ciphertext) + .map_err(|e| format!("Failed to decode ciphertext: {}", e))?; + + // Get the current encryption key + let key_data = ENCRYPTION_KEY.lock().map_err(|e| e.to_string())?; + let key = Key::::from_slice(&key_data); + let cipher = Aes256Gcm::new(key); + let nonce = Nonce::from_slice(&nonce_bytes); + + // Decrypt + let plaintext = cipher + .decrypt(nonce, ciphertext.as_ref()) + .map_err(|e| format!("Decryption failed: {}", e))?; + + String::from_utf8(plaintext) + .map_err(|e| format!("Failed to convert decrypted data to string: {}", e)) +} + +pub fn write_encrypted_file(path: &std::path::Path, content: &str) -> Result<(), String> { + let encrypted = encrypt_string(content)?; + fs::write(path, encrypted) + .map_err(|e| format!("Failed to write encrypted file: {}", e)) +} + +pub fn read_encrypted_file(path: &std::path::Path) -> Result { + let encrypted = fs::read_to_string(path) + .map_err(|e| format!("Failed to read encrypted file: {}", e))?; + decrypt_string(&encrypted) +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cc623fb..e6da44d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize}; use std::sync::Mutex; use once_cell::sync::Lazy; use opener; +mod encryption; // Add this at the top +use crate::encryption::{write_encrypted_file, read_encrypted_file, set_encryption_key}; // App configuration structure #[derive(Debug, Serialize, Deserialize, Clone)] @@ -16,6 +18,7 @@ pub struct AppConfig { theme: String, backup_enabled: bool, auto_backup_interval: u32, // in minutes + encryption_key: Option, } #[derive(Serialize, Deserialize, Clone)] @@ -53,10 +56,7 @@ async fn save_profiles(profiles: Vec) -> Result<(), String> { let json = serde_json::to_string_pretty(&profiles) .map_err(|e| format!("Failed to serialize profiles: {}", e))?; - fs::write(&profiles_file, json) - .map_err(|e| format!("Failed to write profiles: {}", e))?; - - Ok(()) + write_encrypted_file(&profiles_file, &json) } #[command] @@ -70,8 +70,7 @@ async fn load_profiles() -> Result, String> { return Ok(Vec::new()); } - let json = fs::read_to_string(&profiles_file) - .map_err(|e| format!("Failed to read profiles: {}", e))?; + let json = read_encrypted_file(&profiles_file)?; serde_json::from_str(&json) .map_err(|e| format!("Failed to deserialize profiles: {}", e)) @@ -152,6 +151,7 @@ impl Default for AppConfig { theme: "system".to_string(), backup_enabled: true, auto_backup_interval: 60, + encryption_key: None, } } } @@ -172,8 +172,7 @@ fn load_config() -> Result { return Ok(config); } - let config_str = fs::read_to_string(&config_path) - .map_err(|e| format!("Failed to read config file: {}", e))?; + let config_str = read_encrypted_file(&config_path)?; serde_json::from_str(&config_str) .map_err(|e| format!("Failed to parse config file: {}", e)) @@ -191,8 +190,15 @@ fn save_config(config: &AppConfig) -> Result<(), String> { let config_str = serde_json::to_string_pretty(config) .map_err(|e| format!("Failed to serialize config: {}", e))?; - fs::write(&config_path, config_str) - .map_err(|e| format!("Failed to write config file: {}", e)) + write_encrypted_file(&config_path, &config_str) +} +// Add this function to initialize encryption on startup +fn initialize_encryption() -> Result<(), String> { + let config = APP_CONFIG.lock().map_err(|e| e.to_string())?; + if let Some(key) = &config.encryption_key { + set_encryption_key(key)?; + } + Ok(()) } #[command] @@ -205,6 +211,12 @@ async fn get_app_config() -> Result { async fn update_app_config(config: AppConfig) -> Result<(), String> { let mut current_config = APP_CONFIG.lock().map_err(|e| e.to_string())?; *current_config = config.clone(); + + // If there's an encryption key, update it + if let Some(key) = &config.encryption_key { + set_encryption_key(key)?; + } + save_config(&config) } @@ -228,8 +240,26 @@ async fn get_profiles_dir() -> Result { Ok(config.profiles_dir.clone()) } +#[command] +async fn update_encryption_key(key: String) -> Result<(), String> { + let mut config = APP_CONFIG.lock().map_err(|e| e.to_string())?; + config.encryption_key = Some(key.clone()); + save_config(&config)?; + set_encryption_key(&key)?; + Ok(()) +} + +#[command] +async fn is_encryption_setup() -> Result { + let config = APP_CONFIG.lock().map_err(|e| e.to_string())?; + Ok(config.encryption_key.is_some()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + if let Err(e) = initialize_encryption() { + eprintln!("Failed to initialize encryption: {}", e); + } tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![update_git_config, @@ -240,7 +270,9 @@ pub fn run() { get_app_config, update_app_config, get_profiles_dir, - open_git_config]) + open_git_config, + update_encryption_key, + is_encryption_setup,]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..40fdb1a --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "../../lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } \ No newline at end of file diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 689dc07..1239564 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -3,12 +3,13 @@ import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; -import { Moon, Sun, ArrowLeft } from "lucide-react"; +import { Moon, Sun, ArrowLeft, Lock } from "lucide-react"; import { useTheme } from "next-themes"; import { toast } from "sonner"; import { Link } from "react-router-dom"; import { invoke } from '@tauri-apps/api/core'; import { Switch } from "../components/ui/switch"; +import { Alert, AlertDescription } from "../components/ui/alert"; interface AppConfig { @@ -16,6 +17,7 @@ interface AppConfig { theme: string; backup_enabled: boolean; auto_backup_interval: number; + encryption_key?: string; } const Settings = () => { @@ -23,6 +25,10 @@ const Settings = () => { const [config, setConfig] = useState(null); const [profilesPath, setProfilesPath] = useState(""); const [backupEnabled, setBackupEnabled] = useState(true); + const [encryptionKey, setEncryptionKey] = useState(""); + const [isEncryptionSetup, setIsEncryptionSetup] = useState(false); + const [showKey, setShowKey] = useState(false); + useEffect(() => { loadConfig(); }, []); @@ -38,6 +44,31 @@ const Settings = () => { } }; + const checkEncryptionStatus = async () => { + try { + const isSetup = await invoke("is_encryption_setup"); + setIsEncryptionSetup(isSetup); + } catch (error) { + toast.error("Failed to check encryption status"); + } + }; + + const handleUpdateEncryption = async () => { + if (!encryptionKey) { + toast.error("Please enter an encryption key"); + return; + } + + try { + await invoke("update_encryption_key", { key: encryptionKey }); + toast.success("Encryption key updated successfully"); + setIsEncryptionSetup(true); + setShowKey(false); + } catch (error) { + toast.error("Failed to update encryption key"); + } + }; + const handleSaveProfilesPath = async () => { if (!config) return; @@ -76,7 +107,7 @@ const Settings = () => { return (
-
+
- + Appearance @@ -130,7 +161,7 @@ const Settings = () => { value={profilesPath} onChange={(e) => setProfilesPath(e.target.value)} placeholder="/path/to/profiles" - className="bg-background/50" + className="bg-background/50" />
@@ -153,6 +184,47 @@ const Settings = () => {
+ + + Security Settings + + +
+ +
+ setEncryptionKey(e.target.value)} + placeholder="Enter encryption key" + className="bg-background/50" + /> + + +
+ {isEncryptionSetup && ( + + + Files are currently encrypted. Changing the key will re-encrypt all files. + + + )} +

+ This key will be used to encrypt your profile and configuration files +

+
+
+
);