#![cfg(target_os = "windows")] use anyhow::Result; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; use rand::rngs::SmallRng; use rand::RngCore; use rand::SeedableRng; use serde::Serialize; use std::ffi::c_void; use std::ffi::OsStr; use std::fs::File; use std::path::Path; use std::path::PathBuf; use windows_sys::Win32::Foundation::GetLastError; use windows_sys::Win32::Foundation::LocalFree; use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER; use windows_sys::Win32::NetworkManagement::NetManagement::NERR_Success; use windows_sys::Win32::NetworkManagement::NetManagement::NetLocalGroupAdd; use windows_sys::Win32::NetworkManagement::NetManagement::NetLocalGroupAddMembers; use windows_sys::Win32::NetworkManagement::NetManagement::NetUserAdd; use windows_sys::Win32::NetworkManagement::NetManagement::NetUserSetInfo; use windows_sys::Win32::NetworkManagement::NetManagement::LOCALGROUP_INFO_1; use windows_sys::Win32::NetworkManagement::NetManagement::LOCALGROUP_MEMBERS_INFO_3; use windows_sys::Win32::NetworkManagement::NetManagement::UF_DONT_EXPIRE_PASSWD; use windows_sys::Win32::NetworkManagement::NetManagement::UF_SCRIPT; use windows_sys::Win32::NetworkManagement::NetManagement::USER_INFO_1; use windows_sys::Win32::NetworkManagement::NetManagement::USER_INFO_1003; use windows_sys::Win32::NetworkManagement::NetManagement::USER_PRIV_USER; use windows_sys::Win32::Security::Authorization::ConvertStringSidToSidW; use windows_sys::Win32::Security::CopySid; use windows_sys::Win32::Security::GetLengthSid; use windows_sys::Win32::Security::LookupAccountNameW; use windows_sys::Win32::Security::LookupAccountSidW; use windows_sys::Win32::Security::SID_NAME_USE; use codex_windows_sandbox::dpapi_protect; use codex_windows_sandbox::sandbox_dir; use codex_windows_sandbox::sandbox_secrets_dir; use codex_windows_sandbox::string_from_sid_bytes; use codex_windows_sandbox::to_wide; use codex_windows_sandbox::SetupErrorCode; use codex_windows_sandbox::SetupFailure; use codex_windows_sandbox::SETUP_VERSION; pub const SANDBOX_USERS_GROUP: &str = "CodexSandboxUsers"; const SANDBOX_USERS_GROUP_COMMENT: &str = "Codex sandbox internal group (managed)"; const SID_ADMINISTRATORS: &str = "S-1-5-32-544"; const SID_USERS: &str = "S-1-5-32-545"; const SID_AUTHENTICATED_USERS: &str = "S-1-5-11"; const SID_EVERYONE: &str = "S-1-1-0"; const SID_SYSTEM: &str = "S-1-5-18"; pub fn ensure_sandbox_users_group(log: &mut File) -> Result<()> { ensure_local_group(SANDBOX_USERS_GROUP, SANDBOX_USERS_GROUP_COMMENT, log) } pub fn resolve_sandbox_users_group_sid() -> Result> { resolve_sid(SANDBOX_USERS_GROUP) } pub fn provision_sandbox_users( codex_home: &Path, offline_username: &str, online_username: &str, log: &mut File, ) -> Result<()> { ensure_sandbox_users_group(log)?; super::log_line( log, &format!("ensuring sandbox users offline={offline_username} online={online_username}"), )?; let offline_password = random_password(); let online_password = random_password(); ensure_sandbox_user(offline_username, &offline_password, log)?; ensure_sandbox_user(online_username, &online_password, log)?; write_secrets( codex_home, offline_username, &offline_password, online_username, &online_password, )?; Ok(()) } pub fn ensure_sandbox_user(username: &str, password: &str, log: &mut File) -> Result<()> { ensure_local_user(username, password, log)?; ensure_local_group_member(SANDBOX_USERS_GROUP, username)?; Ok(()) } pub fn ensure_local_user(name: &str, password: &str, log: &mut File) -> Result<()> { let name_w = to_wide(OsStr::new(name)); let pwd_w = to_wide(OsStr::new(password)); unsafe { let info = USER_INFO_1 { usri1_name: name_w.as_ptr() as *mut u16, usri1_password: pwd_w.as_ptr() as *mut u16, usri1_password_age: 0, usri1_priv: USER_PRIV_USER, usri1_home_dir: std::ptr::null_mut(), usri1_comment: std::ptr::null_mut(), usri1_flags: UF_SCRIPT | UF_DONT_EXPIRE_PASSWD, usri1_script_path: std::ptr::null_mut(), }; let status = NetUserAdd( std::ptr::null(), 1, &info as *const _ as *mut u8, std::ptr::null_mut(), ); if status != NERR_Success { // Try update password via level 1003. let pw_info = USER_INFO_1003 { usri1003_password: pwd_w.as_ptr() as *mut u16, }; let upd = NetUserSetInfo( std::ptr::null(), name_w.as_ptr(), 1003, &pw_info as *const _ as *mut u8, std::ptr::null_mut(), ); if upd != NERR_Success { super::log_line(log, &format!("NetUserSetInfo failed for {name} code {upd}"))?; return Err(anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperUserCreateOrUpdateFailed, format!("failed to create/update user {name}, code {status}/{upd}"), ))); } } // Ensure the principal is a regular local user account. if let Ok(group_name) = lookup_account_name_for_sid(SID_USERS) { let group = to_wide(OsStr::new(&group_name)); let member = LOCALGROUP_MEMBERS_INFO_3 { lgrmi3_domainandname: name_w.as_ptr() as *mut u16, }; let _ = NetLocalGroupAddMembers( std::ptr::null(), group.as_ptr(), 3, &member as *const _ as *mut u8, 1, ); } else { super::log_line( log, "LookupAccountSidW failed for Users SID; skipping Users group membership", )?; } } Ok(()) } pub fn ensure_local_group(name: &str, comment: &str, log: &mut File) -> Result<()> { const ERROR_ALIAS_EXISTS: u32 = 1379; const NERR_GROUP_EXISTS: u32 = 2223; let name_w = to_wide(OsStr::new(name)); let comment_w = to_wide(OsStr::new(comment)); unsafe { let info = LOCALGROUP_INFO_1 { lgrpi1_name: name_w.as_ptr() as *mut u16, lgrpi1_comment: comment_w.as_ptr() as *mut u16, }; let mut parm_err: u32 = 0; let status = NetLocalGroupAdd( std::ptr::null(), 1, &info as *const _ as *mut u8, &mut parm_err as *mut _, ); if status != NERR_Success && status != ERROR_ALIAS_EXISTS && status != NERR_GROUP_EXISTS { super::log_line( log, &format!("NetLocalGroupAdd failed for {name} code {status} parm_err={parm_err}"), )?; return Err(anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperUsersGroupCreateFailed, format!("failed to create local group {name}, code {status}"), ))); } } Ok(()) } pub fn ensure_local_group_member(group_name: &str, member_name: &str) -> Result<()> { // If the member is already in the group, NetLocalGroupAddMembers may // return an error code. We don't care. let group_w = to_wide(OsStr::new(group_name)); let member_w = to_wide(OsStr::new(member_name)); unsafe { let member = LOCALGROUP_MEMBERS_INFO_3 { lgrmi3_domainandname: member_w.as_ptr() as *mut u16, }; let _ = NetLocalGroupAddMembers( std::ptr::null(), group_w.as_ptr(), 3, &member as *const _ as *mut u8, 1, ); } Ok(()) } pub fn resolve_sid(name: &str) -> Result> { if let Some(sid_str) = well_known_sid_str(name) { return sid_bytes_from_string(sid_str); } let name_w = to_wide(OsStr::new(name)); let mut sid_buffer = vec![0u8; 68]; let mut sid_len: u32 = sid_buffer.len() as u32; let mut domain: Vec = Vec::new(); let mut domain_len: u32 = 0; let mut use_type: SID_NAME_USE = 0; loop { let ok = unsafe { LookupAccountNameW( std::ptr::null(), name_w.as_ptr(), sid_buffer.as_mut_ptr() as *mut c_void, &mut sid_len, domain.as_mut_ptr(), &mut domain_len, &mut use_type, ) }; if ok != 0 { sid_buffer.truncate(sid_len as usize); return Ok(sid_buffer); } let err = unsafe { GetLastError() }; if err == ERROR_INSUFFICIENT_BUFFER { sid_buffer.resize(sid_len as usize, 0); domain.resize(domain_len as usize, 0); continue; } return Err(anyhow::anyhow!( "LookupAccountNameW failed for {name}: {err}" )); } } fn well_known_sid_str(name: &str) -> Option<&'static str> { match name { "Administrators" => Some(SID_ADMINISTRATORS), "Users" => Some(SID_USERS), "Authenticated Users" => Some(SID_AUTHENTICATED_USERS), "Everyone" => Some(SID_EVERYONE), "SYSTEM" => Some(SID_SYSTEM), _ => None, } } fn sid_bytes_from_string(sid_str: &str) -> Result> { let sid_w = to_wide(OsStr::new(sid_str)); let mut psid: *mut c_void = std::ptr::null_mut(); if unsafe { ConvertStringSidToSidW(sid_w.as_ptr(), &mut psid) } == 0 { return Err(anyhow::anyhow!( "ConvertStringSidToSidW failed for {sid_str}: {}", unsafe { GetLastError() } )); } let sid_len = unsafe { GetLengthSid(psid) }; if sid_len == 0 { unsafe { LocalFree(psid as _); } return Err(anyhow::anyhow!("GetLengthSid failed for {sid_str}")); } let mut out = vec![0u8; sid_len as usize]; let ok = unsafe { CopySid(sid_len, out.as_mut_ptr() as *mut c_void, psid) }; unsafe { LocalFree(psid as _); } if ok == 0 { return Err(anyhow::anyhow!("CopySid failed for {sid_str}")); } Ok(out) } fn lookup_account_name_for_sid(sid_str: &str) -> Result { let sid_w = to_wide(OsStr::new(sid_str)); let mut psid: *mut c_void = std::ptr::null_mut(); if unsafe { ConvertStringSidToSidW(sid_w.as_ptr(), &mut psid) } == 0 { return Err(anyhow::anyhow!( "ConvertStringSidToSidW failed for {sid_str}: {}", unsafe { GetLastError() } )); } let mut name_len: u32 = 0; let mut domain_len: u32 = 0; let mut use_type: SID_NAME_USE = 0; let ok = unsafe { LookupAccountSidW( std::ptr::null(), psid, std::ptr::null_mut(), &mut name_len, std::ptr::null_mut(), &mut domain_len, &mut use_type, ) }; if ok == 0 { let err = unsafe { GetLastError() }; if err != ERROR_INSUFFICIENT_BUFFER { unsafe { LocalFree(psid as _); } return Err(anyhow::anyhow!( "LookupAccountSidW preflight failed for {sid_str}: {err}" )); } } let mut name_buf: Vec = vec![0u16; name_len as usize]; let mut domain_buf: Vec = vec![0u16; domain_len as usize]; let ok = unsafe { LookupAccountSidW( std::ptr::null(), psid, name_buf.as_mut_ptr(), &mut name_len, domain_buf.as_mut_ptr(), &mut domain_len, &mut use_type, ) }; unsafe { LocalFree(psid as _); } if ok == 0 { return Err(anyhow::anyhow!( "LookupAccountSidW failed for {sid_str}: {}", unsafe { GetLastError() } )); } let name = String::from_utf16_lossy(&name_buf); Ok(name.trim_end_matches('\0').to_string()) } pub fn sid_bytes_to_psid(sid: &[u8]) -> Result<*mut c_void> { let sid_str = string_from_sid_bytes(sid).map_err(anyhow::Error::msg)?; let sid_w = to_wide(OsStr::new(&sid_str)); let mut psid: *mut c_void = std::ptr::null_mut(); if unsafe { ConvertStringSidToSidW(sid_w.as_ptr(), &mut psid) } == 0 { return Err(anyhow::anyhow!( "ConvertStringSidToSidW failed: {}", unsafe { GetLastError() } )); } Ok(psid) } fn random_password() -> String { const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+"; let mut rng = SmallRng::from_entropy(); let mut buf = [0u8; 24]; rng.fill_bytes(&mut buf); buf.iter() .map(|b| { let idx = (*b as usize) % CHARS.len(); CHARS[idx] as char }) .collect() } #[derive(Serialize)] struct SandboxUserRecord { username: String, password: String, } #[derive(Serialize)] struct SandboxUsersFile { version: u32, offline: SandboxUserRecord, online: SandboxUserRecord, } #[derive(Serialize)] struct SetupMarker { version: u32, offline_username: String, online_username: String, created_at: String, read_roots: Vec, write_roots: Vec, } fn write_secrets( codex_home: &Path, offline_user: &str, offline_pwd: &str, online_user: &str, online_pwd: &str, ) -> Result<()> { let sandbox_dir = sandbox_dir(codex_home); std::fs::create_dir_all(&sandbox_dir).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperUsersFileWriteFailed, format!( "failed to create sandbox dir {}: {err}", sandbox_dir.display() ), )) })?; let secrets_dir = sandbox_secrets_dir(codex_home); std::fs::create_dir_all(&secrets_dir).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperUsersFileWriteFailed, format!( "failed to create secrets dir {}: {err}", secrets_dir.display() ), )) })?; let offline_blob = dpapi_protect(offline_pwd.as_bytes()).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperDpapiProtectFailed, format!("dpapi protect failed for offline user: {err}"), )) })?; let online_blob = dpapi_protect(online_pwd.as_bytes()).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperDpapiProtectFailed, format!("dpapi protect failed for online user: {err}"), )) })?; let users = SandboxUsersFile { version: SETUP_VERSION, offline: SandboxUserRecord { username: offline_user.to_string(), password: BASE64.encode(offline_blob), }, online: SandboxUserRecord { username: online_user.to_string(), password: BASE64.encode(online_blob), }, }; let marker = SetupMarker { version: SETUP_VERSION, offline_username: offline_user.to_string(), online_username: online_user.to_string(), created_at: chrono::Utc::now().to_rfc3339(), read_roots: Vec::new(), write_roots: Vec::new(), }; let users_path = secrets_dir.join("sandbox_users.json"); let marker_path = sandbox_dir.join("setup_marker.json"); let users_json = serde_json::to_vec_pretty(&users).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperUsersFileWriteFailed, format!("serialize sandbox users failed: {err}"), )) })?; std::fs::write(&users_path, users_json).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperUsersFileWriteFailed, format!( "write sandbox users file {} failed: {err}", users_path.display() ), )) })?; let marker_json = serde_json::to_vec_pretty(&marker).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperSetupMarkerWriteFailed, format!("serialize setup marker failed: {err}"), )) })?; std::fs::write(&marker_path, marker_json).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperSetupMarkerWriteFailed, format!( "write setup marker file {} failed: {err}", marker_path.display() ), )) })?; Ok(()) }