// Package hwid collects stable hardware identifiers to bind licenses // to a specific machine. Works on Linux, Windows, and macOS. package hwid import ( "crypto/sha256" "fmt" "net" "os" "os/exec" "runtime" "strings" ) // Fingerprint holds raw hardware identifiers collected from the system. type Fingerprint struct { MACAddresses []string // Physical network interface MACs Hostname string CPUInfo string // CPU model / identifier MachineID string // OS-level machine UUID OS string // runtime.GOOS } // Collect gathers hardware identifiers from the current machine. func Collect() (*Fingerprint, error) { fp := &Fingerprint{OS: runtime.GOOS} fp.Hostname, _ = os.Hostname() fp.MACAddresses = collectMACs() fp.CPUInfo = collectCPU() fp.MachineID = collectMachineID() return fp, nil } // Hash returns a stable SHA-256 fingerprint string derived from the // collected identifiers. Use this as the machine ID in your license claims. // The hash is hex-encoded and always 64 characters long. func (f *Fingerprint) Hash() string { // Sort MACs for stability (interface order can vary) macs := stableJoin(f.MACAddresses) parts := []string{ "mac=" + macs, "host=" + f.Hostname, "cpu=" + f.CPUInfo, "mid=" + f.MachineID, "os=" + f.OS, } raw := strings.Join(parts, "|") sum := sha256.Sum256([]byte(raw)) return fmt.Sprintf("%x", sum) } // --- MAC addresses ------------------------------------------------------- // collectMACs returns the hardware MAC addresses of all physical interfaces. // Loopback and virtual interfaces are excluded. func collectMACs() []string { ifaces, err := net.Interfaces() if err != nil { return nil } var macs []string for _, iface := range ifaces { mac := iface.HardwareAddr.String() if mac == "" { continue } // Skip loopback, virtual, docker, VPN interfaces name := strings.ToLower(iface.Name) if isVirtual(name) { continue } macs = append(macs, strings.ToUpper(mac)) } return macs } func isVirtual(name string) bool { prefixes := []string{"lo", "docker", "veth", "br-", "virbr", "vmnet", "vbox", "utun", "tun", "tap"} for _, p := range prefixes { if strings.HasPrefix(name, p) { return true } } return false } // --- CPU info ------------------------------------------------------------ func collectCPU() string { switch runtime.GOOS { case "linux": return readLinuxCPU() case "darwin": return runCmd("sysctl", "-n", "machdep.cpu.brand_string") case "windows": return runCmd("wmic", "cpu", "get", "ProcessorId", "/value") } return "" } func readLinuxCPU() string { data, err := os.ReadFile("/proc/cpuinfo") if err != nil { return "" } for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "model name") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { return strings.TrimSpace(parts[1]) } } } return "" } // --- Machine ID ---------------------------------------------------------- // collectMachineID reads the OS-level machine UUID: // - Linux: /etc/machine-id (systemd) or /var/lib/dbus/machine-id // - macOS: ioreg hardware UUID // - Windows: registry MachineGuid func collectMachineID() string { switch runtime.GOOS { case "linux": return readLinuxMachineID() case "darwin": out := runCmd("ioreg", "-rd1", "-c", "IOPlatformExpertDevice") return extractIORegValue(out, "IOPlatformUUID") case "windows": out := runCmd("reg", "query", `HKLM\SOFTWARE\Microsoft\Cryptography`, "/v", "MachineGuid") return parseRegOutput(out) } return "" } func readLinuxMachineID() string { for _, path := range []string{"/etc/machine-id", "/var/lib/dbus/machine-id"} { data, err := os.ReadFile(path) if err == nil { return strings.TrimSpace(string(data)) } } return "" } func extractIORegValue(output, key string) string { for _, line := range strings.Split(output, "\n") { if strings.Contains(line, key) { parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { return strings.Trim(strings.TrimSpace(parts[1]), `"`) } } } return "" } func parseRegOutput(output string) string { for _, line := range strings.Split(output, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "MachineGuid") { parts := strings.Fields(line) if len(parts) >= 3 { return parts[len(parts)-1] } } } return "" } // --- Helpers ------------------------------------------------------------- func runCmd(name string, args ...string) string { out, err := exec.Command(name, args...).Output() if err != nil { return "" } return strings.TrimSpace(string(out)) } func stableJoin(ss []string) string { // Simple insertion sort for stability without importing sort a := append([]string(nil), ss...) for i := 1; i < len(a); i++ { for j := i; j > 0 && a[j] < a[j-1]; j-- { a[j], a[j-1] = a[j-1], a[j] } } return strings.Join(a, ",") }