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!(
218 "read {path}: {err}"
219 )));
220 }
221 }
222 }
223 Err(SidError::PlatformLookup(
224 "no /etc/machine-id or /var/lib/dbus/machine-id found".into(),
225 ))
226}
227
228#[cfg(target_os = "macos")]
229fn macos_platform_uuid() -> Result<String, SidError> {
230 use std::process::Command;
231 let output = Command::new("ioreg")
235 .args(["-d2", "-c", "IOPlatformExpertDevice"])
236 .output()
237 .map_err(|e| SidError::PlatformLookup(format!("spawn ioreg: {e}")))?;
238 if !output.status.success() {
239 return Err(SidError::PlatformLookup(format!(
240 "ioreg failed (status={:?})",
241 output.status.code()
242 )));
243 }
244 let stdout = String::from_utf8_lossy(&output.stdout);
245 for line in stdout.lines() {
246 let line = line.trim();
247 if let Some(rest) = line.strip_prefix("\"IOPlatformUUID\"") {
248 if let Some(eq_idx) = rest.find('=') {
250 let value = rest[eq_idx + 1..].trim();
251 let unquoted = value.trim_matches('"');
252 if !unquoted.is_empty() {
253 return Ok(unquoted.to_string());
254 }
255 }
256 }
257 }
258 Err(SidError::PlatformLookup(
259 "ioreg output did not contain IOPlatformUUID".into(),
260 ))
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn hash_is_16_lowercase_hex() {
269 let h = hash_to_16_hex(b"sample-input");
270 assert_eq!(h.len(), 16, "hash must be 16 chars");
271 for c in h.chars() {
272 assert!(
273 c.is_ascii_digit() || ('a'..='f').contains(&c),
274 "non-lowercase-hex char in {h:?}"
275 );
276 }
277 }
278
279 #[test]
280 fn different_inputs_yield_different_hashes() {
281 let a = hash_to_16_hex(b"alice:machine-1");
282 let b = hash_to_16_hex(b"bob:machine-1");
283 assert_ne!(a, b);
284 }
285
286 #[test]
287 fn same_input_is_stable() {
288 let a = hash_to_16_hex(b"alice:machine-1");
289 let b = hash_to_16_hex(b"alice:machine-1");
290 assert_eq!(a, b);
291 }
292
293 #[test]
294 fn current_user_hash_resolves() {
295 match user_sid_hash() {
299 Ok(h) => {
300 assert_eq!(h.len(), 16);
301 }
302 Err(e) => {
303 eprintln!("user_sid_hash unavailable on this host: {e}");
304 }
305 }
306 }
307}