196 lines
4.8 KiB
Go
196 lines
4.8 KiB
Go
// 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, ",")
|
|
}
|