Pulse/pkg/licensing/activation_store.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()
}