mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 04:38:48 +00:00
Harden CodeQL-scanned request, command, path, and frontend sinks across relay proxying, availability probes, connection probing, notification CLI execution, report storage, licensing persistence, preview bootstrapping, tooltip rendering, logging, and test identity generation.
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 := p.SaveInstanceFingerprint(fingerprint); 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()
|
|
}
|