running_process/broker/lifecycle/sid.rs
1//! Per-user identity hash used by every broker pipe name.
2//!
3//! Returns a 16-character lowercase hex string (the first 8 bytes of a
4//! blake3 digest, hex-encoded). Stable across runs for the same user
5//! on the same machine; collision resistant in practice.
6//!
7//! ## Platform inputs
8//!
9//! | Platform | Hash input |
10//! |----------|------------|
11//! | Windows | The current process token user SID, in `S-1-...` text form, obtained via `OpenProcessToken(GetCurrentProcess())` → `GetTokenInformation(TokenUser)` → `ConvertSidToStringSidW`. |
12//! | Linux | `format!("{uid}:{machine_id}")` where `machine_id` is the contents of `/etc/machine-id`, falling back to `/var/lib/dbus/machine-id`, then to a boot-scoped `boot:<boot_id>` kernel identity when neither file exists (minimal containers). |
13//! | macOS | `format!("{uid}:{machine_uuid}")` where `machine_uuid` comes from `ioreg -d2 -c IOPlatformExpertDevice` (the `IOPlatformUUID` field). |
14//!
15//! ## Why a hash?
16//!
17//! Pipe-name length limits are tight: Windows MAX_PATH (260) and the
18//! macOS `sun_path` field (104 bytes). A blake3 16-char hex is short,
19//! collision-resistant for the namespace size we care about
20//! (per-machine per-user), and avoids leaking the literal SID or
21//! machine UUID into world-readable filesystem paths.
22
23/// Errors that can prevent computing the user SID hash.
24#[derive(Debug, thiserror::Error)]
25pub enum SidError {
26 /// Could not read the platform user identity (e.g. machine-id
27 /// missing, ioreg unavailable, OpenProcessToken failed).
28 #[error("failed to read platform user identity: {0}")]
29 PlatformLookup(String),
30}
31
32/// Return the 16-character lowercase hex blake3 hash of the current
33/// user's platform identity. Stable across runs.
34pub fn user_sid_hash() -> Result<String, SidError> {
35 let input = platform_identity_string()?;
36 Ok(hash_to_16_hex(input.as_bytes()))
37}
38
39/// Hash arbitrary bytes to 16 lowercase hex characters using blake3.
40///
41/// Exposed for testing and for the rare caller that wants to hash a
42/// non-default identity string (e.g. a CI runner ID).
43pub fn hash_to_16_hex(input: &[u8]) -> String {
44 let digest = blake3::hash(input);
45 let bytes = digest.as_bytes();
46 // 8 bytes → 16 hex chars.
47 let mut out = String::with_capacity(16);
48 for b in &bytes[..8] {
49 // Lowercase hex, fixed width.
50 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 crate::platform::host::user_machine_identity()
67 .map_err(|error| SidError::PlatformLookup(error.to_string()))
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn hash_is_16_lowercase_hex() {
76 let h = hash_to_16_hex(b"sample-input");
77 assert_eq!(h.len(), 16, "hash must be 16 chars");
78 for c in h.chars() {
79 assert!(
80 c.is_ascii_digit() || ('a'..='f').contains(&c),
81 "non-lowercase-hex char in {h:?}"
82 );
83 }
84 }
85
86 #[test]
87 fn different_inputs_yield_different_hashes() {
88 let a = hash_to_16_hex(b"alice:machine-1");
89 let b = hash_to_16_hex(b"bob:machine-1");
90 assert_ne!(a, b);
91 }
92
93 #[test]
94 fn same_input_is_stable() {
95 let a = hash_to_16_hex(b"alice:machine-1");
96 let b = hash_to_16_hex(b"alice:machine-1");
97 assert_eq!(a, b);
98 }
99
100 #[test]
101 fn current_user_hash_resolves() {
102 // On a healthy dev machine this should succeed on all three
103 // platforms. CI containers without /etc/machine-id will skip
104 // (we don't want to make this test platform-fragile).
105 match user_sid_hash() {
106 Ok(h) => {
107 assert_eq!(h.len(), 16);
108 }
109 Err(e) => {
110 eprintln!("user_sid_hash unavailable on this host: {e}");
111 }
112 }
113 }
114}