running_process/broker/lifecycle/
sid.rs1#[derive(Debug, thiserror::Error)]
25pub enum SidError {
26 #[error("failed to read platform user identity: {0}")]
29 PlatformLookup(String),
30}
31
32pub fn user_sid_hash() -> Result<String, SidError> {
35 let input = platform_identity_string()?;
36 Ok(hash_to_16_hex(input.as_bytes()))
37}
38
39pub fn hash_to_16_hex(input: &[u8]) -> String {
44 let digest = blake3::hash(input);
45 let bytes = digest.as_bytes();
46 let mut out = String::with_capacity(16);
48 for b in &bytes[..8] {
49 out.push(nibble_to_hex(b >> 4));
51 out.push(nibble_to_hex(b & 0x0F));
52 }
53 out
54}
55
56#[inline]
57fn nibble_to_hex(n: u8) -> char {
58 match n {
59 0..=9 => (b'0' + n) as char,
60 10..=15 => (b'a' + (n - 10)) as char,
61 _ => unreachable!("nibble out of range"),
62 }
63}
64
65fn platform_identity_string() -> Result<String, SidError> {
66 #[cfg(windows)]
67 {
68 windows_current_user_sid()
69 }
70 #[cfg(target_os = "macos")]
71 {
72 let uid = unsafe { libc::getuid() };
73 let uuid = macos_platform_uuid()?;
74 Ok(format!("{uid}:{uuid}"))
75 }
76 #[cfg(all(unix, not(target_os = "macos")))]
77 {
78 let uid = unsafe { libc::getuid() };
79 let machine_id = linux_machine_id()?;
80 Ok(format!("{uid}:{machine_id}"))
81 }
82}
83
84#[cfg(windows)]
85fn windows_current_user_sid() -> Result<String, SidError> {
86 use std::ptr;
87 use winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER;
88 use winapi::um::errhandlingapi::GetLastError;
89 use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
90 use winapi::um::securitybaseapi::GetTokenInformation;
91 use winapi::um::winnt::{TokenUser, HANDLE, TOKEN_QUERY, TOKEN_USER};
92
93 unsafe {
98 let mut token: HANDLE = ptr::null_mut();
99 if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
100 return Err(SidError::PlatformLookup(format!(
101 "OpenProcessToken failed (GetLastError={})",
102 GetLastError()
103 )));
104 }
105 let close_token = TokenHandle(token);
106
107 let mut required_size: u32 = 0;
109 let ok = GetTokenInformation(
110 close_token.0,
111 TokenUser,
112 ptr::null_mut(),
113 0,
114 &mut required_size,
115 );
116 let last = GetLastError();
118 if ok != 0 || last != ERROR_INSUFFICIENT_BUFFER {
119 return Err(SidError::PlatformLookup(format!(
120 "GetTokenInformation size query failed (ok={ok}, GetLastError={last})"
121 )));
122 }
123 if required_size == 0 {
124 return Err(SidError::PlatformLookup(
125 "GetTokenInformation reported 0 required bytes".into(),
126 ));
127 }
128
129 let mut buf: Vec<u8> = vec![0u8; required_size as usize];
130 if GetTokenInformation(
131 close_token.0,
132 TokenUser,
133 buf.as_mut_ptr().cast(),
134 required_size,
135 &mut required_size,
136 ) == 0
137 {
138 return Err(SidError::PlatformLookup(format!(
139 "GetTokenInformation real query failed (GetLastError={})",
140 GetLastError()
141 )));
142 }
143
144 let token_user: *const TOKEN_USER = buf.as_ptr().cast();
147 let sid_ptr = (*token_user).User.Sid;
148 if sid_ptr.is_null() {
149 return Err(SidError::PlatformLookup(
150 "TOKEN_USER returned a null SID pointer".into(),
151 ));
152 }
153 sid_to_string(sid_ptr)
154 }
155}
156
157#[cfg(windows)]
158struct TokenHandle(winapi::um::winnt::HANDLE);
159
160#[cfg(windows)]
161impl Drop for TokenHandle {
162 fn drop(&mut self) {
163 unsafe {
166 winapi::um::handleapi::CloseHandle(self.0);
167 }
168 }
169}
170
171#[cfg(windows)]
172unsafe fn sid_to_string(sid: winapi::um::winnt::PSID) -> Result<String, SidError> {
173 use winapi::um::securitybaseapi::{GetLengthSid, IsValidSid};
182
183 if IsValidSid(sid) == 0 {
184 return Err(SidError::PlatformLookup("IsValidSid returned false".into()));
185 }
186 let len = GetLengthSid(sid) as usize;
187 if len == 0 || len > 1024 {
188 return Err(SidError::PlatformLookup(format!(
189 "GetLengthSid returned implausible length {len}"
190 )));
191 }
192 let slice = std::slice::from_raw_parts(sid as *const u8, len);
193 let mut hex = String::with_capacity(len * 2);
197 for b in slice {
198 hex.push(nibble_to_hex(b >> 4));
199 hex.push(nibble_to_hex(b & 0x0F));
200 }
201 Ok(format!("windows-sid:{hex}"))
202}
203
204#[cfg(all(unix, not(target_os = "macos")))]
205fn linux_machine_id() -> Result<String, SidError> {
206 const PATHS: &[&str] = &["/etc/machine-id", "/var/lib/dbus/machine-id"];
207 for path in PATHS {
208 match std::fs::read_to_string(path) {
209 Ok(s) => {
210 let trimmed = s.trim();
211 if !trimmed.is_empty() {
212 return Ok(trimmed.to_string());
213 }
214 }
215 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
216 Err(err) => {
217 return Err(SidError::PlatformLookup(format!("read {path}: {err}")));
218 }
219 }
220 }
221 Err(SidError::PlatformLookup(
222 "no /etc/machine-id or /var/lib/dbus/machine-id found".into(),
223 ))
224}
225
226#[cfg(target_os = "macos")]
227fn macos_platform_uuid() -> Result<String, SidError> {
228 use std::process::Command;
229 let output = Command::new("ioreg")
233 .args(["-d2", "-c", "IOPlatformExpertDevice"])
234 .output()
235 .map_err(|e| SidError::PlatformLookup(format!("spawn ioreg: {e}")))?;
236 if !output.status.success() {
237 return Err(SidError::PlatformLookup(format!(
238 "ioreg failed (status={:?})",
239 output.status.code()
240 )));
241 }
242 let stdout = String::from_utf8_lossy(&output.stdout);
243 for line in stdout.lines() {
244 let line = line.trim();
245 if let Some(rest) = line.strip_prefix("\"IOPlatformUUID\"") {
246 if let Some(eq_idx) = rest.find('=') {
248 let value = rest[eq_idx + 1..].trim();
249 let unquoted = value.trim_matches('"');
250 if !unquoted.is_empty() {
251 return Ok(unquoted.to_string());
252 }
253 }
254 }
255 }
256 Err(SidError::PlatformLookup(
257 "ioreg output did not contain IOPlatformUUID".into(),
258 ))
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn hash_is_16_lowercase_hex() {
267 let h = hash_to_16_hex(b"sample-input");
268 assert_eq!(h.len(), 16, "hash must be 16 chars");
269 for c in h.chars() {
270 assert!(
271 c.is_ascii_digit() || ('a'..='f').contains(&c),
272 "non-lowercase-hex char in {h:?}"
273 );
274 }
275 }
276
277 #[test]
278 fn different_inputs_yield_different_hashes() {
279 let a = hash_to_16_hex(b"alice:machine-1");
280 let b = hash_to_16_hex(b"bob:machine-1");
281 assert_ne!(a, b);
282 }
283
284 #[test]
285 fn same_input_is_stable() {
286 let a = hash_to_16_hex(b"alice:machine-1");
287 let b = hash_to_16_hex(b"alice:machine-1");
288 assert_eq!(a, b);
289 }
290
291 #[test]
292 fn current_user_hash_resolves() {
293 match user_sid_hash() {
297 Ok(h) => {
298 assert_eq!(h.len(), 16);
299 }
300 Err(e) => {
301 eprintln!("user_sid_hash unavailable on this host: {e}");
302 }
303 }
304 }
305}