#[derive(Debug, thiserror::Error)]
pub enum SidError {
#[error("failed to read platform user identity: {0}")]
PlatformLookup(String),
}
pub fn user_sid_hash() -> Result<String, SidError> {
let input = platform_identity_string()?;
Ok(hash_to_16_hex(input.as_bytes()))
}
pub fn hash_to_16_hex(input: &[u8]) -> String {
let digest = blake3::hash(input);
let bytes = digest.as_bytes();
let mut out = String::with_capacity(16);
for b in &bytes[..8] {
out.push(nibble_to_hex(b >> 4));
out.push(nibble_to_hex(b & 0x0F));
}
out
}
#[inline]
fn nibble_to_hex(n: u8) -> char {
match n {
0..=9 => (b'0' + n) as char,
10..=15 => (b'a' + (n - 10)) as char,
_ => unreachable!("nibble out of range"),
}
}
fn platform_identity_string() -> Result<String, SidError> {
#[cfg(windows)]
{
windows_current_user_sid()
}
#[cfg(target_os = "macos")]
{
let uid = unsafe { libc::getuid() };
let uuid = macos_platform_uuid()?;
Ok(format!("{uid}:{uuid}"))
}
#[cfg(all(unix, not(target_os = "macos")))]
{
let uid = unsafe { libc::getuid() };
let machine_id = linux_machine_id()?;
Ok(format!("{uid}:{machine_id}"))
}
}
#[cfg(windows)]
fn windows_current_user_sid() -> Result<String, SidError> {
use std::ptr;
use winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER;
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
use winapi::um::securitybaseapi::GetTokenInformation;
use winapi::um::winnt::{TokenUser, HANDLE, TOKEN_QUERY, TOKEN_USER};
unsafe {
let mut token: HANDLE = ptr::null_mut();
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
return Err(SidError::PlatformLookup(format!(
"OpenProcessToken failed (GetLastError={})",
GetLastError()
)));
}
let close_token = TokenHandle(token);
let mut required_size: u32 = 0;
let ok = GetTokenInformation(
close_token.0,
TokenUser,
ptr::null_mut(),
0,
&mut required_size,
);
let last = GetLastError();
if ok != 0 || last != ERROR_INSUFFICIENT_BUFFER {
return Err(SidError::PlatformLookup(format!(
"GetTokenInformation size query failed (ok={ok}, GetLastError={last})"
)));
}
if required_size == 0 {
return Err(SidError::PlatformLookup(
"GetTokenInformation reported 0 required bytes".into(),
));
}
let mut buf: Vec<u8> = vec![0u8; required_size as usize];
if GetTokenInformation(
close_token.0,
TokenUser,
buf.as_mut_ptr().cast(),
required_size,
&mut required_size,
) == 0
{
return Err(SidError::PlatformLookup(format!(
"GetTokenInformation real query failed (GetLastError={})",
GetLastError()
)));
}
let token_user: *const TOKEN_USER = buf.as_ptr().cast();
let sid_ptr = (*token_user).User.Sid;
if sid_ptr.is_null() {
return Err(SidError::PlatformLookup(
"TOKEN_USER returned a null SID pointer".into(),
));
}
sid_to_string(sid_ptr)
}
}
#[cfg(windows)]
struct TokenHandle(winapi::um::winnt::HANDLE);
#[cfg(windows)]
impl Drop for TokenHandle {
fn drop(&mut self) {
unsafe {
winapi::um::handleapi::CloseHandle(self.0);
}
}
}
#[cfg(windows)]
unsafe fn sid_to_string(sid: winapi::um::winnt::PSID) -> Result<String, SidError> {
use winapi::um::securitybaseapi::{GetLengthSid, IsValidSid};
if IsValidSid(sid) == 0 {
return Err(SidError::PlatformLookup("IsValidSid returned false".into()));
}
let len = GetLengthSid(sid) as usize;
if len == 0 || len > 1024 {
return Err(SidError::PlatformLookup(format!(
"GetLengthSid returned implausible length {len}"
)));
}
let slice = std::slice::from_raw_parts(sid as *const u8, len);
let mut hex = String::with_capacity(len * 2);
for b in slice {
hex.push(nibble_to_hex(b >> 4));
hex.push(nibble_to_hex(b & 0x0F));
}
Ok(format!("windows-sid:{hex}"))
}
#[cfg(all(unix, not(target_os = "macos")))]
fn linux_machine_id() -> Result<String, SidError> {
const PATHS: &[&str] = &["/etc/machine-id", "/var/lib/dbus/machine-id"];
for path in PATHS {
match std::fs::read_to_string(path) {
Ok(s) => {
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
Err(err) => {
return Err(SidError::PlatformLookup(format!("read {path}: {err}")));
}
}
}
Err(SidError::PlatformLookup(
"no /etc/machine-id or /var/lib/dbus/machine-id found".into(),
))
}
#[cfg(target_os = "macos")]
fn macos_platform_uuid() -> Result<String, SidError> {
use std::process::Command;
let output = Command::new("ioreg")
.args(["-d2", "-c", "IOPlatformExpertDevice"])
.output()
.map_err(|e| SidError::PlatformLookup(format!("spawn ioreg: {e}")))?;
if !output.status.success() {
return Err(SidError::PlatformLookup(format!(
"ioreg failed (status={:?})",
output.status.code()
)));
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("\"IOPlatformUUID\"") {
if let Some(eq_idx) = rest.find('=') {
let value = rest[eq_idx + 1..].trim();
let unquoted = value.trim_matches('"');
if !unquoted.is_empty() {
return Ok(unquoted.to_string());
}
}
}
}
Err(SidError::PlatformLookup(
"ioreg output did not contain IOPlatformUUID".into(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_is_16_lowercase_hex() {
let h = hash_to_16_hex(b"sample-input");
assert_eq!(h.len(), 16, "hash must be 16 chars");
for c in h.chars() {
assert!(
c.is_ascii_digit() || ('a'..='f').contains(&c),
"non-lowercase-hex char in {h:?}"
);
}
}
#[test]
fn different_inputs_yield_different_hashes() {
let a = hash_to_16_hex(b"alice:machine-1");
let b = hash_to_16_hex(b"bob:machine-1");
assert_ne!(a, b);
}
#[test]
fn same_input_is_stable() {
let a = hash_to_16_hex(b"alice:machine-1");
let b = hash_to_16_hex(b"alice:machine-1");
assert_eq!(a, b);
}
#[test]
fn current_user_hash_resolves() {
match user_sid_hash() {
Ok(h) => {
assert_eq!(h.len(), 16);
}
Err(e) => {
eprintln!("user_sid_hash unavailable on this host: {e}");
}
}
}
}