Skip to main content

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}