package main import ( "crypto/pbkdf2" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" "sync" "time" ) // pbkdf2Iters: avvägning mellan OWASP-rekommendation och att hubben ska // köra på en Pi5 — inloggning sker sällan så kostnaden syns inte. const pbkdf2Iters = 210_000 // HubUser är ett lokalt konto på hubben (auth: "local"). type HubUser struct { Username string `json:"username"` Salt string `json:"salt"` Hash string `json:"hash"` // pbkdf2-sha256 } type HubConfig struct { Listen string `json:"listen"` Auth string `json:"auth"` // "local" — byts mot "ldap" e.d. längre fram Users []HubUser `json:"users"` AgentKey string `json:"agent_key"` // delad nyckel som agenter registrerar sig med SessionTTLHours int `json:"session_ttl_hours"` path string } func defaultHubConfig() *HubConfig { return &HubConfig{ Listen: "127.0.0.1:8790", Auth: "local", SessionTTLHours: 24 * 7, } } func hubConfigPath() string { if p := os.Getenv("HELMD_HUB_CONFIG"); p != "" { return p } dir, err := os.UserConfigDir() if err != nil { dir = "." } return filepath.Join(dir, "helmd", "hub.json") } func loadHubConfig(path string) (*HubConfig, error) { cfg := defaultHubConfig() cfg.path = path data, err := os.ReadFile(path) if os.IsNotExist(err) { cfg.AgentKey = newToken() if err := cfg.Save(); err != nil { return nil, err } fmt.Fprintf(os.Stderr, "helmd: skapade %s (ny agent-nyckel genererad)\n", path) return cfg, nil } if err != nil { return nil, err } if err := json.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("%s: %w", path, err) } if cfg.SessionTTLHours <= 0 { cfg.SessionTTLHours = 24 * 7 } if cfg.AgentKey == "" { cfg.AgentKey = newToken() if err := cfg.Save(); err != nil { return nil, err } } return cfg, nil } func (c *HubConfig) Save() error { if err := os.MkdirAll(filepath.Dir(c.path), 0o755); err != nil { return err } data, err := json.MarshalIndent(c, "", " ") if err != nil { return err } return os.WriteFile(c.path, append(data, '\n'), 0o600) } func hashPassword(pass string, salt []byte) (string, error) { key, err := pbkdf2.Key(sha256.New, pass, salt, pbkdf2Iters, 32) if err != nil { return "", err } return hex.EncodeToString(key), nil } // SetPassword skapar eller uppdaterar ett lokalt konto. func (c *HubConfig) SetPassword(user, pass string) error { if user == "" || pass == "" { return fmt.Errorf("användarnamn och lösenord får inte vara tomma") } salt := make([]byte, 16) if _, err := rand.Read(salt); err != nil { return err } hash, err := hashPassword(pass, salt) if err != nil { return err } for i := range c.Users { if c.Users[i].Username == user { c.Users[i].Salt = hex.EncodeToString(salt) c.Users[i].Hash = hash return nil } } c.Users = append(c.Users, HubUser{Username: user, Salt: hex.EncodeToString(salt), Hash: hash}) return nil } // Verify kontrollerar användarnamn + lösenord. Kör alltid en // hash-beräkning så att svarstiden inte avslöjar om kontot finns. func (c *HubConfig) Verify(user, pass string) bool { var found *HubUser for i := range c.Users { if c.Users[i].Username == user { found = &c.Users[i] } } salt := make([]byte, 16) want := "ingen-träff" if found != nil { if s, err := hex.DecodeString(found.Salt); err == nil { salt = s } want = found.Hash } got, err := hashPassword(pass, salt) if err != nil { return false } return found != nil && subtle.ConstantTimeCompare([]byte(got), []byte(want)) == 1 } // Authenticator kontrollerar styrklienters inloggning. Lokala konton är // den första implementationen; LDAP eller annan backend pluggas in som // en ny implementation och väljs med "auth" i hub-konfigen. type Authenticator interface { Authenticate(username, password string) bool } type localAuth struct{ cfg *HubConfig } func (l *localAuth) Authenticate(user, pass string) bool { return l.cfg.Verify(user, pass) } // sessionStore håller inloggade klienters tokens i minnet — en omstart // av hubben loggar ut alla, vilket är acceptabelt. type sessionStore struct { ttl time.Duration mu sync.Mutex toks map[string]clientSession } type clientSession struct { User string Expires time.Time } func newSessionStore(ttl time.Duration) *sessionStore { return &sessionStore{ttl: ttl, toks: map[string]clientSession{}} } func (s *sessionStore) Create(user string) string { tok := newToken() s.mu.Lock() defer s.mu.Unlock() for t, c := range s.toks { if time.Now().After(c.Expires) { delete(s.toks, t) } } s.toks[tok] = clientSession{User: user, Expires: time.Now().Add(s.ttl)} return tok } func (s *sessionStore) Lookup(tok string) (string, bool) { if tok == "" { return "", false } s.mu.Lock() defer s.mu.Unlock() c, ok := s.toks[tok] if !ok || time.Now().After(c.Expires) { delete(s.toks, tok) return "", false } return c.User, true } func (s *sessionStore) Delete(tok string) { s.mu.Lock() defer s.mu.Unlock() delete(s.toks, tok) }