machine-uid 0.6.0

Get os native machine id without root permission.
Documentation
// See README.md and LICENSE for more details.

//! Get os native machine id without root permission.

//! ## About machine id
//!
//! In Linux, machine id is a single newline-terminated, hexadecimal, 32-character, lowercase ID.
//! When decoded from hexadecimal, this corresponds to a 16-byte/128-bit value.
//! This ID may not be all zeros.
//! This ID uniquely identifies the host. It should be considered "confidential",
//! and must not be exposed in untrusted environments.
//! And do note that the machine id can be re-generated by root.
//!
//! ## Usage
//!
//! ```Rust
//! extern crate machine_uid;
//!
//! fn main() {
//!     let id: String = machine_uid::get().unwrap();
//!     println!("{}", id);
//! }
//! ```
//!
//! ## How it works
//!
//! It get machine id from following source:
//!
//! Linux or who use systemd:
//!
//! ```Bash
//! cat /var/lib/dbus/machine-id # or /etc/machine-id
//! ```
//!
//! BSD:
//!
//! ```Bash
//! cat /etc/hostid # or kenv -q smbios.system.uuid
//! ```
//!
//! OSX:
//!
//! ```C
//! gethostuuid(3) // same value as `ioreg -rd1 -c IOPlatformExpertDevice`
//! ```
//!
//! Windows:
//!
//! ```powershell
//! (Get-ItemProperty -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography).MachineGuid
//! ```
//!
//! illumos:
//!
//! ```Bash
//! gethostid(3C)
//! ```
//!
//! ## Supported Platform
//!
//! I have tested in following platform:
//!
//! - Debian 8
//! - OS X 10.6
//! - FreeBSD 10.4
//! - Fedora 28
//! - Windows 10
//! - OmniOS r151050
//!

use std::error::Error;
use std::fs::File;
use std::io::prelude::*;

#[allow(dead_code)]
fn read_file(file_path: &str) -> Result<String, Box<dyn Error>> {
    let mut fd = File::open(file_path)?;
    let mut content = String::new();
    fd.read_to_string(&mut content)?;
    Ok(content.trim().to_string())
}

#[cfg(target_os = "linux")]
pub mod machine_id {
    use super::read_file;
    use std::error::Error;

    // dbusPath is the default path for dbus machine id.
    const DBUS_PATH: &str = "/var/lib/dbus/machine-id";
    // or when not found (e.g. Fedora 20)
    const DBUS_PATH_ETC: &str = "/etc/machine-id";

    /// Return machine id
    pub fn get_machine_id() -> Result<String, Box<dyn Error>> {
        match read_file(DBUS_PATH) {
            Ok(machine_id) => Ok(machine_id),
            Err(_) => Ok(read_file(DBUS_PATH_ETC)?),
        }
    }
}

#[cfg(any(
    target_os = "freebsd",
    target_os = "dragonfly",
    target_os = "openbsd",
    target_os = "netbsd"
))]
pub mod machine_id {
    use super::read_file;
    use std::error::Error;
    use std::process::Command;

    const HOST_ID_PATH: &str = "/etc/hostid";

    /// Return machine id
    pub fn get_machine_id() -> Result<String, Box<dyn Error>> {
        match read_file(HOST_ID_PATH) {
            Ok(machine_id) => Ok(machine_id),
            Err(_) => Ok(read_from_kenv()?),
        }
    }

    fn read_from_kenv() -> Result<String, Box<dyn Error>> {
        let output = Command::new("kenv")
            .args(&["-q", "smbios.system.uuid"])
            .output()?;
        let content = String::from_utf8_lossy(&output.stdout);
        Ok(content.trim().to_string())
    }
}

#[cfg(target_os = "macos")]
mod machine_id {
    // Returns the same UUID exposed by `ioreg -rd1 -c IOPlatformExpertDevice`
    // (i.e. `IOPlatformUUID`), but via the BSD `gethostuuid(3)` syscall.
    //
    // Both surfaces are fronted by the same kernel data, so the value is
    // bit-for-bit identical (same hyphenation, same uppercase hex). Going
    // through libc avoids a fork+exec of `ioreg` per call, which on macOS
    // costs ~90ms (subprocess + dyld + IOKit init) versus ~10µs for the
    // syscall.
    use std::error::Error;
    use std::io;
    #[cfg(test)]
    use std::process::Command;

    /// Return machine id
    pub fn get_machine_id() -> Result<String, Box<dyn Error>> {
        let mut uuid: [u8; 16] = [0; 16];
        // A zero `timespec` requests an indefinite wait. In practice the
        // call returns immediately; the timeout only matters when the
        // hostuuid has not yet been initialized very early in boot.
        let wait = libc::timespec {
            tv_sec: 0,
            tv_nsec: 0,
        };
        let rc = unsafe { libc::gethostuuid(uuid.as_mut_ptr(), &wait) };
        if rc != 0 {
            return Err(Box::new(io::Error::last_os_error()));
        }
        Ok(format!(
            "{:02X}{:02X}{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
            uuid[0],
            uuid[1],
            uuid[2],
            uuid[3],
            uuid[4],
            uuid[5],
            uuid[6],
            uuid[7],
            uuid[8],
            uuid[9],
            uuid[10],
            uuid[11],
            uuid[12],
            uuid[13],
            uuid[14],
            uuid[15],
        ))
    }

    /// Return machine id using the legacy `ioreg`-based implementation.
    #[cfg(test)]
    fn get_machine_id_legacy() -> Result<String, Box<dyn Error>> {
        let output = Command::new("ioreg")
            .args(["-rd1", "-c", "IOPlatformExpertDevice"])
            .output()?;
        let content = String::from_utf8_lossy(&output.stdout);
        extract_id(&content)
    }

    #[cfg(test)]
    fn extract_id(content: &str) -> Result<String, Box<dyn Error>> {
        let lines = content.split('\n');
        for line in lines {
            if line.contains("IOPlatformUUID") {
                let k: Vec<&str> = line.rsplitn(2, '=').collect();
                let id = k[0].trim_matches(|c: char| c == '"' || c.is_whitespace());
                return Ok(id.to_string());
            }
        }
        Err(From::from(
            "No matching IOPlatformUUID in `ioreg -rd1 -c IOPlatformExpertDevice` command.",
        ))
    }

    #[test]
    fn test_macos_get_matches_legacy_ioreg() {
        let id = get_machine_id().unwrap();
        let legacy_id = get_machine_id_legacy().unwrap();

        assert_eq!(id, legacy_id);
    }
}

#[cfg(target_os = "windows")]
pub mod machine_id {
    use std::error::Error;

    use windows_registry::LOCAL_MACHINE;
    use windows_sys::Win32::Foundation::GetLastError;
    use windows_sys::Win32::System::Registry::{KEY_READ, KEY_WOW64_64KEY};
    use windows_sys::Win32::System::Threading::{GetCurrentProcess, IsWow64Process};

    fn machine_uid_is_wow64() -> Result<bool, Box<dyn Error>> {
        unsafe {
            let mut is_wow64: i32 = 0;
            if IsWow64Process(GetCurrentProcess(), &mut is_wow64) == 0 {
                return Err(From::from(format!("Failed to determine whether the specified process is running under WOW64 or an Intel64 of x64 processor: {}", GetLastError())));
            }

            Ok(is_wow64 == 1)
        }
    }

    /// Return machine id
    pub fn get_machine_id() -> Result<String, Box<dyn Error>> {
        let flag = if machine_uid_is_wow64()? && cfg!(target_pointer_width = "32") {
            KEY_READ | KEY_WOW64_64KEY
        } else {
            KEY_READ
        };

        let key = LOCAL_MACHINE
            .options()
            .access(flag)
            .open("SOFTWARE\\Microsoft\\Cryptography")?;
        let id = key.get_string("MachineGuid")?;

        Ok(id.trim().to_string())
    }
}

#[cfg(target_os = "illumos")]
pub mod machine_id {
    use std::error::Error;

    /// Return machine id
    pub fn get_machine_id() -> Result<String, Box<dyn Error>> {
        Ok(format!("{:x}", unsafe { libc::gethostid() }))
    }
}

pub use machine_id::get_machine_id as get;