From 55dd308b2ab1f3f92b9771ecb1a987c6d57a1c50 Mon Sep 17 00:00:00 2001 From: Jules Date: Wed, 24 Jun 2026 15:10:43 +0000 Subject: [PATCH 1/5] feat: ephemeral Session Randomization for VesselUser - Remove hardcoded VESSEL_PASS constant - Generate a high-entropy, memory-resident password for VesselUser on every engine startup - Update VesselUser account dynamically via net user command - Use memory-resident password for authentication - Comply with Windows local account complexity requirements - Ensure zero persistent storage of the password --- Cargo.lock | 8 +----- crates/security-engine/Cargo.toml | 1 + crates/security-engine/src/windows.rs | 37 ++++++++++++++++++++++++--- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 322ebd1..85066d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1395,6 +1395,7 @@ dependencies = [ "anyhow", "libc", "log", + "rand", "regex", "serde", "toml", @@ -1998,13 +1999,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "win_test" -version = "0.1.0" -dependencies = [ - "windows", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/crates/security-engine/Cargo.toml b/crates/security-engine/Cargo.toml index 98ba360..e3f0871 100644 --- a/crates/security-engine/Cargo.toml +++ b/crates/security-engine/Cargo.toml @@ -11,5 +11,6 @@ serde.workspace = true log.workspace = true regex = "1.12.4" +rand = "0.8" [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/security-engine/src/windows.rs b/crates/security-engine/src/windows.rs index 20009c1..41d4461 100644 --- a/crates/security-engine/src/windows.rs +++ b/crates/security-engine/src/windows.rs @@ -59,7 +59,27 @@ use windows::Win32::System::StationsAndDesktops::{ }; pub const VESSEL_USER: &str = "VesselUser"; -pub const VESSEL_PASS: &str = "VesselPassword123!"; +static VESSEL_PASS_STORE: OnceLock = OnceLock::new(); + +pub fn get_or_create_vessel_password() -> &'static str { + VESSEL_PASS_STORE.get_or_init(|| { + use rand::Rng; + 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( 0x6994AD04, @@ -241,8 +261,10 @@ pub fn setup_vessel_environment() -> Result<()> { } fn create_vessel_user() -> Result<()> { + let pass = get_or_create_vessel_password(); + let output = Command::new("net") - .args(["user", VESSEL_USER, VESSEL_PASS, "/add", "/passwordchg:no"]) + .args(["user", VESSEL_USER, pass, "/add", "/passwordchg:no"]) .output()?; let stderr = String::from_utf8_lossy(&output.stderr); @@ -250,6 +272,15 @@ fn create_vessel_user() -> Result<()> { return Err(anyhow!("Failed to create VesselUser: {}", stderr)); } + let update_output = Command::new("net") + .args(["user", VESSEL_USER, pass]) + .output()?; + + let update_stderr = String::from_utf8_lossy(&update_output.stderr); + if !update_output.status.success() { + return Err(anyhow!("Failed to update VesselUser password: {}", update_stderr)); + } + Ok(()) } @@ -385,7 +416,7 @@ pub fn get_restricted_token() -> Result { .encode_utf16() .chain(std::iter::once(0)) .collect(); - let pass_u16: Vec = VESSEL_PASS + let pass_u16: Vec = get_or_create_vessel_password() .encode_utf16() .chain(std::iter::once(0)) .collect(); From 8f02e5eb57af41b4803be5ab784932499099b95b Mon Sep 17 00:00:00 2001 From: Jules Date: Wed, 24 Jun 2026 15:53:29 +0000 Subject: [PATCH 2/5] Enhance create_vessel_user robustness and add tests for password randomization --- crates/security-engine/src/windows.rs | 63 ++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/crates/security-engine/src/windows.rs b/crates/security-engine/src/windows.rs index 41d4461..eaa7b93 100644 --- a/crates/security-engine/src/windows.rs +++ b/crates/security-engine/src/windows.rs @@ -267,18 +267,32 @@ fn create_vessel_user() -> Result<()> { .args(["user", VESSEL_USER, pass, "/add", "/passwordchg:no"]) .output()?; - let stderr = String::from_utf8_lossy(&output.stderr); - if !output.status.success() && !stderr.contains("already exists") { - return Err(anyhow!("Failed to create VesselUser: {}", stderr)); - } - - let update_output = Command::new("net") - .args(["user", VESSEL_USER, pass]) - .output()?; - - let update_stderr = String::from_utf8_lossy(&update_output.stderr); - if !update_output.status.success() { - return Err(anyhow!("Failed to update VesselUser password: {}", update_stderr)); + if !output.status.success() { + // 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", VESSEL_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", VESSEL_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())); + } } Ok(()) @@ -527,3 +541,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"); + } +} From 3eef887f7e26adb29ee3b31495f831a926406aca Mon Sep 17 00:00:00 2001 From: Jules Date: Wed, 24 Jun 2026 16:47:44 +0000 Subject: [PATCH 3/5] Fix mismatched type in get_vessel_user and update Windows tests to match new API --- crates/security-engine/src/windows.rs | 2 +- crates/security-engine/tests/windows_tests.rs | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/security-engine/src/windows.rs b/crates/security-engine/src/windows.rs index 71549aa..44198ff 100644 --- a/crates/security-engine/src/windows.rs +++ b/crates/security-engine/src/windows.rs @@ -68,7 +68,7 @@ pub fn get_vessel_user() -> String { VESSEL_USER_STORE.get_or_init(|| { let store = PlatformCredentialStore {}; match store.get_credential_by_target("Antigravity/VesselUser") { - Ok(Some(u)) => u, + Ok(Some(u)) => u.to_string(), _ => { let mut rng = rand::thread_rng(); let random_suffix: String = (&mut rng).sample_iter(&Alphanumeric).take(8).map(char::from).collect(); diff --git a/crates/security-engine/tests/windows_tests.rs b/crates/security-engine/tests/windows_tests.rs index 26d830e..a370db7 100644 --- a/crates/security-engine/tests/windows_tests.rs +++ b/crates/security-engine/tests/windows_tests.rs @@ -3,9 +3,11 @@ use security_engine; #[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_")); } From abdc91ffeac83e4b4e7612cdeec992cac86ef2a5 Mon Sep 17 00:00:00 2001 From: Jules Date: Wed, 24 Jun 2026 16:51:17 +0000 Subject: [PATCH 4/5] Add password length check to windows tests --- crates/security-engine/tests/windows_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/security-engine/tests/windows_tests.rs b/crates/security-engine/tests/windows_tests.rs index a370db7..1592e67 100644 --- a/crates/security-engine/tests/windows_tests.rs +++ b/crates/security-engine/tests/windows_tests.rs @@ -10,4 +10,5 @@ fn test_get_vessel_credentials_consistency() { 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"); } From a3f4abb73296f8d5065f043b0856e8afeb79bda6 Mon Sep 17 00:00:00 2001 From: Jules Date: Wed, 24 Jun 2026 16:54:21 +0000 Subject: [PATCH 5/5] Fix clippy warnings across workspace --- crates/security-engine/tests/linux_tests.rs | 2 -- crates/security-engine/tests/windows_tests.rs | 2 -- crates/vessel/src/lock.rs | 1 + crates/vm-orch/src/guard.rs | 6 +++--- 4 files changed, 4 insertions(+), 7 deletions(-) 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 1592e67..35a754d 100644 --- a/crates/security-engine/tests/windows_tests.rs +++ b/crates/security-engine/tests/windows_tests.rs @@ -1,5 +1,3 @@ -use security_engine; - #[cfg(windows)] #[test] fn test_get_vessel_credentials_consistency() { 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 aa2f8f8..945b035 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() { @@ -187,7 +187,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("\"", "\\\""))); @@ -340,7 +340,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 {