diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5839137e..548ab58f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -23,6 +23,16 @@ 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" @@ -34,6 +44,20 @@ dependencies = [ "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" @@ -338,18 +362,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base32" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" - -[[package]] -name = "base32" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" - [[package]] name = "base64" version = "0.13.1" @@ -756,12 +768,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "constant_time_eq" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" - [[package]] name = "convert_case" version = "0.4.0" @@ -890,6 +896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -930,6 +937,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.8" @@ -1680,6 +1696,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.0" @@ -3060,6 +3086,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "open" version = "3.2.0" @@ -3375,15 +3407,16 @@ dependencies = [ [[package]] name = "phoenix-code-ide" -version = "4.0.3" +version = "4.1.0" dependencies = [ + "aes-gcm", "backtrace", - "base32 0.5.1", "chrono", "clipboard-files", "dialog", "fix-path-env", "gtk", + "hex", "keyring", "lazy_static", "native-dialog", @@ -3402,7 +3435,6 @@ dependencies = [ "tauri-plugin-single-instance", "tauri-plugin-window-state", "tokio", - "totp-rs", "trash", "webbrowser", "webkit2gtk", @@ -3497,6 +3529,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -5154,19 +5198,6 @@ dependencies = [ "winnow 0.6.5", ] -[[package]] -name = "totp-rs" -version = "5.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90" -dependencies = [ - "base32 0.4.0", - "constant_time_eq", - "hmac", - "sha1", - "sha2", -] - [[package]] name = "tower" version = "0.5.2" @@ -5337,6 +5368,16 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[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 = "untrusted" version = "0.9.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 484b9bdd..618a5458 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -34,9 +34,11 @@ tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-works tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } keyring = "2.3.3" -totp-rs = "5.6.0" -base32 = "0.5.1" whoami = "1.5.2" +# AES encryption dependencies +# AES encryption dependencies +aes-gcm = "0.10" +hex = "0.4.3" [target.'cfg(target_os = "windows")'.dependencies] native-dialog = "0.7.0" diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 312cdf4e..7e090fb1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -47,10 +47,6 @@ mod platform; use tauri_plugin_window_state::StateFlags; use keyring::Entry; -use totp_rs::{Algorithm, Secret, TOTP}; -use std::time::{SystemTime, UNIX_EPOCH}; -use serde_json::json; -use base32::{Alphabet, encode as base32_encode}; use whoami; #[derive(Clone, serde::Serialize)] @@ -59,6 +55,17 @@ struct Payload { cwd: String, } +// AES Key Trust Management Structure +#[derive(Clone, Debug)] +struct AesKeyData { + key: String, + iv: String, +} + +struct WindowAesTrust { + trust_map: Mutex>, +} + // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command #[tauri::command] fn console_log(_handle: tauri::AppHandle, message: &str) { @@ -176,6 +183,74 @@ fn delete_item(state: State<'_, Storage>, key: String) { } // in memory hashmap end +// AES Key Trust Management Commands +#[tauri::command] +fn trust_window_aes_key(window: tauri::Window, key: String, iv: String, trust_state: State<'_, WindowAesTrust>) -> Result<(), String> { + let window_label = window.label().to_string(); + let mut trust_map = trust_state.trust_map.lock().unwrap(); + + // Check if trust is already established + if trust_map.contains_key(&window_label) { + return Err("Trust has already been established for this window. remove trust to set again.".to_string()); + } + + // Validate AES key format and length + let key_bytes = hex::decode(&key).map_err(|_| "Invalid AES key format. Key must be a valid hex string.".to_string())?; + if key_bytes.len() != 32 { + return Err("Invalid AES key length. Key must be 32 bytes (64 hex characters) for AES-256.".to_string()); + } + + // Validate nonce format and length + let nonce_bytes = hex::decode(&iv).map_err(|_| "Invalid nonce format. Nonce must be a valid hex string.".to_string())?; + if nonce_bytes.len() != 12 { + return Err("Invalid nonce length. Nonce must be 12 bytes (24 hex characters) for AES-GCM.".to_string()); + } + + // Test that we can create a valid cipher with these parameters + use aes_gcm::{Aes256Gcm, Key}; + use aes_gcm::aead::KeyInit; + + let aes_key = Key::::from_slice(&key_bytes); + let _cipher = Aes256Gcm::new(aes_key); + + // Just verify we can create a slice of the right size - no need to store it + if nonce_bytes.len() == 12 { + // This validates that we have the right size for AES-GCM nonce + } else { + return Err("Invalid nonce length. Nonce must be 12 bytes (24 hex characters) for AES-GCM.".to_string()); + } + + // If we get here, the key and nonce are valid + // Store the AES key and IV for this window + trust_map.insert(window_label.clone(), AesKeyData { key, iv }); + + println!("AES trust established for window: {}", window_label); + Ok(()) +} + +#[tauri::command] +fn remove_trust_window_aes_key(window: tauri::Window, key: String, iv: String, trust_state: State<'_, WindowAesTrust>) -> Result<(), String> { + let window_label = window.label().to_string(); + let mut trust_map = trust_state.trust_map.lock().unwrap(); + + // Check if trust exists for this window + match trust_map.get(&window_label) { + Some(stored_data) => { + // Verify the provided key and IV match the stored ones + if stored_data.key == key && stored_data.iv == iv { + trust_map.remove(&window_label); + println!("AES trust removed for window: {}", window_label); + Ok(()) + } else { + Err("Provided key and IV do not match the stored trust data.".to_string()) + } + } + None => { + Err("No trust association found for this window.".to_string()) + } + } +} + static mut DEVTOOLS_LOADED:bool = false; #[tauri::command] @@ -264,8 +339,16 @@ fn zoom_window(window: tauri::Window, scale_factor: f64) { }); } -fn process_window_event(event: &GlobalWindowEvent) { +fn process_window_event(event: &GlobalWindowEvent, trust_state: &State) { if let tauri::WindowEvent::CloseRequested { .. } = event.event() { + // Remove AES trust for the closing window + let window_label = event.window().label().to_string(); + let mut trust_map = trust_state.trust_map.lock().unwrap(); + + if trust_map.remove(&window_label).is_some() { + println!("AES trust removed for closing window: {}", window_label); + } + // this does nothing and is here if in future you need to persist something on window close. boot_config::write_boot_config(1); } @@ -305,7 +388,6 @@ fn get_mac_deep_link_requests() -> Vec { } const PHOENIX_CRED_PREFIX: &str = "phcode_"; -const MIN_SEED_LENGTH: usize = 10; // Minimum required length for a valid TOTP seed fn get_username() -> String { // Ensure a fallback username in case retrieval fails @@ -315,29 +397,14 @@ fn get_username() -> String { } } -// Stores or updates the sessionID and OTP seed securely. The otp_seed can never be read from js and only -// the 30-second valid t-otp can be read. this helps improving security posture with auth flows as -// unsecure extensions are not being able to get long term session tokens. -// A compromised extension will need real time control of the editor instance to get the changing session -// otp to steal user session serving as a deterrent. +// Stores the secret value securely in the system keyring #[tauri::command] -fn store_credential(scope_name: String, session_id: String, otp_seed: String) -> Result<(), String> { +fn store_credential(scope_name: String, secret_val: String) -> Result<(), String> { let service = format!("{}{}", PHOENIX_CRED_PREFIX, scope_name); // Unique service name per scope let user = get_username(); - // Check if the seed is too short - if otp_seed.len() < MIN_SEED_LENGTH { - return Err(format!( - "SEED_TOO_SHORT: Seed length must be at least {} characters, but got {}.", - MIN_SEED_LENGTH, otp_seed.len() - )); - } - - // Combine sessionID and OTP seed into one stored value - let credential_data = format!("{}|{}", session_id, otp_seed); - let entry = Entry::new(&service, &user).map_err(|e| e.to_string())?; - entry.set_password(&credential_data).map_err(|e| e.to_string())?; + entry.set_password(&secret_val).map_err(|e| e.to_string())?; Ok(()) } @@ -354,64 +421,59 @@ fn delete_credential(scope_name: String) -> Result<(), String> { Ok(()) } -// credential is applied on seed that is base32:Rfc4648 encoded +// Gets the stored credential, encrypts it with the window's AES key, and returns the encrypted value +// Returns None if no credential is found #[tauri::command] -fn get_credential_otp(scope_name: String) -> serde_json::Value { +fn get_credential(window: tauri::Window, scope_name: String, trust_state: State<'_, WindowAesTrust>) -> Result, String> { + let window_label = window.label().to_string(); + + // Check if AES trust is established for this window + let trust_map = trust_state.trust_map.lock().unwrap(); + let aes_data = match trust_map.get(&window_label) { + Some(data) => data.clone(), + None => { + return Err("Trust needs to be first established by calling the trust_window_aes_key API to use this API.".to_string()); + } + }; + drop(trust_map); // Release the lock early + + // Retrieve the stored credential let service = format!("{}{}", PHOENIX_CRED_PREFIX, scope_name); let user = get_username(); - let entry = match Entry::new(&service, &user) { - Ok(entry) => entry, - Err(_) => return json!({ "err_code": "CREDENTIAL_ERROR" }), // Error creating keyring entry - }; + let entry = Entry::new(&service, &user).map_err(|e| e.to_string())?; - // Retrieve stored credentials - let stored_data = match entry.get_password() { + let stored_credential = match entry.get_password() { Ok(data) => data, - Err(keyring::Error::NoEntry) => return json!({ "err_code": "NO_ENTRY" }), // No credentials stored - Err(_) => return json!({ "err_code": "RETRIEVE_ERROR" }), // Other keyring errors + Err(keyring::Error::NoEntry) => return Ok(None), // Return None if no credential found + Err(e) => return Err(format!("Failed to retrieve credential: {}", e.to_string())), }; - let parts: Vec<&str> = stored_data.split('|').collect(); - if parts.len() != 2 { - return json!({ "err_code": "INVALID_FORMAT" }); // Data stored incorrectly - } + // Encrypt the credential using AES-GCM + use aes_gcm::{Aes256Gcm, Key, Nonce}; + use aes_gcm::aead::{Aead, KeyInit}; - let session_id = parts[0].to_string(); - let otp_seed = parts[1]; + // Decode the key and nonce from hex strings + let key_bytes = hex::decode(&aes_data.key).map_err(|_| "Invalid AES key format".to_string())?; + let nonce_bytes = hex::decode(&aes_data.iv).map_err(|_| "Invalid nonce format".to_string())?; - if otp_seed.len() < MIN_SEED_LENGTH { - return json!({ - "err_code": "SEED_TOO_SHORT", - "message": format!("Seed length must be at least {} characters, but got {}.", MIN_SEED_LENGTH, otp_seed.len()) - }); + if key_bytes.len() != 32 { + return Err("AES key must be 32 bytes (256 bits)".to_string()); + } + if nonce_bytes.len() != 12 { + return Err("Nonce must be 12 bytes for AES-GCM".to_string()); } - // Convert the OTP seed to Base32 (Required for TOTP) - let otp_seed_base32 = base32_encode(Alphabet::Rfc4648 { padding: true }, otp_seed.as_bytes()); - - // Convert the Base32-encoded OTP seed into a Secret - let secret = match Secret::Encoded(otp_seed_base32).to_bytes() { - Ok(secret) => secret, - Err(_) => return json!({ "err_code": "SECRET_ERROR" }), // Error converting seed - }; - - // Create a TOTP instance - let totp = match TOTP::new(Algorithm::SHA1, 6, 1, 30, secret) { - Ok(totp) => totp, - Err(_) => return json!({ "err_code": "TOTP_CREATION_ERROR" }), // Error creating TOTP instance - }; - - // Get the current timestamp (Unix time in seconds) - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + // Create cipher + let key = Key::::from_slice(&key_bytes); + let cipher = Aes256Gcm::new(key); + let nonce = Nonce::from_slice(&nonce_bytes); - // Generate the OTP - let otp = totp.generate(timestamp); + // Encrypt the credential + let encrypted_data = cipher.encrypt(nonce, stored_credential.as_bytes()) + .map_err(|_| "Failed to encrypt credential".to_string())?; - // Return JSON with sessionID and OTP - json!({ - "session_id": session_id, - "totp": otp - }) + // Return the encrypted data as a hex string wrapped in Some + Ok(Some(hex::encode(encrypted_data))) } fn main() { @@ -496,6 +558,9 @@ fn main() { .manage(Storage { map: Mutex::new(HashMap::new()), }) + .manage(WindowAesTrust { + trust_map: Mutex::new(HashMap::new()), + }) .register_uri_scheme_protocol("phtauri", move |app, request| { // can't use `tauri` because that's already in use let path = remove_version_from_url(request.uri()); let path = path.strip_prefix("phtauri://localhost"); @@ -548,13 +613,19 @@ fn main() { app.emit_all("single-instance", Payload { args: argv, cwd }).unwrap(); })) - .on_window_event(|event| process_window_event(&event)) + .on_window_event(|event| { + // Get the trust state from the app handle + let app_handle = event.window().app_handle(); + let trust_state = app_handle.state::(); + process_window_event(&event, &trust_state); + }) .invoke_handler(tauri::generate_handler![ get_mac_deep_link_requests, get_process_id, toggle_devtools, console_log, console_error, _get_commandline_args, get_current_working_dir, _get_window_labels, - store_credential, get_credential_otp, delete_credential, + store_credential, get_credential, delete_credential, put_item, get_item, get_all_items, delete_item, + trust_window_aes_key, remove_trust_window_aes_key, _get_windows_drives, _rename_path, show_in_folder, move_to_trash, zoom_window, _get_clipboard_files, _open_url_in_browser_win]) .setup(|app| {