fix: pool backup failover on reconnect, perfect dropper scripts, bcrypt password hashing
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -53,6 +54,31 @@ func SetRotateSecretFn(fn func() (string, error)) {
|
||||
rotateSecretFn = fn
|
||||
}
|
||||
|
||||
// isBcryptHash returns true when s looks like a bcrypt hash ($2a$, $2b$, $2y$).
|
||||
func isBcryptHash(s string) bool {
|
||||
return len(s) > 4 && s[0] == '$' && s[1] == '2'
|
||||
}
|
||||
|
||||
// hashPassword returns a bcrypt hash of password (cost 12).
|
||||
func hashPassword(password string) (string, error) {
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(password), 12)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(h), nil
|
||||
}
|
||||
|
||||
// checkPassword verifies password against the stored value. Stored values are
|
||||
// always bcrypt hashes after migration; plain-text legacy values are accepted
|
||||
// once then re-hashed automatically.
|
||||
func checkPassword(stored, provided string) bool {
|
||||
if isBcryptHash(stored) {
|
||||
return bcrypt.CompareHashAndPassword([]byte(stored), []byte(provided)) == nil
|
||||
}
|
||||
// Legacy plain-text comparison (constant-time).
|
||||
return subtle.ConstantTimeCompare([]byte(provided), []byte(stored)) == 1
|
||||
}
|
||||
|
||||
func loadUsers(dataDir string) {
|
||||
usersFilePath = filepath.Join(dataDir, "users.json")
|
||||
usersMu.Lock()
|
||||
@@ -62,16 +88,36 @@ func loadUsers(dataDir string) {
|
||||
if err == nil {
|
||||
var loaded map[string]string
|
||||
if json.Unmarshal(data, &loaded) == nil && len(loaded) > 0 {
|
||||
// Migration: re-hash any plain-text entries left from an older version.
|
||||
migrated := false
|
||||
for u, v := range loaded {
|
||||
if !isBcryptHash(v) {
|
||||
if h, herr := hashPassword(v); herr == nil {
|
||||
loaded[u] = h
|
||||
migrated = true
|
||||
log.Printf("[Auth] Migrated plain-text password for user %q to bcrypt", u)
|
||||
}
|
||||
}
|
||||
}
|
||||
authUsers = loaded
|
||||
if migrated {
|
||||
d, _ := json.MarshalIndent(authUsers, "", " ")
|
||||
_ = os.WriteFile(usersFilePath, d, 0600)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// First run — no users.json (or empty). Generate a random admin password,
|
||||
// save it, and print it clearly so the operator can log in immediately.
|
||||
// hash it, save it, and print the plain-text once to the console.
|
||||
pw := generateRandomPassword()
|
||||
authUsers = map[string]string{"admin": pw}
|
||||
if err := os.MkdirAll(dataDir, 0755); err == nil {
|
||||
hashed, herr := hashPassword(pw)
|
||||
if herr != nil {
|
||||
hashed = pw // extremely unlikely; degrade gracefully
|
||||
log.Printf("[Auth] WARNING: bcrypt failed, storing plain-text password: %v", herr)
|
||||
}
|
||||
authUsers = map[string]string{"admin": hashed}
|
||||
if mkErr := os.MkdirAll(dataDir, 0755); mkErr == nil {
|
||||
d, _ := json.MarshalIndent(authUsers, "", " ")
|
||||
if writeErr := os.WriteFile(usersFilePath, d, 0600); writeErr != nil {
|
||||
log.Printf("[Auth] WARNING: could not save users.json: %v", writeErr)
|
||||
@@ -97,16 +143,19 @@ func loadUsers(dataDir string) {
|
||||
func generateRandomPassword() string {
|
||||
b := make([]byte, 10)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
// Fallback to a fixed marker so the operator knows something went wrong.
|
||||
return "CHANGE-ME-NOW-12345"
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func saveUser(username, password string) error {
|
||||
hashed, err := hashPassword(password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bcrypt: %w", err)
|
||||
}
|
||||
usersMu.Lock()
|
||||
defer usersMu.Unlock()
|
||||
authUsers[username] = password
|
||||
authUsers[username] = hashed
|
||||
if usersFilePath == "" {
|
||||
usersFilePath = filepath.Join("data", "users.json")
|
||||
}
|
||||
@@ -161,10 +210,10 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
usersMu.RLock()
|
||||
expectedPass, exists := authUsers[user]
|
||||
storedHash, exists := authUsers[user]
|
||||
usersMu.RUnlock()
|
||||
|
||||
if !exists || subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass)) != 1 {
|
||||
if !exists || !checkPassword(storedHash, pass) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user