package secrets import ( "bytes" "crypto/rand" "fmt" "math/big" "os/exec" "path/filepath" "strings" "github.com/wild-cloud/wild-central/internal/storage" ) // YQ provides a wrapper around the yq command-line tool type YQ struct { yqPath string } // NewYQ creates a new YQ wrapper func NewYQ() *YQ { path, err := exec.LookPath("yq") if err != nil { path = "yq" } return &YQ{yqPath: path} } // Get retrieves a value from a YAML file using a yq expression func (y *YQ) Get(filePath, expression string) (string, error) { cmd := exec.Command(y.yqPath, expression, filePath) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return "", fmt.Errorf("yq get failed: %w, stderr: %s", err, stderr.String()) } return strings.TrimSpace(stdout.String()), nil } // Set sets a value in a YAML file using a yq expression func (y *YQ) Set(filePath, expression, value string) error { if !strings.HasPrefix(expression, ".") { expression = "." + expression } quotedValue := fmt.Sprintf(`"%s"`, strings.ReplaceAll(value, `"`, `\"`)) setExpr := fmt.Sprintf("%s = %s", expression, quotedValue) cmd := exec.Command(y.yqPath, "-i", setExpr, filePath) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return fmt.Errorf("yq set failed: %w, stderr: %s", err, stderr.String()) } return nil } // Delete removes a key from a YAML file func (y *YQ) Delete(filePath, expression string) error { delExpr := fmt.Sprintf("del(%s)", expression) cmd := exec.Command(y.yqPath, "-i", delExpr, filePath) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return fmt.Errorf("yq delete failed: %w, stderr: %s", err, stderr.String()) } return nil } // Validate checks if a YAML file is valid func (y *YQ) Validate(filePath string) error { cmd := exec.Command(y.yqPath, "eval", ".", filePath) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return fmt.Errorf("yaml validation failed: %w, stderr: %s", err, stderr.String()) } return nil } const ( // DefaultSecretLength is 32 characters DefaultSecretLength = 32 // Alphanumeric characters for secret generation alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ) // Manager handles secret generation and storage type Manager struct { yq *YQ } // NewManager creates a new secrets manager func NewManager() *Manager { return &Manager{ yq: NewYQ(), } } // GenerateSecret generates a cryptographically secure random alphanumeric string func GenerateSecret(length int) (string, error) { if length <= 0 { length = DefaultSecretLength } result := make([]byte, length) for i := range result { num, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphanumeric)))) if err != nil { return "", fmt.Errorf("generating random number: %w", err) } result[i] = alphanumeric[num.Int64()] } return string(result), nil } // EnsureSecretsFile ensures a secrets file exists with proper structure and permissions func (m *Manager) EnsureSecretsFile(dataDir, instanceName string) error { secretsPath := filepath.Join(dataDir, "instances", instanceName, "config", "secrets.yaml") // Check if secrets file already exists if storage.FileExists(secretsPath) { // Ensure proper permissions if err := storage.EnsureFilePermissions(secretsPath, 0600); err != nil { return err } return nil } // Create minimal secrets structure initialSecrets := `# Wild Cloud Instance Secrets # WARNING: This file contains sensitive data. Keep secure! cluster: talosSecrets: "" kubeconfig: "" cloudflare: token: "" ` // Ensure config directory exists if err := storage.EnsureDir(filepath.Join(dataDir, "instances", instanceName, "config"), 0755); err != nil { return err } // Write secrets file with restrictive permissions (0600) if err := storage.WriteFile(secretsPath, []byte(initialSecrets), 0600); err != nil { return err } return nil } // GetSecret retrieves a secret value from a secrets file func (m *Manager) GetSecret(secretsPath, key string) (string, error) { if !storage.FileExists(secretsPath) { return "", fmt.Errorf("secrets file not found: %s", secretsPath) } value, err := m.yq.Get(secretsPath, fmt.Sprintf(".%s", key)) if err != nil { return "", fmt.Errorf("getting secret %s: %w", key, err) } // yq returns "null" for non-existent keys if value == "" || value == "null" { return "", fmt.Errorf("secret not found: %s", key) } return value, nil } // SetSecret sets a secret value in a secrets file func (m *Manager) SetSecret(secretsPath, key, value string) error { if !storage.FileExists(secretsPath) { return fmt.Errorf("secrets file not found: %s", secretsPath) } // Acquire lock before modifying lockPath := secretsPath + ".lock" return storage.WithLock(lockPath, func() error { // Don't wrap value in quotes - yq handles YAML quoting automatically if err := m.yq.Set(secretsPath, fmt.Sprintf(".%s", key), value); err != nil { return err } // Ensure permissions remain secure after modification return storage.EnsureFilePermissions(secretsPath, 0600) }) } // EnsureSecret generates and sets a secret only if it doesn't exist (idempotent) func (m *Manager) EnsureSecret(secretsPath, key string, length int) (string, error) { if !storage.FileExists(secretsPath) { return "", fmt.Errorf("secrets file not found: %s", secretsPath) } // Check if secret already exists existingSecret, err := m.GetSecret(secretsPath, key) if err == nil && existingSecret != "" && existingSecret != "null" { // Secret already exists, return it return existingSecret, nil } // Generate new secret secret, err := GenerateSecret(length) if err != nil { return "", err } // Set the secret if err := m.SetSecret(secretsPath, key, secret); err != nil { return "", err } return secret, nil } // GenerateAndStoreSecret is a convenience function that generates a secret and stores it func (m *Manager) GenerateAndStoreSecret(secretsPath, key string) (string, error) { return m.EnsureSecret(secretsPath, key, DefaultSecretLength) } // DeleteSecret removes a secret from a secrets file func (m *Manager) DeleteSecret(secretsPath, key string) error { if !storage.FileExists(secretsPath) { return fmt.Errorf("secrets file not found: %s", secretsPath) } // Acquire lock before modifying lockPath := secretsPath + ".lock" return storage.WithLock(lockPath, func() error { if err := m.yq.Delete(secretsPath, fmt.Sprintf(".%s", key)); err != nil { return err } // Ensure permissions remain secure after modification return storage.EnsureFilePermissions(secretsPath, 0600) }) }