mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
201 lines
6.3 KiB
Go
201 lines
6.3 KiB
Go
package licensing
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// ActivationStateFileName is the name of the encrypted activation state file.
|
|
const ActivationStateFileName = "activation.enc"
|
|
|
|
// InstanceFingerprintFileName is the durable installation identity used for
|
|
// activation-key installs. It is intentionally separate from activation.enc so
|
|
// clearing a license does not make one machine consume another install slot.
|
|
const InstanceFingerprintFileName = "instance-fingerprint"
|
|
|
|
const maxInstanceFingerprintFileSize = 512
|
|
|
|
// LoadOrCreateInstanceFingerprint returns the stable local installation
|
|
// fingerprint, creating it if this config directory has not activated before.
|
|
func (p *Persistence) LoadOrCreateInstanceFingerprint() (string, error) {
|
|
fingerprintPath, err := resolvePersistencePath(p.configDir, InstanceFingerprintFileName)
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve instance fingerprint path: %w", err)
|
|
}
|
|
|
|
data, err := readBoundedPersistenceRegularFile(fingerprintPath, maxInstanceFingerprintFileSize)
|
|
if err == nil {
|
|
fingerprint := strings.TrimSpace(string(data))
|
|
if fingerprint == "" {
|
|
return "", fmt.Errorf("instance fingerprint file is empty")
|
|
}
|
|
if err := os.Chmod(fingerprintPath, persistencePrivateFilePerm); err != nil {
|
|
return "", fmt.Errorf("secure instance fingerprint file: %w", err)
|
|
}
|
|
return fingerprint, nil
|
|
}
|
|
if !isMissingPersistencePathError(err) {
|
|
return "", fmt.Errorf("read instance fingerprint file: %w", err)
|
|
}
|
|
|
|
state, err := p.LoadActivationState()
|
|
if err != nil {
|
|
return "", fmt.Errorf("load activation state for instance fingerprint: %w", err)
|
|
}
|
|
if state != nil {
|
|
fingerprint := strings.TrimSpace(state.InstanceFingerprint)
|
|
if fingerprint != "" {
|
|
if err := p.SaveInstanceFingerprint(fingerprint); err != nil {
|
|
return "", err
|
|
}
|
|
return fingerprint, nil
|
|
}
|
|
}
|
|
|
|
fingerprint, err := generateFingerprint()
|
|
if err != nil {
|
|
return "", fmt.Errorf("generate instance fingerprint: %w", err)
|
|
}
|
|
if err := p.SaveInstanceFingerprint(fingerprint); err != nil {
|
|
return "", err
|
|
}
|
|
return fingerprint, nil
|
|
}
|
|
|
|
// SaveInstanceFingerprint persists the stable local installation fingerprint.
|
|
func (p *Persistence) SaveInstanceFingerprint(fingerprint string) error {
|
|
fingerprint = strings.TrimSpace(fingerprint)
|
|
if fingerprint == "" {
|
|
return fmt.Errorf("instance fingerprint cannot be empty")
|
|
}
|
|
fingerprintPath, err := resolvePersistencePath(p.configDir, InstanceFingerprintFileName)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve instance fingerprint path: %w", err)
|
|
}
|
|
if err := writeOwnerOnlyPersistenceFileAtomic(fingerprintPath, []byte(fingerprint+"\n")); err != nil {
|
|
return fmt.Errorf("write instance fingerprint file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SaveActivationState encrypts and persists the activation state to disk.
|
|
func (p *Persistence) SaveActivationState(state *ActivationState) error {
|
|
if state == nil {
|
|
return fmt.Errorf("activation state cannot be nil")
|
|
}
|
|
|
|
// Ensure we have a persistent encryption key.
|
|
newKey, err := p.ensurePersistentKey()
|
|
if err != nil {
|
|
return fmt.Errorf("ensure persistent encryption key: %w", err)
|
|
}
|
|
if newKey != "" {
|
|
p.encryptionKey = newKey
|
|
}
|
|
|
|
jsonData, err := json.Marshal(state)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal activation state: %w", err)
|
|
}
|
|
|
|
encrypted, err := p.encrypt(jsonData)
|
|
if err != nil {
|
|
return fmt.Errorf("encrypt activation state: %w", err)
|
|
}
|
|
|
|
configDir, err := normalizePersistenceConfigDir(p.configDir)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve config directory: %w", err)
|
|
}
|
|
if err := ensurePersistenceOwnerOnlyDir(configDir); err != nil {
|
|
return fmt.Errorf("secure config directory: %w", err)
|
|
}
|
|
|
|
statePath, err := resolvePersistencePath(configDir, ActivationStateFileName)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve activation state file path: %w", err)
|
|
}
|
|
encoded := base64.StdEncoding.EncodeToString(encrypted)
|
|
|
|
if err := writeOwnerOnlyPersistenceFileAtomic(statePath, []byte(encoded)); err != nil {
|
|
return fmt.Errorf("write activation state file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadActivationState reads and decrypts the activation state from disk.
|
|
// Returns nil, nil if no activation state file exists.
|
|
func (p *Persistence) LoadActivationState() (*ActivationState, error) {
|
|
statePath, err := resolvePersistencePath(p.configDir, ActivationStateFileName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve activation state file path: %w", err)
|
|
}
|
|
|
|
encoded, err := readBoundedPersistenceRegularFile(statePath, maxLicenseFileSize)
|
|
if err != nil {
|
|
if isMissingPersistencePathError(err) {
|
|
return nil, nil // No activation state saved
|
|
}
|
|
return nil, fmt.Errorf("read activation state file: %w", err)
|
|
}
|
|
|
|
var state ActivationState
|
|
migratedPlaintext := false
|
|
|
|
if encrypted, err := base64.StdEncoding.DecodeString(string(encoded)); err == nil {
|
|
decrypted, decErr := p.decryptWithCompatibleKeys(encrypted)
|
|
if decErr != nil {
|
|
return nil, fmt.Errorf("decrypt activation state: %w", decErr)
|
|
}
|
|
if err := json.Unmarshal(decrypted, &state); err != nil {
|
|
return nil, fmt.Errorf("unmarshal activation state: %w", err)
|
|
}
|
|
} else {
|
|
// Legacy plaintext activation.enc is migration-only input.
|
|
if err := json.Unmarshal(encoded, &state); err != nil {
|
|
return nil, fmt.Errorf("decode activation state file: %w", err)
|
|
}
|
|
migratedPlaintext = true
|
|
}
|
|
|
|
if migratedPlaintext {
|
|
if err := p.SaveActivationState(&state); err != nil {
|
|
return nil, fmt.Errorf("rewrite plaintext activation state file: %w", err)
|
|
}
|
|
}
|
|
|
|
return &state, nil
|
|
}
|
|
|
|
// ClearActivationState removes the activation state file from disk.
|
|
func (p *Persistence) ClearActivationState() error {
|
|
statePath, err := resolvePersistencePath(p.configDir, ActivationStateFileName)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve activation state file path: %w", err)
|
|
}
|
|
err = os.Remove(statePath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("delete activation state file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ActivationStateExists checks if an activation state file exists on disk.
|
|
func (p *Persistence) ActivationStateExists() bool {
|
|
statePath, err := resolvePersistencePath(p.configDir, ActivationStateFileName)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
info, err := os.Lstat(statePath)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
return false
|
|
}
|
|
return info.Mode().IsRegular()
|
|
}
|