initital commit
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
// 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, ",")
|
||||
}
|
||||
Reference in New Issue
Block a user