/* Volt Platform — License Persistence Store and retrieve license data and cryptographic keys */ package license import ( "crypto/ecdh" "crypto/rand" "encoding/hex" "fmt" "os" "path/filepath" "time" "gopkg.in/yaml.v3" ) const ( LicenseDir = "/etc/volt/license" LicenseFile = "/etc/volt/license/license.yaml" NodeKeyFile = "/etc/volt/license/node.key" NodePubFile = "/etc/volt/license/node.pub" ) // Store handles license persistence type Store struct { Dir string } // NewStore creates a license store with the default directory func NewStore() *Store { return &Store{Dir: LicenseDir} } // licensePath returns the full path for the license file func (s *Store) licensePath() string { return filepath.Join(s.Dir, "license.yaml") } // keyPath returns the full path for the node private key func (s *Store) keyPath() string { return filepath.Join(s.Dir, "node.key") } // pubPath returns the full path for the node public key func (s *Store) pubPath() string { return filepath.Join(s.Dir, "node.pub") } // Load reads the license from disk func (s *Store) Load() (*License, error) { data, err := os.ReadFile(s.licensePath()) if err != nil { if os.IsNotExist(err) { return nil, fmt.Errorf("no license found (not registered)") } return nil, fmt.Errorf("failed to read license: %w", err) } var lic License if err := yaml.Unmarshal(data, &lic); err != nil { return nil, fmt.Errorf("failed to parse license: %w", err) } return &lic, nil } // Save writes the license to disk func (s *Store) Save(lic *License) error { if err := os.MkdirAll(s.Dir, 0700); err != nil { return fmt.Errorf("failed to create license directory: %w", err) } data, err := yaml.Marshal(lic) if err != nil { return fmt.Errorf("failed to marshal license: %w", err) } if err := os.WriteFile(s.licensePath(), data, 0600); err != nil { return fmt.Errorf("failed to write license: %w", err) } return nil } // IsRegistered checks if a valid license exists on disk func (s *Store) IsRegistered() bool { _, err := s.Load() return err == nil } // IsExpired checks if the current license has expired func (s *Store) IsExpired() (bool, error) { lic, err := s.Load() if err != nil { return false, err } if lic.ExpiresAt.IsZero() { return false, nil // no expiry = never expires } return time.Now().After(lic.ExpiresAt), nil } // HasFeature checks if the current license tier includes a feature func (s *Store) HasFeature(feature string) (bool, error) { lic, err := s.Load() if err != nil { return false, err } return TierIncludes(lic.Tier, feature), nil } // GenerateKeypair generates an X25519 keypair and stores it on disk func (s *Store) GenerateKeypair() (pubHex string, err error) { if err := os.MkdirAll(s.Dir, 0700); err != nil { return "", fmt.Errorf("failed to create license directory: %w", err) } // Generate X25519 keypair using crypto/ecdh curve := ecdh.X25519() privKey, err := curve.GenerateKey(rand.Reader) if err != nil { return "", fmt.Errorf("failed to generate keypair: %w", err) } // Encode to hex privHex := hex.EncodeToString(privKey.Bytes()) pubHex = hex.EncodeToString(privKey.PublicKey().Bytes()) // Store private key (restrictive permissions) if err := os.WriteFile(s.keyPath(), []byte(privHex+"\n"), 0600); err != nil { return "", fmt.Errorf("failed to write private key: %w", err) } // Store public key if err := os.WriteFile(s.pubPath(), []byte(pubHex+"\n"), 0644); err != nil { return "", fmt.Errorf("failed to write public key: %w", err) } return pubHex, nil } // ReadPublicKey reads the stored node public key func (s *Store) ReadPublicKey() (string, error) { data, err := os.ReadFile(s.pubPath()) if err != nil { return "", fmt.Errorf("failed to read public key: %w", err) } return string(data), nil } // Remove deletes the license and keypair from disk func (s *Store) Remove() error { files := []string{s.licensePath(), s.keyPath(), s.pubPath()} for _, f := range files { if err := os.Remove(f); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove %s: %w", f, err) } } return nil }