diff --git a/crates/security-engine/src/windows.rs b/crates/security-engine/src/windows.rs index 6f0d7f4..44198ff 100644 --- a/crates/security-engine/src/windows.rs +++ b/crates/security-engine/src/windows.rs @@ -61,12 +61,13 @@ use windows::Win32::System::StationsAndDesktops::{ use nim_credentials::{CredentialStore, PlatformCredentialStore}; use rand::{distributions::Alphanumeric, Rng}; -static CREDENTIALS: OnceLock<(String, String)> = OnceLock::new(); +static VESSEL_USER_STORE: OnceLock = OnceLock::new(); +static VESSEL_PASS_STORE: OnceLock = OnceLock::new(); -pub fn get_vessel_credentials() -> (String, String) { - CREDENTIALS.get_or_init(|| { +pub fn get_vessel_user() -> String { + VESSEL_USER_STORE.get_or_init(|| { let store = PlatformCredentialStore {}; - let user = match store.get_credential_by_target("Antigravity/VesselUser") { + match store.get_credential_by_target("Antigravity/VesselUser") { Ok(Some(u)) => u.to_string(), _ => { let mut rng = rand::thread_rng(); @@ -75,24 +76,27 @@ pub fn get_vessel_credentials() -> (String, String) { let _ = store.set_credential_by_target("Antigravity/VesselUser", &new_user); new_user } - }; - - let pass = match store.get_credential_by_target("Antigravity/VesselPassword") { - Ok(Some(p)) => p.to_string(), - _ => { - let mut rng = rand::thread_rng(); - let new_pass: String = (&mut rng).sample_iter(&Alphanumeric).take(20).map(char::from).collect(); - let new_pass = format!("{}!Aa1", new_pass); - let _ = store.set_credential_by_target("Antigravity/VesselPassword", &new_pass); - new_pass - } - }; - (user, pass) + } }).clone() } -pub fn get_vessel_user() -> String { - get_vessel_credentials().0 +pub fn get_or_create_vessel_password() -> &'static str { + VESSEL_PASS_STORE.get_or_init(|| { + let mut rng = rand::thread_rng(); + let mut pass = String::new(); + + pass.push(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"[rng.gen_range(0..26)] as char); + pass.push(b"abcdefghijklmnopqrstuvwxyz"[rng.gen_range(0..26)] as char); + pass.push(b"0123456789"[rng.gen_range(0..10)] as char); + pass.push(b"!@#$%^&*()-_=+"[rng.gen_range(0..14)] as char); + + let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+"; + for _ in 0..16 { + pass.push(chars[rng.gen_range(0..chars.len())] as char); + } + + pass + }) } const KSCATEGORY_AUDIO: GUID = GUID::from_values( @@ -275,26 +279,38 @@ pub fn setup_vessel_environment() -> Result<()> { } fn create_vessel_user() -> Result<()> { - let (user, pass) = get_vessel_credentials(); + let user = get_vessel_user(); + let pass = get_or_create_vessel_password(); + let output = Command::new("net") - .args(["user", &user, &pass, "/add", "/passwordchg:no"]) + .args(["user", &user, pass, "/add", "/passwordchg:no"]) .output()?; if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let stdout = String::from_utf8_lossy(&output.stdout); - let combined = format!("{} {}", stdout, stderr); - - // If the user already exists, update their password to ensure it matches the credential store - if combined.contains("already exists") || combined.contains("2224") { - let update_output = Command::new("net") - .args(["user", &user, &pass]) - .output()?; - if !update_output.status.success() { - return Err(anyhow!("Failed to update existing VesselUser password: {}", String::from_utf8_lossy(&update_output.stderr))); - } - } else { - return Err(anyhow!("Failed to create VesselUser: {}", combined)); + // If creation fails, the user might already exist. + // Try updating the password to ensure it matches the current session. + let update_output = Command::new("net") + .args(["user", &user, pass]) + .output()?; + + if !update_output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let update_stderr = String::from_utf8_lossy(&update_output.stderr); + return Err(anyhow!( + "Failed to create VesselUser (create error: {}, update error: {})", + stderr.trim(), update_stderr.trim() + )); + } + } else { + // If creation succeeded, we still run the update to be absolutely sure + // the password is set correctly (just in case). + let update_output = Command::new("net") + .args(["user", &user, pass]) + .output()?; + + if !update_output.status.success() { + let update_stderr = String::from_utf8_lossy(&update_output.stderr); + return Err(anyhow!("Failed to update VesselUser password: {}", update_stderr.trim())); } } @@ -429,7 +445,8 @@ fn modify_access( pub fn get_restricted_token() -> Result { unsafe { let mut raw_token = HANDLE::default(); - let (user, pass) = get_vessel_credentials(); + let user = get_vessel_user(); + let pass = get_or_create_vessel_password(); let user_u16: Vec = user .encode_utf16() .chain(std::iter::once(0)) @@ -545,3 +562,28 @@ pub fn audit_process_desktop(thread_id: u32, expected_desktop: &str) -> Result<( } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vessel_password_generation() { + let pass = get_or_create_vessel_password(); + assert_eq!(pass.len(), 20, "Password length must be exactly 20 characters"); + + let has_upper = pass.chars().any(|c| c.is_ascii_uppercase()); + let has_lower = pass.chars().any(|c| c.is_ascii_lowercase()); + let has_digit = pass.chars().any(|c| c.is_ascii_digit()); + let has_special = pass.chars().any(|c| "!@#$%^&*()-_=+".contains(c)); + + assert!(has_upper, "Password should contain at least one uppercase letter"); + assert!(has_lower, "Password should contain at least one lowercase letter"); + assert!(has_digit, "Password should contain at least one digit"); + assert!(has_special, "Password should contain at least one special character"); + + // Ensure repeated calls return the same password (Since it uses OnceLock) + let pass2 = get_or_create_vessel_password(); + assert_eq!(pass, pass2, "Multiple calls must return the same password"); + } +} diff --git a/crates/security-engine/tests/linux_tests.rs b/crates/security-engine/tests/linux_tests.rs index f66974e..e8d0efb 100644 --- a/crates/security-engine/tests/linux_tests.rs +++ b/crates/security-engine/tests/linux_tests.rs @@ -1,5 +1,3 @@ -use security_engine; - #[cfg(unix)] #[test] fn test_get_vessel_user() { diff --git a/crates/security-engine/tests/windows_tests.rs b/crates/security-engine/tests/windows_tests.rs index 343f9d2..35a754d 100644 --- a/crates/security-engine/tests/windows_tests.rs +++ b/crates/security-engine/tests/windows_tests.rs @@ -1,9 +1,12 @@ #[cfg(windows)] #[test] fn test_get_vessel_credentials_consistency() { - let creds1 = security_engine::windows::get_vessel_credentials(); - let creds2 = security_engine::windows::get_vessel_credentials(); - assert_eq!(creds1, creds2, "Credentials should be cached and consistent"); - assert!(creds1.0.starts_with("VesselUser_")); - assert!(creds1.1.ends_with("!Aa1")); + let user1 = security_engine::windows::get_vessel_user(); + let pass1 = security_engine::windows::get_or_create_vessel_password(); + let user2 = security_engine::windows::get_vessel_user(); + let pass2 = security_engine::windows::get_or_create_vessel_password(); + assert_eq!(user1, user2, "User should be cached and consistent"); + assert_eq!(pass1, pass2, "Password should be cached and consistent"); + assert!(user1.starts_with("VesselUser_")); + assert_eq!(pass1.len(), 20, "Password length must be exactly 20 characters"); } diff --git a/crates/vessel/src/lock.rs b/crates/vessel/src/lock.rs index c321892..64a4f74 100644 --- a/crates/vessel/src/lock.rs +++ b/crates/vessel/src/lock.rs @@ -59,6 +59,7 @@ impl GlobalLock { .read(true) .write(true) .create(true) + .truncate(false) .mode(0o666) // Try to set it during creation, but umask might restrict it .open(&lock_path) .or_else(|e| { diff --git a/crates/vm-orch/src/guard.rs b/crates/vm-orch/src/guard.rs index f041c04..5fbc9e4 100644 --- a/crates/vm-orch/src/guard.rs +++ b/crates/vm-orch/src/guard.rs @@ -124,7 +124,7 @@ impl GuardedChild { fn get_host_git_config(key: &str) -> Option { let output = std::process::Command::new("git") - .args(&["config", "--get", key]) + .args(["config", "--get", key]) .output() .ok()?; if output.status.success() { @@ -191,7 +191,7 @@ impl CommandExt for Command { let mut full_cmd = program_name.clone(); for arg in self.get_args() { let arg_str = arg.to_string_lossy(); - full_cmd.push_str(" "); + full_cmd.push(' '); if arg_str.contains(' ') || arg_str.contains('\t') || arg_str.contains('"') || arg_str.contains('\'') { // basic quoting for security evaluation full_cmd.push_str(&format!("\"{}\"", arg_str.replace("\"", "\\\""))); @@ -358,7 +358,7 @@ impl CommandExt for Command { let mut cmd_line = program.clone(); for arg in self.get_args() { let arg_str = arg.to_string_lossy(); - cmd_line.push_str(" "); + cmd_line.push(' '); if arg_str.contains(' ') || arg_str.contains('\t') || arg_str.contains('"') { cmd_line.push_str(&format!("\"{}\"", arg_str.replace("\"", "\\\""))); } else {