use std::sync::OnceLock;
pub const HOST_ID_DOMAIN: &[u8] = b"openlatch-hostid/1";
const TEST_SEAM_ENV: &str = "OPENLATCH_TEST_HOST_ID";
static HOST_ID: OnceLock<Option<String>> = OnceLock::new();
pub fn host_id() -> Option<String> {
HOST_ID.get_or_init(resolve).clone()
}
pub fn host_key(agent_id: &str) -> Option<String> {
key_from(host_id(), agent_id)
}
fn key_from(host_id: Option<String>, agent_id: &str) -> Option<String> {
match host_id {
Some(id) => Some(id),
None if !agent_id.is_empty() => Some(format!("agent:{agent_id}")),
None => None,
}
}
pub fn hash_machine_id(normalised: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(HOST_ID_DOMAIN);
hasher.update(normalised.as_bytes());
hex::encode(&hasher.finalize()[..16])
}
pub fn normalise_machine_id(raw: &str) -> Option<String> {
let trimmed = raw
.trim()
.trim_start_matches('{')
.trim_end_matches('}')
.trim();
if trimmed.is_empty() {
return None;
}
Some(trimmed.to_ascii_lowercase())
}
pub fn validate_linux_machine_id(normalised: &str) -> Option<String> {
if normalised == "uninitialized" {
return None;
}
let compact: String = normalised.chars().filter(|c| *c != '-').collect();
if compact.len() != 32 || !compact.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
if compact.chars().all(|c| c == '0') {
return None;
}
Some(compact)
}
fn resolve() -> Option<String> {
if let Some(seam) = std::env::var(TEST_SEAM_ENV).ok().filter(|v| !v.is_empty()) {
return if seam == "none" {
None
} else {
Some(hash_machine_id(&seam))
};
}
resolve_platform()
.and_then(|raw| normalise_machine_id(&raw))
.filter(|normalised| normalised.chars().any(|c| c != '0' && c != '-'))
.map(|normalised| hash_machine_id(&normalised))
}
#[cfg(target_os = "linux")]
const LINUX_MACHINE_ID_PATHS: [&str; 2] = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
#[cfg(target_os = "linux")]
fn resolve_platform() -> Option<String> {
let paths: Vec<&std::path::Path> = LINUX_MACHINE_ID_PATHS
.iter()
.map(std::path::Path::new)
.collect();
resolve_linux(&paths)
}
#[cfg(target_os = "linux")]
fn resolve_linux(paths: &[&std::path::Path]) -> Option<String> {
paths.iter().find_map(|path| {
std::fs::read_to_string(path)
.ok()
.and_then(|raw| normalise_machine_id(&raw))
.and_then(|normalised| validate_linux_machine_id(&normalised))
})
}
#[cfg(target_os = "macos")]
fn resolve_platform() -> Option<String> {
let mut raw = [0u8; 16];
let timeout = libc::timespec {
tv_sec: 5,
tv_nsec: 0,
};
let rc = unsafe { libc::gethostuuid(raw.as_mut_ptr(), &timeout) };
if rc != 0 {
return None;
}
Some(format_uuid(&raw))
}
#[cfg(target_os = "macos")]
fn format_uuid(bytes: &[u8; 16]) -> String {
let hex = hex::encode(bytes);
format!(
"{}-{}-{}-{}-{}",
&hex[0..8],
&hex[8..12],
&hex[12..16],
&hex[16..20],
&hex[20..32]
)
}
#[cfg(windows)]
fn resolve_platform() -> Option<String> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::System::Registry::{
RegCloseKey, RegOpenKeyExW, HKEY, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_64KEY,
};
const MAX_VALUE_BYTES: u32 = 64 * 1024;
fn wide(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
let subkey = wide("SOFTWARE\\Microsoft\\Cryptography");
let value = wide("MachineGuid");
let mut hkey: HKEY = std::ptr::null_mut();
let rc = unsafe {
RegOpenKeyExW(
HKEY_LOCAL_MACHINE,
subkey.as_ptr(),
0,
KEY_READ | KEY_WOW64_64KEY,
&mut hkey,
)
};
if rc != ERROR_SUCCESS {
return None;
}
let guid = read_machine_guid(hkey, &value, MAX_VALUE_BYTES);
unsafe { RegCloseKey(hkey) };
guid
}
#[cfg(windows)]
fn read_machine_guid(
hkey: windows_sys::Win32::System::Registry::HKEY,
value: &[u16],
max_bytes: u32,
) -> Option<String> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::System::Registry::{RegGetValueW, RRF_RT_REG_SZ};
let mut bytes: u32 = 0;
let rc = unsafe {
RegGetValueW(
hkey,
std::ptr::null(),
value.as_ptr(),
RRF_RT_REG_SZ,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut bytes,
)
};
if rc != ERROR_SUCCESS || bytes == 0 || bytes > max_bytes {
return None;
}
let mut buf = vec![0u16; bytes as usize / 2 + 1];
let mut written = bytes;
let rc = unsafe {
RegGetValueW(
hkey,
std::ptr::null(),
value.as_ptr(),
RRF_RT_REG_SZ,
std::ptr::null_mut(),
buf.as_mut_ptr().cast(),
&mut written,
)
};
if rc != ERROR_SUCCESS {
return None;
}
let len = (written as usize / 2).saturating_sub(1).min(buf.len());
let guid = OsString::from_wide(&buf[..len])
.to_string_lossy()
.into_owned();
if guid.is_empty() {
None
} else {
Some(guid)
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
fn resolve_platform() -> Option<String> {
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::daemon::identity::test_support::EnvGuard;
use crate::daemon::identity::ENV_LOCK;
const SAMPLE_ID: &str = "75f76b18-6d6c-4a7f-9f0e-2b1c3d4e5f60";
const SAMPLE_HASH: &str = "9262b296baa37a205734509345581251";
#[test]
fn hash_is_pinned_to_a_fixed_vector_and_to_its_domain() {
assert_eq!(hash_machine_id(SAMPLE_ID), SAMPLE_HASH);
use sha2::{Digest, Sha256};
let mut bare = Sha256::new();
bare.update(SAMPLE_ID.as_bytes());
assert_ne!(hex::encode(&bare.finalize()[..16]), SAMPLE_HASH);
}
#[test]
fn hash_is_always_32_lowercase_hex_characters() {
for input in [
"",
"a",
SAMPLE_ID,
"0f0e0d0c0b0a09080706050403020100",
"ÄÖÜ-non-ascii",
&"x".repeat(4096),
] {
let hashed = hash_machine_id(input);
assert_eq!(hashed.len(), 32, "wrong length for {input:?}");
assert!(
hashed
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()),
"not lower-case hex for {input:?}: {hashed}"
);
}
}
#[test]
fn normalise_strips_whitespace_braces_and_case() {
assert_eq!(
normalise_machine_id(" {ABC-def} ").as_deref(),
Some("abc-def")
);
assert_eq!(
normalise_machine_id("\n75F76B18-6D6C\n").as_deref(),
Some("75f76b18-6d6c")
);
assert_eq!(normalise_machine_id(" "), None);
assert_eq!(normalise_machine_id("{}"), None);
}
#[test]
fn linux_validity_rejects_every_shape_a_container_ships() {
assert_eq!(
validate_linux_machine_id("75f76b186d6c4a7f9f0e2b1c3d4e5f60").as_deref(),
Some("75f76b186d6c4a7f9f0e2b1c3d4e5f60")
);
assert_eq!(
validate_linux_machine_id(SAMPLE_ID).as_deref(),
Some("75f76b186d6c4a7f9f0e2b1c3d4e5f60")
);
assert_eq!(validate_linux_machine_id(""), None);
assert_eq!(validate_linux_machine_id("uninitialized"), None);
assert_eq!(validate_linux_machine_id(&"0".repeat(32)), None);
assert_eq!(validate_linux_machine_id("abc"), None);
assert_eq!(validate_linux_machine_id(&"z".repeat(32)), None);
}
#[test]
fn host_key_falls_back_to_the_agent_id_and_then_to_nothing() {
assert_eq!(
key_from(Some(SAMPLE_HASH.to_string()), "agt_x").as_deref(),
Some(SAMPLE_HASH)
);
assert_eq!(key_from(None, "agt_x").as_deref(), Some("agent:agt_x"));
assert_eq!(key_from(None, ""), None);
}
#[test]
fn seam_outranks_the_platform_and_an_empty_value_means_unset() {
let _lock = ENV_LOCK.blocking_lock();
let env = EnvGuard::clear();
env.set(TEST_SEAM_ENV, "none");
assert_eq!(resolve(), None, "`none` forces an absent hostid");
env.set(TEST_SEAM_ENV, SAMPLE_ID);
assert_eq!(
resolve().as_deref(),
Some(SAMPLE_HASH),
"a seam value is hashed exactly as a real identifier is"
);
env.unset(TEST_SEAM_ENV);
let unset = resolve();
env.set(TEST_SEAM_ENV, "");
assert_eq!(
resolve(),
unset,
"an empty seam must behave exactly as an unset one"
);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_reads_etc_first_and_falls_through_an_unusable_file() {
let dir = tempfile::tempdir().expect("tempdir");
let etc = dir.path().join("etc-machine-id");
let dbus = dir.path().join("dbus-machine-id");
std::fs::write(&etc, "75f76b186d6c4a7f9f0e2b1c3d4e5f60\n").expect("write etc");
std::fs::write(&dbus, "0123456789abcdef0123456789abcdef\n").expect("write dbus");
assert_eq!(
resolve_linux(&[etc.as_path(), dbus.as_path()]).as_deref(),
Some("75f76b186d6c4a7f9f0e2b1c3d4e5f60")
);
std::fs::write(&etc, "").expect("truncate etc");
assert_eq!(
resolve_linux(&[etc.as_path(), dbus.as_path()]).as_deref(),
Some("0123456789abcdef0123456789abcdef")
);
std::fs::write(&dbus, "uninitialized\n").expect("write dbus");
assert_eq!(resolve_linux(&[etc.as_path(), dbus.as_path()]), None);
assert_eq!(resolve_linux(&[dir.path().join("absent").as_path()]), None);
}
#[cfg(windows)]
#[test]
fn windows_machine_guid_is_a_real_guid() {
let raw = resolve_platform().expect("HKLM MachineGuid must exist on a Windows host");
assert_eq!(raw.trim().len(), 36, "MachineGuid is a 36-char GUID: {raw}");
let normalised = normalise_machine_id(&raw).expect("a GUID normalises");
assert_eq!(hash_machine_id(&normalised).len(), 32);
}
#[cfg(target_os = "macos")]
#[test]
fn macos_platform_uuid_has_the_canonical_shape() {
let raw = resolve_platform().expect("gethostuuid must answer on a booted Mac");
assert_eq!(raw.len(), 36, "8-4-4-4-12: {raw}");
assert_eq!(
raw.chars().filter(|c| *c == '-').count(),
4,
"four separators: {raw}"
);
assert_eq!(raw, raw.to_ascii_lowercase(), "lower-case: {raw}");
println!("macOS hostid={}", hash_machine_id(&raw));
}
}