package main import ( "crypto/ed25519" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "time" ) // --------------------------------------------------------------------------- // License config and public status types // --------------------------------------------------------------------------- type LicenseConfig struct { Enabled bool `yaml:"enabled"` TrialDays int `yaml:"trial_days"` RequireAfterTrial bool `yaml:"require_after_trial"` DataDir string `yaml:"data_dir"` PublicKeyBase64 string `yaml:"public_key_base64"` ProductCode string `yaml:"product_code"` } const embeddedLicensePublicKeyBase64 = "k0k+ZtOpDWTyO8+uJY9+yL2S/ZzOxyBbaUldw1SJDGc=" var embeddedLicensePolicy = LicenseConfig{ Enabled: true, TrialDays: 7, RequireAfterTrial: true, DataDir: "license", PublicKeyBase64: embeddedLicensePublicKeyBase64, ProductCode: "force_monitor", } func runtimeLicenseConfig() LicenseConfig { return embeddedLicensePolicy } type ActivationRequest struct { App string `json:"app"` Version string `json:"version"` GeneratedAt string `json:"generated_at"` Hostname string `json:"hostname"` Platform string `json:"platform"` Fingerprint string `json:"fingerprint"` FingerprintShort string `json:"fingerprint_short"` Components []string `json:"components"` } type LicenseStatus struct { Enabled bool `json:"enabled"` Mode string `json:"mode"` Locked bool `json:"locked"` Message string `json:"message"` Fingerprint string `json:"fingerprint"` FingerprintShort string `json:"fingerprint_short"` Hostname string `json:"hostname"` TrialDays int `json:"trial_days"` DaysRemaining int `json:"days_remaining"` TrialStartedAt string `json:"trial_started_at,omitempty"` TrialExpiresAt string `json:"trial_expires_at,omitempty"` Customer string `json:"customer,omitempty"` LicenseID string `json:"license_id,omitempty"` ExpiresAt string `json:"expires_at,omitempty"` Features []string `json:"features,omitempty"` ActivationConfigured bool `json:"activation_configured"` Tampered bool `json:"tampered"` } type TrialState struct { FirstRunUTC string `json:"first_run_utc"` LastSeenUTC string `json:"last_seen_utc"` Fingerprint string `json:"fingerprint"` Checksum string `json:"checksum"` Tampered bool `json:"tampered"` } type SignedLicense struct { App string `json:"app"` LicenseID string `json:"license_id"` Customer string `json:"customer"` Fingerprint string `json:"fingerprint"` IssuedAt string `json:"issued_at"` ExpiresAt string `json:"expires_at,omitempty"` Features []string `json:"features,omitempty"` Signature string `json:"signature"` } type licensePayload struct { App string `json:"app"` LicenseID string `json:"license_id"` Customer string `json:"customer"` Fingerprint string `json:"fingerprint"` IssuedAt string `json:"issued_at"` ExpiresAt string `json:"expires_at,omitempty"` Features []string `json:"features,omitempty"` } type LicenseManager struct { mu sync.RWMutex cfg LicenseConfig dataDir string trialPath string licensePath string publicKey ed25519.PublicKey fingerprint string components []string hostname string trial TrialState active *SignedLicense lastStatus LicenseStatus } const trialSalt = "force-monitor-trial-v1" // --------------------------------------------------------------------------- // Constructor // --------------------------------------------------------------------------- func NewLicenseManager(cfg LicenseConfig, dataDir string) (*LicenseManager, error) { if !cfg.Enabled { return &LicenseManager{cfg: cfg, dataDir: dataDir}, nil } if cfg.TrialDays <= 0 { cfg.TrialDays = 7 } if strings.TrimSpace(cfg.ProductCode) == "" { cfg.ProductCode = "force_monitor" } if strings.TrimSpace(dataDir) == "" { dataDir = "license" } if err := os.MkdirAll(dataDir, 0o755); err != nil { return nil, fmt.Errorf("create license data dir: %w", err) } fp, comps, err := buildMachineFingerprint(cfg.ProductCode) if err != nil { return nil, err } hostname, _ := os.Hostname() m := &LicenseManager{ cfg: cfg, dataDir: dataDir, trialPath: filepath.Join(dataDir, "trial_state.json"), licensePath: filepath.Join(dataDir, "license.json"), fingerprint: fp, components: comps, hostname: hostname, } if strings.TrimSpace(cfg.PublicKeyBase64) != "" { pk, err := base64.StdEncoding.DecodeString(strings.TrimSpace(cfg.PublicKeyBase64)) if err != nil { return nil, fmt.Errorf("decode license public key: %w", err) } if len(pk) != ed25519.PublicKeySize { return nil, fmt.Errorf("invalid license public key size") } m.publicKey = ed25519.PublicKey(pk) } if err := m.loadOrCreateTrial(); err != nil { return nil, err } if err := m.loadExistingLicense(); err != nil { return nil, err } m.refreshStatusLocked() return m, nil } // --------------------------------------------------------------------------- // Public methods // --------------------------------------------------------------------------- func (m *LicenseManager) BuildActivationRequest() ActivationRequest { m.mu.RLock() defer m.mu.RUnlock() return ActivationRequest{ App: m.cfg.ProductCode, Version: version, GeneratedAt: time.Now().UTC().Format(time.RFC3339), Hostname: m.hostname, Platform: runtime.GOOS + "/" + runtime.GOARCH, Fingerprint: m.fingerprint, FingerprintShort: shortFingerprint(m.fingerprint), Components: append([]string(nil), m.components...), } } func (m *LicenseManager) Status() LicenseStatus { m.mu.Lock() defer m.mu.Unlock() m.refreshStatusLocked() return m.lastStatus } func (m *LicenseManager) Touch() error { if m == nil || !m.cfg.Enabled { return nil } m.mu.Lock() defer m.mu.Unlock() now := time.Now().UTC() lastSeen, _ := time.Parse(time.RFC3339, m.trial.LastSeenUTC) if !lastSeen.IsZero() && now.Add(2*time.Minute).Before(lastSeen) { m.trial.Tampered = true } if !lastSeen.IsZero() && now.Sub(lastSeen) < 15*time.Minute && !m.trial.Tampered { m.refreshStatusLocked() return nil } m.trial.LastSeenUTC = now.Format(time.RFC3339) m.trial.Fingerprint = m.fingerprint m.trial.Checksum = m.signTrialState(m.trial) if err := writeJSONFileAtomic(m.trialPath, m.trial); err != nil { return err } m.refreshStatusLocked() return nil } func (m *LicenseManager) ActivateFromText(text string) error { m.mu.Lock() defer m.mu.Unlock() if !m.cfg.Enabled { return errors.New("licensing disabled") } if len(m.publicKey) != ed25519.PublicKeySize { return errors.New("no license public key configured; set the embedded verifier public key") } text = strings.TrimSpace(text) if text == "" { return errors.New("license text is empty") } var lic SignedLicense if err := json.Unmarshal([]byte(text), &lic); err != nil { return fmt.Errorf("parse license json: %w", err) } if err := m.validateLicenseLocked(lic); err != nil { return err } if err := os.MkdirAll(filepath.Dir(m.licensePath), 0o755); err != nil { return fmt.Errorf("create license dir: %w", err) } if err := writeBytesAtomic(m.licensePath, []byte(text)); err != nil { return fmt.Errorf("write license: %w", err) } m.active = &lic m.refreshStatusLocked() return nil } // --------------------------------------------------------------------------- // Internal load / validate // --------------------------------------------------------------------------- func (m *LicenseManager) loadOrCreateTrial() error { state, err := readJSONFile[TrialState](m.trialPath) if err != nil { if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("read trial state: %w", err) } now := time.Now().UTC().Format(time.RFC3339) state = TrialState{ FirstRunUTC: now, LastSeenUTC: now, Fingerprint: m.fingerprint, } state.Checksum = m.signTrialState(state) if err := writeJSONFileAtomic(m.trialPath, state); err != nil { return fmt.Errorf("create trial state: %w", err) } } if state.Checksum != m.signTrialState(state) { state.Tampered = true } if state.Fingerprint != "" && state.Fingerprint != m.fingerprint { state.Tampered = true } m.trial = state return nil } func (m *LicenseManager) loadExistingLicense() error { data, err := os.ReadFile(m.licensePath) if err != nil { if errors.Is(err, os.ErrNotExist) { m.active = nil return nil } return fmt.Errorf("read existing license: %w", err) } var lic SignedLicense if err := json.Unmarshal(data, &lic); err != nil { return fmt.Errorf("parse existing license: %w", err) } if err := m.validateLicenseLocked(lic); err != nil { m.active = nil return nil } m.active = &lic return nil } func (m *LicenseManager) validateLicenseLocked(lic SignedLicense) error { if strings.TrimSpace(lic.App) == "" { return errors.New("license app is empty") } if lic.App != m.cfg.ProductCode { return fmt.Errorf("license app mismatch: got %q want %q", lic.App, m.cfg.ProductCode) } if strings.TrimSpace(lic.Fingerprint) == "" { return errors.New("license fingerprint is empty") } if !strings.EqualFold(strings.TrimSpace(lic.Fingerprint), strings.TrimSpace(m.fingerprint)) { return errors.New("license fingerprint does not match this machine") } if strings.TrimSpace(lic.LicenseID) == "" { return errors.New("license_id is required") } if strings.TrimSpace(lic.IssuedAt) == "" { return errors.New("issued_at is required") } if _, err := time.Parse(time.RFC3339, lic.IssuedAt); err != nil { return fmt.Errorf("invalid issued_at: %w", err) } if strings.TrimSpace(lic.ExpiresAt) != "" { exp, err := time.Parse(time.RFC3339, lic.ExpiresAt) if err != nil { return fmt.Errorf("invalid expires_at: %w", err) } if time.Now().UTC().After(exp) { return errors.New("license has expired") } } if len(m.publicKey) != ed25519.PublicKeySize { return errors.New("no public key configured") } sig, err := base64.StdEncoding.DecodeString(strings.TrimSpace(lic.Signature)) if err != nil { return fmt.Errorf("decode license signature: %w", err) } payload := licensePayload{ App: lic.App, LicenseID: lic.LicenseID, Customer: lic.Customer, Fingerprint: lic.Fingerprint, IssuedAt: lic.IssuedAt, ExpiresAt: lic.ExpiresAt, Features: lic.Features, } b, err := json.Marshal(payload) if err != nil { return fmt.Errorf("marshal license payload: %w", err) } if !ed25519.Verify(m.publicKey, b, sig) { return errors.New("invalid license signature") } return nil } func (m *LicenseManager) refreshStatusLocked() { status := LicenseStatus{ Enabled: m.cfg.Enabled, Fingerprint: m.fingerprint, FingerprintShort: shortFingerprint(m.fingerprint), Hostname: m.hostname, TrialDays: m.cfg.TrialDays, ActivationConfigured: len(m.publicKey) == ed25519.PublicKeySize, Tampered: m.trial.Tampered, } if !m.cfg.Enabled { status.Mode = "disabled" status.Message = "licensing disabled" status.Locked = false m.lastStatus = status return } if m.active != nil { status.Mode = "licensed" status.Locked = false status.Message = "license active" status.Customer = m.active.Customer status.LicenseID = m.active.LicenseID status.Features = append([]string(nil), m.active.Features...) status.ExpiresAt = m.active.ExpiresAt m.lastStatus = status return } start, _ := time.Parse(time.RFC3339, m.trial.FirstRunUTC) status.TrialStartedAt = m.trial.FirstRunUTC if start.IsZero() || m.trial.Tampered { status.Mode = "trial_invalid" status.Locked = true status.Message = "trial state invalid or tampered" m.lastStatus = status return } exp := start.Add(time.Duration(m.cfg.TrialDays) * 24 * time.Hour) status.TrialExpiresAt = exp.Format(time.RFC3339) days := int(time.Until(exp).Hours() / 24) if time.Until(exp) > 0 { days++ } if days < 0 { days = 0 } status.DaysRemaining = days if time.Now().UTC().Before(exp) { status.Mode = "trial" status.Locked = false status.Message = fmt.Sprintf("trial active: %d day(s) remaining", status.DaysRemaining) m.lastStatus = status return } if m.cfg.RequireAfterTrial { status.Mode = "expired" status.Locked = true status.Message = "trial expired; activation required" } else { status.Mode = "grace" status.Locked = false status.Message = "trial expired, but app allowed to continue" } m.lastStatus = status } // --------------------------------------------------------------------------- // Fingerprint helpers // --------------------------------------------------------------------------- func buildMachineFingerprint(productCode string) (string, []string, error) { parts := []string{} add := func(label, value string) { v := normalizeMachineValue(value) if v != "" { parts = append(parts, label+"="+v) } } if runtime.GOOS == "windows" { add("uuid", runWindowsCommand(`(Get-CimInstance Win32_ComputerSystemProduct).UUID`)) add("board", runWindowsCommand(`(Get-CimInstance Win32_BaseBoard).SerialNumber`)) add("machineguid", runWindowsCommand(`(Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Cryptography").MachineGuid`)) add("bios", runWindowsCommand(`(Get-CimInstance Win32_BIOS).SerialNumber`)) } else { add("machineid", readTextFile("/etc/machine-id")) add("product_uuid", readTextFile("/sys/class/dmi/id/product_uuid")) add("board_serial", readTextFile("/sys/class/dmi/id/board_serial")) } add("hostname", hostNameSafe()) add("mac", firstPhysicalMAC()) if len(parts) == 0 { return "", nil, errors.New("could not derive machine fingerprint") } raw := strings.Join(parts, "|") + "|app=" + normalizeMachineValue(productCode) + "|salt=force-monitor-fp-v1" sum := sha256.Sum256([]byte(raw)) return strings.ToUpper(hex.EncodeToString(sum[:])), parts, nil } func normalizeMachineValue(s string) string { s = strings.TrimSpace(strings.ToUpper(s)) s = strings.ReplaceAll(s, "\x00", "") s = strings.ReplaceAll(s, "\r", "") s = strings.ReplaceAll(s, "\n", "") s = strings.ReplaceAll(s, " ", "") switch s { case "", "TOBEFILLEDBYO.E.M.", "NONE", "DEFAULTSTRING", "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", "00000000-0000-0000-0000-000000000000": return "" } return s } func runWindowsCommand(script string) string { cmd := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script) out, err := cmd.Output() if err == nil { return string(out) } cmd = exec.Command("cmd.exe", "/C", "powershell", "-NoProfile", "-NonInteractive", "-Command", script) out, err = cmd.Output() if err == nil { return string(out) } return "" } func readTextFile(path string) string { b, err := os.ReadFile(path) if err != nil { return "" } return string(b) } func hostNameSafe() string { h, _ := os.Hostname() return h } func firstPhysicalMAC() string { ifs, err := net.Interfaces() if err != nil { return "" } for _, ifc := range ifs { if ifc.Flags&net.FlagLoopback != 0 { continue } if ifc.Flags&net.FlagUp == 0 { continue } if len(ifc.HardwareAddr) == 0 { continue } return ifc.HardwareAddr.String() } return "" } func shortFingerprint(full string) string { full = strings.TrimSpace(full) if len(full) <= 16 { return full } return full[:8] + "-" + full[8:16] } // --------------------------------------------------------------------------- // Trial integrity helpers // --------------------------------------------------------------------------- func (m *LicenseManager) signTrialState(state TrialState) string { sum := sha256.Sum256([]byte( strings.Join([]string{ state.FirstRunUTC, state.LastSeenUTC, state.Fingerprint, boolToString(state.Tampered), trialSalt, m.cfg.ProductCode, }, "|"), )) return hex.EncodeToString(sum[:]) } func boolToString(v bool) string { if v { return "1" } return "0" } // --------------------------------------------------------------------------- // File helpers // --------------------------------------------------------------------------- func writeJSONFileAtomic(path string, v any) error { b, err := json.MarshalIndent(v, "", " ") if err != nil { return err } return writeBytesAtomic(path, b) } func writeBytesAtomic(path string, b []byte) error { tmp := path + ".tmp" if err := os.WriteFile(tmp, b, 0o644); err != nil { return err } return os.Rename(tmp, path) } func readJSONFile[T any](path string) (T, error) { var zero T b, err := os.ReadFile(path) if err != nil { return zero, err } var out T if err := json.Unmarshal(b, &out); err != nil { return zero, err } return out, nil } // Optional helper for external tools: create canonical bytes to sign. // Keep this available so you can reuse the same code in a separate // private license generator app without changing the verification logic. func MarshalLicensePayloadForSigning(lic SignedLicense) ([]byte, error) { payload := licensePayload{ App: lic.App, LicenseID: lic.LicenseID, Customer: lic.Customer, Fingerprint: lic.Fingerprint, IssuedAt: lic.IssuedAt, ExpiresAt: lic.ExpiresAt, Features: lic.Features, } return json.Marshal(payload) } // The private signing key should live only in a separate offline signer tool. // This app intentionally does not include any signing helper. // Small utility for loading a signed license from a reader if you later want // to support multipart file upload without changing the validation flow. func ReadLicenseText(r io.Reader) (string, error) { b, err := io.ReadAll(r) if err != nil { return "", err } return strings.TrimSpace(string(b)), nil }