From: PhiTux Date: Thu, 17 Jul 2025 15:36:11 +0000 (+0200) Subject: added changing password X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=71dda2334817badc487cf83f50c17120a5294041;p=DailyTxT.git added changing password --- diff --git a/backend/handlers/users.go b/backend/handlers/users.go index 6cf551c..8f2f75d 100644 --- a/backend/handlers/users.go +++ b/backend/handlers/users.go @@ -1,6 +1,7 @@ package handlers import ( + "crypto/rand" "encoding/base64" "encoding/json" "fmt" @@ -191,7 +192,7 @@ func Login(w http.ResponseWriter, r *http.Request) { } // Verify password - if !utils.VerifyPassword(req.Password, hashedPassword, salt) { + if !utils.VerifyPassword(req.Password, hashedPassword) { utils.Logger.Printf("Login failed. Password for user '%s' is incorrect", req.Username) http.Error(w, "User/Password combination not found", http.StatusNotFound) return @@ -291,20 +292,28 @@ func Register(username string, password string) (bool, error) { } // Create new user data - hashedPassword, salt, err := utils.HashPassword(password) + hashedPassword, err := utils.HashPassword(password) if err != nil { return false, fmt.Errorf("internal Server Error: %d", http.StatusInternalServerError) } + // Generate a random salt + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + return false, fmt.Errorf("internal Server Error: %d", http.StatusInternalServerError) + } + // Convert salt to base64 + saltBase64 := base64.StdEncoding.EncodeToString(salt) + // Create encryption key - derivedKey, err := utils.DeriveKeyFromPassword(password, salt) + derivedKey, err := utils.DeriveKeyFromPassword(password, saltBase64) if err != nil { return false, fmt.Errorf("internal Server Error: %d", http.StatusInternalServerError) } // Generate a new random encryption key encryptionKey := make([]byte, 32) - if _, err := utils.RandRead(encryptionKey); err != nil { + if _, err := rand.Read(encryptionKey); err != nil { return false, fmt.Errorf("internal Server Error: %d", http.StatusInternalServerError) } @@ -315,7 +324,7 @@ func Register(username string, password string) (bool, error) { } nonce := make([]byte, aead.NonceSize()) - if _, err := utils.RandRead(nonce); err != nil { + if _, err := rand.Read(nonce); err != nil { return false, fmt.Errorf("internal Server Error: %d", http.StatusInternalServerError) } @@ -631,3 +640,219 @@ func GetMigrationProgress(w http.ResponseWriter, r *http.Request) { "progress": progress, }) } + +// ChangePasswordRequest represents the change password request body +type ChangePasswordRequest struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` +} + +// ChangePassword changes the user's password +func ChangePassword(w http.ResponseWriter, r *http.Request) { + // Get user ID from context + userID, ok := r.Context().Value(utils.UserIDKey).(int) + if !ok { + utils.JSONResponse(w, http.StatusOK, map[string]any{ + "success": false, + "message": "User not authenticated", + }) + return + } + + // Get derived key from context + derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string) + if !ok { + utils.JSONResponse(w, http.StatusOK, map[string]any{ + "success": false, + "message": "User not authenticated", + }) + return + } + + // Parse request body + var req ChangePasswordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + utils.JSONResponse(w, http.StatusBadRequest, map[string]any{ + "success": false, + "message": "Invalid request body", + }) + return + } + + // Get user data + users, err := utils.GetUsers() + if err != nil { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": fmt.Sprintf("Error retrieving users: %v", err), + }) + return + } + + usersList, ok := users["users"].([]any) + if !ok { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": "Users data is not in the correct format", + }) + return + } + + var user map[string]any + for _, u := range usersList { + uMap, ok := u.(map[string]any) + if !ok { + continue + } + if id, ok := uMap["user_id"].(float64); ok && int(id) == userID { + user = uMap + break + } + } + + if user == nil { + utils.JSONResponse(w, http.StatusNotFound, map[string]any{ + "success": false, + "message": "User not found", + }) + return + } + + currentPassword, ok := user["password"].(string) + if !ok { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": "Current hashed password not found for user", + }) + return + } + + if !utils.VerifyPassword(req.OldPassword, currentPassword) { + utils.JSONResponse(w, http.StatusOK, map[string]any{ + "success": false, + "message": "Old password is incorrect", + "password_incorrect": true, + }) + return + } + + newHashedPassword, err := utils.HashPassword(req.NewPassword) + if err != nil { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": fmt.Sprintf("Error hashing new password: %v", err), + }) + return + } + + // Update user data + user["password"] = newHashedPassword + + // Decrypt the existing encryption key + // Get encryption key + encKey, err := utils.GetEncryptionKey(userID, derivedKey) + if err != nil { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": fmt.Sprintf("Error getting encryption key: %v", err), + }) + return + } + encKeyBytes, err := base64.URLEncoding.DecodeString(encKey) + if err != nil { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": fmt.Sprintf("Error decoding encryption key: %v", err), + }) + return + } + + // Re-Encrypt the encryption key with the new password + // Generate a random salt + salt := make([]byte, 16) + _, err = rand.Read(salt) + if err != nil { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": fmt.Sprintf("Error generating salt: %v", err), + }) + return + } + + saltBase64 := base64.StdEncoding.EncodeToString(salt) + newDerivedKey, err := utils.DeriveKeyFromPassword(req.NewPassword, saltBase64) + if err != nil { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": fmt.Sprintf("Error deriving new key from password: %v", err), + }) + return + } + + // Encrypt the encryption key with the new derived key + aead, err := utils.CreateAEAD(newDerivedKey) + if err != nil { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": fmt.Sprintf("Error creating AEAD: %v", err), + }) + return + } + nonce := make([]byte, aead.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": fmt.Sprintf("Error generating nonce: %v", err), + }) + return + } + encryptedKey := aead.Seal(nonce, nonce, encKeyBytes, nil) + encEncKey := base64.StdEncoding.EncodeToString(encryptedKey) + + // Update user data with new salt and encrypted key + user["salt"] = saltBase64 + user["enc_enc_key"] = encEncKey + + // Update users data + for i, u := range usersList { + if uMap, ok := u.(map[string]any); ok && uMap["user_id"] == userID { + usersList[i] = user + break + } + } + users["users"] = usersList + // Write updated users data to file + if err := utils.WriteUsers(users); err != nil { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": fmt.Sprintf("Error writing users data: %v", err), + }) + return + } + + // create new JWT token with updated derived key + token, err := utils.GenerateToken(userID, user["username"].(string), base64.StdEncoding.EncodeToString(newDerivedKey)) + if err != nil { + utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "message": fmt.Sprintf("Error generating new token: %v", err), + }) + return + } + + // Set new token cookie + http.SetCookie(w, &http.Cookie{ + Name: "token", + Value: token, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + Path: "/", + Expires: time.Now().Add(time.Duration(utils.Settings.LogoutAfterDays) * 24 * time.Hour), + }) + + // Return success and return a new cookie + utils.JSONResponse(w, http.StatusOK, map[string]any{ + "success": true, + "message": "Password changed successfully", + }) +} diff --git a/backend/main.go b/backend/main.go index 238032e..16c5ab0 100644 --- a/backend/main.go +++ b/backend/main.go @@ -39,6 +39,7 @@ func main() { mux.HandleFunc("GET /users/check", middleware.RequireAuth(handlers.CheckLogin)) mux.HandleFunc("GET /users/getUserSettings", middleware.RequireAuth(handlers.GetUserSettings)) mux.HandleFunc("POST /users/saveUserSettings", middleware.RequireAuth(handlers.SaveUserSettings)) + mux.HandleFunc("POST /users/changePassword", middleware.RequireAuth(handlers.ChangePassword)) mux.HandleFunc("POST /logs/saveLog", middleware.RequireAuth(handlers.SaveLog)) mux.HandleFunc("GET /logs/getLog", middleware.RequireAuth(handlers.GetLog)) diff --git a/backend/utils/file_handling.go b/backend/utils/file_handling.go index 098063a..07a0e1f 100644 --- a/backend/utils/file_handling.go +++ b/backend/utils/file_handling.go @@ -1,7 +1,6 @@ package utils import ( - "crypto/rand" "encoding/json" "fmt" "io" @@ -208,11 +207,6 @@ func WriteTags(userID int, content map[string]any) error { return nil } -// RandRead is a helper function for reading random bytes -func RandRead(b []byte) (int, error) { - return rand.Read(b) -} - // GetUserSettings retrieves the settings for a specific user func GetUserSettings(userID int) (string, error) { userSettingsMutex.RLock() diff --git a/backend/utils/migration.go b/backend/utils/migration.go index 685ff02..da69b73 100644 --- a/backend/utils/migration.go +++ b/backend/utils/migration.go @@ -313,7 +313,7 @@ func MigrateUserData(username, password string, registerFunc RegisterUserFunc, p newUserID = int(id) // Verify password - if !VerifyPassword(password, u["password"].(string), u["salt"].(string)) { + if !VerifyPassword(password, u["password"].(string)) { return handleError(fmt.Sprintf("Login failed. Password for user '%s' is incorrect", username), nil) } diff --git a/backend/utils/security.go b/backend/utils/security.go index 8f72092..4c49769 100644 --- a/backend/utils/security.go +++ b/backend/utils/security.go @@ -3,9 +3,12 @@ package utils import ( "crypto/cipher" "crypto/rand" + "crypto/subtle" "encoding/base64" "fmt" "io" + "runtime" + "strings" "time" "github.com/golang-jwt/jwt/v5" @@ -73,42 +76,107 @@ func ValidateToken(tokenString string) (*Claims, error) { return claims, nil } +type Argon2Configuration struct { + HashRaw []byte + Salt []byte + TimeCost uint32 + MemoryCost uint32 + Threads uint8 + KeyLength uint32 +} + // HashPassword hashes a password using Argon2 -func HashPassword(password string) (string, string, error) { +func HashPassword(password string) (string, error) { + config := &Argon2Configuration{ + TimeCost: 5, + MemoryCost: 64 * 1024, + Threads: uint8(runtime.NumCPU()), + KeyLength: 32, + } + // Generate a random salt salt := make([]byte, 16) - if _, err := io.ReadFull(rand.Reader, salt); err != nil { - return "", "", err + _, err := rand.Read(salt) + if err != nil { + return "", err } + config.Salt = salt // Hash password - hash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 4, 32) + config.HashRaw = argon2.IDKey([]byte(password), salt, config.TimeCost, config.MemoryCost, config.Threads, config.KeyLength) + + // Generate standardized hash format + encodedHash := fmt.Sprintf( + "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, + config.MemoryCost, + config.TimeCost, + config.Threads, + base64.RawStdEncoding.EncodeToString(config.Salt), + base64.RawStdEncoding.EncodeToString(config.HashRaw), + ) + + return encodedHash, nil +} - // Encode salt and hash to base64 - saltBase64 := base64.StdEncoding.EncodeToString(salt) - hashBase64 := base64.StdEncoding.EncodeToString(hash) +func parseArgon2Hash(encodedHash string) (*Argon2Configuration, error) { + components := strings.Split(encodedHash, "$") + if len(components) != 6 { + return nil, fmt.Errorf("invalid hash format structure") + } - return hashBase64, saltBase64, nil -} + // Validate algorithm identifier + if !strings.HasPrefix(components[1], "argon2id") { + return nil, fmt.Errorf("unsupported algorithm variant") + } -// VerifyPassword verifies if a password matches a hash -func VerifyPassword(password, hashBase64, saltBase64 string) bool { - // Decode salt and hash - salt, err := base64.StdEncoding.DecodeString(saltBase64) + // Extract version information + var version int + fmt.Sscanf(components[2], "v=%d", &version) + + // Parse configuration parameters + config := &Argon2Configuration{} + fmt.Sscanf(components[3], "m=%d,t=%d,p=%d", + &config.MemoryCost, &config.TimeCost, &config.Threads) + + // Decode salt component + salt, err := base64.RawStdEncoding.DecodeString(components[4]) if err != nil { - return false + return nil, fmt.Errorf("salt decoding failed: %w", err) } + config.Salt = salt - _, err = base64.StdEncoding.DecodeString(hashBase64) + // Decode hash component + hash, err := base64.RawStdEncoding.DecodeString(components[5]) if err != nil { - return false + return nil, fmt.Errorf("hash decoding failed: %w", err) } + config.HashRaw = hash + config.KeyLength = uint32(len(hash)) - // Hash the provided password with the same salt - hash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 4, 32) + return config, nil +} + +// VerifyPassword verifies if a password matches a hash +func VerifyPassword(password, hashBase64 string) bool { + // Parse stored hash parameters + config, err := parseArgon2Hash(hashBase64) + if err != nil { + return false + } - // Compare hashes - return base64.StdEncoding.EncodeToString(hash) == hashBase64 + // Generate hash using identical parameters + computedHash := argon2.IDKey( + []byte(password), + config.Salt, + config.TimeCost, + config.MemoryCost, + config.Threads, + config.KeyLength, + ) + + // Perform constant-time comparison to prevent timing attacks + return subtle.ConstantTimeCompare(config.HashRaw, computedHash) == 1 } // DeriveKeyFromPassword derives a key from a password and salt diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index beb7d3e..4df7f67 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -235,7 +235,7 @@ settingsModal.hide(); } else { - throw new Error('Error saving settings'); + console.error('Error saving settings'); } }) .catch((error) => { @@ -503,6 +503,55 @@ localStorage.setItem('autoLoadImagesThisDevice', $autoLoadImagesThisDevice); }); + + let currentPassword = $state(''); + let newPassword = $state(''); + let confirmNewPassword = $state(''); + let changePasswordNotEqual = $state(false); + let isChangingPassword = $state(false); + let changingPasswordSuccess = $state(false); + let changingPasswordError = $state(false); + let changingPasswordIncorrect = $state(false); + + function changePassword() { + changePasswordNotEqual = false; + changingPasswordSuccess = false; + changingPasswordError = false; + changingPasswordIncorrect = false; + + if (newPassword !== confirmNewPassword) { + changePasswordNotEqual = true; + return; + } + + if (isChangingPassword) return; + isChangingPassword = true; + + axios + .post(API_URL + '/users/changePassword', { + old_password: currentPassword, + new_password: newPassword + }) + .then((response) => { + if (response.data.success) { + changingPasswordSuccess = true; + } else { + changingPasswordError = true; + console.error('Error changing password'); + if (response.data.password_incorrect) { + changingPasswordIncorrect = true; + } + } + }) + .catch((error) => { + console.error(error); + console.log('Error on Changing password:', error.response.data.message); + changingPasswordError = true; + }) + .finally(() => { + isChangingPassword = false; + }); + }
@@ -1012,7 +1061,71 @@

🔒 Sicherheit

-
Password ändern
+
+
Password ändern
+
+
+ + +
+
+ + +
+
+ + +
+ +
+ {#if changePasswordNotEqual} + + {/if} + {#if changingPasswordSuccess} + + {/if} + {#if changingPasswordIncorrect} + + {:else if changingPasswordError} + + {/if} +
Backup-Keys
Username ändern
Konto löschen
@@ -1048,127 +1161,127 @@
+ + -
- +
+ - + - + - + - + - + - + - + - + - +