Implement machine fingerprint hashing (T-501)

This commit is contained in:
ila
2026-07-20 00:19:13 +08:00
parent 94ea334d1c
commit 8d81276fd9
16 changed files with 465 additions and 9 deletions
+2
View File
@@ -0,0 +1,2 @@
// Package licensing provides pure, offline licensing primitives.
package licensing
+60
View File
@@ -0,0 +1,60 @@
package licensing
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
)
const machineHashDomain = "softbox.machine-hash.v1\x00"
// ErrInvalidMachineGUID reports a missing or malformed Windows MachineGuid.
// It intentionally contains no source value.
var ErrInvalidMachineGUID = errors.New("machine GUID is invalid")
// DeriveMachineHash returns the v1, machine-bound license hash for a normalized
// Windows MachineGuid and the Windows directory volume serial number. It never
// persists either source identifier.
func DeriveMachineHash(machineGUID string, systemVolumeSerial uint32) (string, error) {
normalizedGUID, err := normalizeMachineGUID(machineGUID)
if err != nil {
return "", err
}
input := machineHashDomain + normalizedGUID + "\x00" + fmt.Sprintf("%08x", systemVolumeSerial)
sum := sha256.Sum256([]byte(input))
return hex.EncodeToString(sum[:]), nil
}
func normalizeMachineGUID(value string) (string, error) {
value = strings.TrimSpace(value)
if len(value) != 36 {
return "", ErrInvalidMachineGUID
}
for index := 0; index < len(value); index++ {
character := value[index]
if character > 0x7f {
return "", ErrInvalidMachineGUID
}
if index == 8 || index == 13 || index == 18 || index == 23 {
if character != '-' {
return "", ErrInvalidMachineGUID
}
continue
}
if !isASCIHex(character) {
return "", ErrInvalidMachineGUID
}
}
return strings.ToLower(value), nil
}
func isASCIHex(character byte) bool {
return character >= '0' && character <= '9' ||
character >= 'a' && character <= 'f' ||
character >= 'A' && character <= 'F'
}
+53
View File
@@ -0,0 +1,53 @@
package licensing
import (
"errors"
"strings"
"testing"
)
func TestDeriveMachineHashCanonicalizesGUID(t *testing.T) {
const want = "0922bbbd8c44576d1afa6fe1309615963493cf2e1c4fddda20d2cb3ecb3c1dff"
got, err := DeriveMachineHash(" 6F9619FF-8B86-D011-B42D-00C04FC964FF\t", 0x1a2b3c4d)
if err != nil {
t.Fatalf("DeriveMachineHash() error = %v", err)
}
if got != want {
t.Fatalf("DeriveMachineHash() = %q, want %q", got, want)
}
if len(got) != 64 || strings.ToLower(got) != got {
t.Fatalf("DeriveMachineHash() = %q, want 64 lowercase hexadecimal characters", got)
}
}
func TestDeriveMachineHashChangesForVolumeSerial(t *testing.T) {
guid := "6f9619ff-8b86-d011-b42d-00c04fc964ff"
first, err := DeriveMachineHash(guid, 1)
if err != nil {
t.Fatalf("DeriveMachineHash(first) error = %v", err)
}
second, err := DeriveMachineHash(guid, 2)
if err != nil {
t.Fatalf("DeriveMachineHash(second) error = %v", err)
}
if first == second {
t.Fatal("DeriveMachineHash() did not include the volume serial number")
}
}
func TestDeriveMachineHashRejectsInvalidGUID(t *testing.T) {
tests := []string{
"",
"6f9619ff8b86-d011-b42d-00c04fc964ff",
"6f9619ff-8b86-d011-b42d-00c04fc964fg",
"6f9619ff-8b86-d011-b42d-00c04fc964f\x00",
"6f9619ff-8b86-d011-b42d-00c04fc964fé",
}
for _, machineGUID := range tests {
_, err := DeriveMachineHash(machineGUID, 1)
if !errors.Is(err, ErrInvalidMachineGUID) {
t.Fatalf("DeriveMachineHash(%q) error = %v, want ErrInvalidMachineGUID", machineGUID, err)
}
}
}