Files
Debian 62505d547c feat(license): Lizenz-System mit Ed25519-Verify gegen license.netcell-it.com
Portiert mail-gateway/internal/license (Verify, Cache, Trial, Signature)
+ DB-Mirror (internal/services/license) + REST-Handler (status/verify/key/clear)
+ UI-Page /license (Activate, Status, Limits, Features, Re-verify)
+ <LicenseBanner /> neben UpdateBanner (trial-expiring, expired, verify-failed)
+ Scheduler: täglich Re-verify (24h-Tick)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:41:16 +02:00

79 lines
2.1 KiB
Go

package license
import (
"errors"
"os"
"path/filepath"
"strings"
"sync/atomic"
)
// KeyStore holds the per-node license key on disk. Every node in a
// cluster runs its own verification against license.netcell-it.com
// with its own fingerprint + key, so the file lives under
// /var/lib/edgeguard/ (node-local, never replicated via the config
// fan-out).
//
// Shape on disk: a single-line text file, leading/trailing whitespace
// stripped. Mode 0600, owner nmg so a compromised read of a
// checked-out working copy never leaks the key.
type KeyStore struct {
Path string
// cached holds the last-loaded key so Get() is cheap; Reload()
// flushes it.
cached atomic.Value // string
}
const DefaultKeyStorePath = "/var/lib/edgeguard/license_key"
// NewKeyStore returns a store writing to /var/lib/edgeguard/license_key.
func NewKeyStore() *KeyStore { return &KeyStore{Path: DefaultKeyStorePath} }
// Get returns the currently persisted key, or "" when the file is
// absent / empty. Never returns an error so callers can happily fall
// through to the trial-mode path.
func (s *KeyStore) Get() string {
if v, ok := s.cached.Load().(string); ok && v != "" {
return v
}
data, err := os.ReadFile(s.Path)
if err != nil {
return ""
}
k := strings.TrimSpace(string(data))
s.cached.Store(k)
return k
}
// Save writes the key atomically (tmp + rename) with mode 0600 and
// primes the in-memory cache. Empty key removes the file — that's
// how an admin returns the node to trial mode.
func (s *KeyStore) Save(key string) error {
key = strings.TrimSpace(key)
dir := filepath.Dir(s.Path)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
if key == "" {
if err := os.Remove(s.Path); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
s.cached.Store("")
return nil
}
tmp := s.Path + ".tmp"
if err := os.WriteFile(tmp, []byte(key+"\n"), 0o600); err != nil {
return err
}
if err := os.Chmod(tmp, 0o600); err != nil {
_ = os.Remove(tmp)
return err
}
if err := os.Rename(tmp, s.Path); err != nil {
return err
}
s.cached.Store(key)
return nil
}