package main import ( "encoding/json" "fmt" "net/http" "os" "sync" "golang.org/x/crypto/bcrypt" ) var pwMu sync.RWMutex // minPasswordLen: the password is the only thing between the internet and a shell on the machines. const minPasswordLen = 12 // passwordOK: bcrypt only uses the first 72 bytes, so longer is refused instead of silently cut. func passwordOK(p string) bool { return len([]rune(p)) >= minPasswordLen && len(p) <= 72 } // authInfo tells the login page whether to ask for a 2FA code. func (s *Server) authInfo(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"totp": s.cfg.TOTPSecret != ""}) } func (s *Server) pwHash() string { pwMu.RLock() defer pwMu.RUnlock() return s.cfg.PasswordHash } // changePassword lets a logged-in user set a new password (current one required). func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || !s.authed(r) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } ip := clientIP(r) if s.locked(ip) { http.Error(w, "too many attempts", http.StatusTooManyRequests) return } var req struct{ Current, New string } if json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&req) != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if bcrypt.CompareHashAndPassword([]byte(s.pwHash()), []byte(req.Current)) != nil { s.fail(ip) http.Error(w, "wrong current password", http.StatusForbidden) return } if !passwordOK(req.New) { http.Error(w, fmt.Sprintf("password must be %d-72 characters", minPasswordLen), http.StatusBadRequest) return } h, err := bcrypt.GenerateFromPassword([]byte(req.New), bcrypt.DefaultCost) if err != nil { http.Error(w, "error", http.StatusInternalServerError) return } pwMu.Lock() defer pwMu.Unlock() // re-read the file so agent tokens added by the CLI are preserved var c Config b, err := os.ReadFile(s.cfgPath) if err != nil || json.Unmarshal(b, &c) != nil { http.Error(w, "config error", http.StatusInternalServerError) return } c.PasswordHash = string(h) out, _ := json.MarshalIndent(c, "", " ") tmp := s.cfgPath + ".tmp" if err := os.WriteFile(tmp, out, 0600); err != nil || os.Rename(tmp, s.cfgPath) != nil { http.Error(w, "write failed", http.StatusInternalServerError) return } s.cfg.PasswordHash = string(h) w.Write([]byte("ok")) }