1use serde::Serialize;
27use std::path::Path;
28
29const FILE_NAME: &str = "authkey.dev";
30
31#[derive(Serialize)]
32pub struct DevAuthFile<'a> {
33 pub version: u32,
34 pub auth_key: &'a str,
35 pub web_endpoint: &'a str,
36 pub ws_endpoint: &'a str,
37 pub ipc_endpoint: &'a str,
38 pub ipc_token: &'a str,
39 pub service_path: &'static str,
40 pub file_path: &'static str,
41 pub instance: &'a str,
42 pub data_dir: String,
43 pub host_pid: u32,
44 pub created_at: String,
45}
46
47pub fn write_dev_auth_file(
52 data_dir: &Path,
53 auth_key: &str,
54 web_endpoint: &str,
55 ws_endpoint: &str,
56 ipc_endpoint: &str,
57 ipc_token: &str,
58 instance: &str,
59 host_pid: u32,
60) -> Result<std::path::PathBuf, String> {
61 let path = data_dir.join(FILE_NAME);
62 let payload = DevAuthFile {
63 version: 1,
64 auth_key,
65 web_endpoint,
66 ws_endpoint,
67 ipc_endpoint,
68 ipc_token,
69 service_path: "/agentmux/service",
70 file_path: "/agentmux/file",
71 instance,
72 data_dir: data_dir.to_string_lossy().into_owned(),
73 host_pid,
74 created_at: chrono::Utc::now().to_rfc3339(),
75 };
76 let json = serde_json::to_string_pretty(&payload)
77 .map_err(|e| format!("serialize authkey.dev: {}", e))?;
78
79 std::fs::write(&path, json.as_bytes())
83 .map_err(|e| format!("write {}: {}", path.display(), e))?;
84
85 #[cfg(unix)]
92 apply_owner_only_mode(&path)?;
93 #[cfg(target_os = "windows")]
94 apply_owner_only_dacl(&path)?;
95
96 Ok(path)
97}
98
99#[cfg(unix)]
100fn apply_owner_only_mode(path: &Path) -> Result<(), String> {
101 use std::os::unix::fs::PermissionsExt;
102 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
103 .map_err(|e| format!("chmod 0600 {}: {}", path.display(), e))
104}
105
106#[cfg(target_os = "windows")]
107fn apply_owner_only_dacl(path: &Path) -> Result<(), String> {
108 use std::ffi::c_void;
109 use std::os::windows::ffi::OsStrExt;
110 use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, HANDLE};
111 use windows_sys::Win32::Security::Authorization::{
112 SetNamedSecurityInfoW, SE_FILE_OBJECT,
113 };
114 use windows_sys::Win32::Security::{
115 AddAccessAllowedAce, GetLengthSid, GetTokenInformation, InitializeAcl,
116 TokenUser, ACL, ACL_REVISION, DACL_SECURITY_INFORMATION,
117 PROTECTED_DACL_SECURITY_INFORMATION, TOKEN_QUERY, TOKEN_USER,
118 };
119 use windows_sys::Win32::System::Threading::{
120 GetCurrentProcess, OpenProcessToken,
121 };
122
123 const FILE_ALL_ACCESS: u32 = 0x001F_01FF;
127
128 unsafe {
129 let mut token: HANDLE = std::ptr::null_mut();
131 if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
132 return Err(format!("OpenProcessToken failed: {}", GetLastError()));
133 }
134 let mut needed: u32 = 0;
135 let _ = GetTokenInformation(
137 token,
138 TokenUser,
139 std::ptr::null_mut(),
140 0,
141 &mut needed,
142 );
143 if needed == 0 {
144 CloseHandle(token);
145 return Err(format!(
146 "GetTokenInformation sizing returned 0: {}",
147 GetLastError()
148 ));
149 }
150 let mut token_buf: Vec<u8> = vec![0; needed as usize];
151 if GetTokenInformation(
152 token,
153 TokenUser,
154 token_buf.as_mut_ptr() as *mut c_void,
155 needed,
156 &mut needed,
157 ) == 0
158 {
159 CloseHandle(token);
160 return Err(format!("GetTokenInformation failed: {}", GetLastError()));
161 }
162 CloseHandle(token);
163
164 let token_user_ptr = token_buf.as_ptr() as *const TOKEN_USER;
167 let sid = (*token_user_ptr).User.Sid;
168 let sid_len = GetLengthSid(sid);
169 if sid_len == 0 {
170 return Err("GetLengthSid returned 0 — invalid SID".to_string());
171 }
172
173 let ace_header_size =
178 std::mem::size_of::<windows_sys::Win32::Security::ACCESS_ALLOWED_ACE>() as u32;
179 let ace_size = ace_header_size
180 .saturating_add(sid_len)
181 .saturating_sub(std::mem::size_of::<u32>() as u32);
182 let acl_size = std::mem::size_of::<ACL>() as u32 + ace_size;
183
184 let mut acl_buf: Vec<u8> = vec![0; acl_size as usize];
185 let acl_ptr = acl_buf.as_mut_ptr() as *mut ACL;
186 if InitializeAcl(acl_ptr, acl_size, ACL_REVISION as u32) == 0 {
187 return Err(format!("InitializeAcl failed: {}", GetLastError()));
188 }
189 if AddAccessAllowedAce(acl_ptr, ACL_REVISION as u32, FILE_ALL_ACCESS, sid)
190 == 0
191 {
192 return Err(format!("AddAccessAllowedAce failed: {}", GetLastError()));
193 }
194
195 let mut wide: Vec<u16> = path
199 .as_os_str()
200 .encode_wide()
201 .chain(std::iter::once(0))
202 .collect();
203 let result = SetNamedSecurityInfoW(
204 wide.as_mut_ptr(),
205 SE_FILE_OBJECT,
206 DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
207 std::ptr::null_mut(),
208 std::ptr::null_mut(),
209 acl_ptr,
210 std::ptr::null_mut(),
211 );
212 if result != 0 {
213 return Err(format!("SetNamedSecurityInfoW returned {}", result));
214 }
215 }
216
217 Ok(())
218}
219
220#[cfg(all(test, feature = "test-authfile"))]
229mod tests {
230 use super::*;
231
232 fn temp_dir(label: &str) -> std::path::PathBuf {
233 let mut p = std::env::temp_dir();
234 p.push(format!("agentmux-authfile-test-{}-{}", label, std::process::id()));
235 std::fs::create_dir_all(&p).expect("mkdir tempdir");
236 p
237 }
238
239 #[test]
240 fn writes_well_formed_json_with_all_fields() {
241 let dir = temp_dir("format");
242 let path = write_dev_auth_file(
243 &dir,
244 "f8c9b0e4-1234-4567-89ab-cdef01234567",
245 "127.0.0.1:59719",
246 "127.0.0.1:59720",
247 "127.0.0.1:59718",
248 "92d136fa-2e14-46d0-9ace-eddee320a35e",
249 "v0.33.265",
250 12345,
251 )
252 .expect("write authfile");
253
254 let body = std::fs::read_to_string(&path).expect("read back");
255 let parsed: serde_json::Value =
256 serde_json::from_str(&body).expect("parse JSON");
257
258 assert_eq!(parsed["version"], 1);
259 assert_eq!(parsed["auth_key"], "f8c9b0e4-1234-4567-89ab-cdef01234567");
260 assert_eq!(parsed["web_endpoint"], "127.0.0.1:59719");
261 assert_eq!(parsed["ws_endpoint"], "127.0.0.1:59720");
262 assert_eq!(parsed["ipc_endpoint"], "127.0.0.1:59718");
263 assert_eq!(parsed["ipc_token"], "92d136fa-2e14-46d0-9ace-eddee320a35e");
264 assert_eq!(parsed["service_path"], "/agentmux/service");
265 assert_eq!(parsed["file_path"], "/agentmux/file");
266 assert_eq!(parsed["instance"], "v0.33.265");
267 assert_eq!(parsed["host_pid"], 12345);
268 assert!(parsed["created_at"].as_str().unwrap_or("").contains("T"));
269 assert!(parsed["data_dir"].as_str().unwrap_or("").contains("agentmux-authfile-test"));
270
271 let _ = std::fs::remove_dir_all(&dir);
273 }
274
275 #[test]
276 fn overwrites_existing_file_on_repeat_call() {
277 let dir = temp_dir("overwrite");
278 let path1 = write_dev_auth_file(
279 &dir, "old-key", "127.0.0.1:1", "127.0.0.1:2", "127.0.0.1:3",
280 "old-token", "v0.0.1", 1,
281 ).unwrap();
282 let path2 = write_dev_auth_file(
283 &dir, "new-key", "127.0.0.1:11", "127.0.0.1:22", "127.0.0.1:33",
284 "new-token", "v0.0.2", 2,
285 ).unwrap();
286 assert_eq!(path1, path2, "same path expected");
287 let body = std::fs::read_to_string(&path2).unwrap();
288 let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
289 assert_eq!(parsed["auth_key"], "new-key");
290 assert_eq!(parsed["instance"], "v0.0.2");
291 let _ = std::fs::remove_dir_all(&dir);
292 }
293
294 #[cfg(target_os = "windows")]
295 #[test]
296 fn dacl_grants_only_current_user() {
297 use std::ffi::c_void;
298 use std::os::windows::ffi::OsStrExt;
299 use windows_sys::Win32::Foundation::{CloseHandle, LocalFree, HANDLE};
300 use windows_sys::Win32::Security::Authorization::{
301 GetNamedSecurityInfoW, SE_FILE_OBJECT,
302 };
303 use windows_sys::Win32::Security::{
304 EqualSid, GetAce, GetTokenInformation, TokenUser,
305 ACCESS_ALLOWED_ACE, ACL, ACL_SIZE_INFORMATION, AclSizeInformation,
306 DACL_SECURITY_INFORMATION, GetAclInformation, TOKEN_QUERY, TOKEN_USER,
307 };
308 use windows_sys::Win32::System::Threading::{
309 GetCurrentProcess, OpenProcessToken,
310 };
311
312 let dir = temp_dir("dacl");
313 let path = write_dev_auth_file(
314 &dir, "k", "127.0.0.1:1", "127.0.0.1:2", "127.0.0.1:3",
315 "t", "v0.0.0", std::process::id(),
316 ).unwrap();
317
318 unsafe {
319 let mut wide: Vec<u16> = path
321 .as_os_str()
322 .encode_wide()
323 .chain(std::iter::once(0))
324 .collect();
325 let mut dacl: *mut ACL = std::ptr::null_mut();
326 let mut sd: *mut c_void = std::ptr::null_mut();
327 let result = GetNamedSecurityInfoW(
328 wide.as_mut_ptr(),
329 SE_FILE_OBJECT,
330 DACL_SECURITY_INFORMATION,
331 std::ptr::null_mut(),
332 std::ptr::null_mut(),
333 &mut dacl,
334 std::ptr::null_mut(),
335 &mut sd,
336 );
337 assert_eq!(result, 0, "GetNamedSecurityInfoW failed: {}", result);
338 assert!(!dacl.is_null(), "DACL should be present");
339
340 let mut sz: ACL_SIZE_INFORMATION = std::mem::zeroed();
342 assert_ne!(
343 GetAclInformation(
344 dacl,
345 &mut sz as *mut _ as *mut c_void,
346 std::mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
347 AclSizeInformation,
348 ),
349 0,
350 "GetAclInformation failed",
351 );
352 assert_eq!(sz.AceCount, 1, "expected exactly one ACE in DACL, got {}", sz.AceCount);
353
354 let mut ace_ptr: *mut c_void = std::ptr::null_mut();
356 assert_ne!(
357 GetAce(dacl, 0, &mut ace_ptr),
358 0,
359 "GetAce failed",
360 );
361 let ace = ace_ptr as *const ACCESS_ALLOWED_ACE;
362 let ace_sid = &(*ace).SidStart as *const u32 as *mut c_void;
363
364 let mut token: HANDLE = std::ptr::null_mut();
366 assert_ne!(
367 OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token),
368 0,
369 );
370 let mut needed = 0u32;
371 let _ = GetTokenInformation(
372 token, TokenUser, std::ptr::null_mut(), 0, &mut needed,
373 );
374 let mut buf = vec![0u8; needed as usize];
375 assert_ne!(
376 GetTokenInformation(
377 token, TokenUser,
378 buf.as_mut_ptr() as *mut c_void, needed, &mut needed,
379 ),
380 0,
381 );
382 CloseHandle(token);
383 let token_user = buf.as_ptr() as *const TOKEN_USER;
384 let user_sid = (*token_user).User.Sid;
385
386 assert_ne!(
387 EqualSid(ace_sid, user_sid),
388 0,
389 "ACE SID does not match current user SID",
390 );
391
392 LocalFree(sd);
393 }
394
395 let _ = std::fs::remove_dir_all(&dir);
396 }
397}