mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-06 16:16:26 +00:00
136 lines
4 KiB
Go
136 lines
4 KiB
Go
package licensing
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
// ActivationStateFileName is the name of the encrypted activation state file.
|
|
const ActivationStateFileName = "activation.enc"
|
|
|
|
// 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 {
|
|
// Try to decrypt with current encryption key.
|
|
decrypted, decErr := p.decrypt(encrypted)
|
|
|
|
// Fall back to machine-id if the current key doesn't work.
|
|
if decErr != nil && p.machineID != p.encryptionKey {
|
|
decrypted, decErr = p.decryptWithKey(encrypted, p.deriveKeyFrom(p.machineID))
|
|
}
|
|
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()
|
|
}
|