backup codes now working
authorPhiTux <redacted>
Tue, 22 Jul 2025 15:31:44 +0000 (17:31 +0200)
committerPhiTux <redacted>
Tue, 22 Jul 2025 15:31:44 +0000 (17:31 +0200)
backend/handlers/users.go
backend/utils/security.go
frontend/src/routes/(authed)/+layout.svelte
frontend/src/routes/+layout.svelte

index 645cad7596bc586bdc9fcae822b1391b3c5c436e..648d520693cc4834b5c57ce75048ffb9efd9c013 100644 (file)
@@ -45,8 +45,6 @@ func Login(w http.ResponseWriter, r *http.Request) {
 
        // Find user
        var userID int
-       var hashedPassword string
-       var salt string
        found := false
        var username string
 
@@ -61,12 +59,6 @@ func Login(w http.ResponseWriter, r *http.Request) {
                        if id, ok := user["user_id"].(float64); ok {
                                userID = int(id)
                        }
-                       if pwd, ok := user["password"].(string); ok {
-                               hashedPassword = pwd
-                       }
-                       if s, ok := user["salt"].(string); ok {
-                               salt = s
-                       }
                        break
                }
        }
@@ -191,23 +183,19 @@ func Login(w http.ResponseWriter, r *http.Request) {
                return
        }
 
-       // Verify password
-       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
-       }
-
-       // Get intermediate key
-       derivedKey, err := utils.DeriveKeyFromPassword(req.Password, salt)
+       derivedKey, availableBackupCodes, err := utils.CheckPasswordForUser(userID, req.Password)
        if err != nil {
+               utils.Logger.Printf("Error checking password for user '%s': %v", req.Username, err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
                return
+       } else if derivedKey == "" {
+               utils.Logger.Printf("Login failed. Password for user '%s' is incorrect", req.Username)
+               http.Error(w, "User/Password combination not found", http.StatusNotFound)
+               return
        }
-       derivedKeyBase64 := base64.StdEncoding.EncodeToString(derivedKey)
 
        // Create JWT token
-       token, err := utils.GenerateToken(userID, req.Username, derivedKeyBase64)
+       token, err := utils.GenerateToken(userID, username, derivedKey)
        if err != nil {
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
                return
@@ -225,8 +213,9 @@ func Login(w http.ResponseWriter, r *http.Request) {
 
        // Return success
        utils.JSONResponse(w, http.StatusOK, map[string]any{
-               "migration_started": false,
-               "username":          username,
+               "migration_started":      false,
+               "username":               username,
+               "available_backup_codes": availableBackupCodes,
        })
 }
 
@@ -660,7 +649,7 @@ func ChangePassword(w http.ResponseWriter, r *http.Request) {
        }
 
        // Get derived key from context
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       _, ok = r.Context().Value(utils.DerivedKeyKey).(string)
        if !ok {
                utils.JSONResponse(w, http.StatusUnauthorized, map[string]any{
                        "success": false,
@@ -679,6 +668,23 @@ func ChangePassword(w http.ResponseWriter, r *http.Request) {
                return
        }
 
+       derivedKey, availableBackupCodes, err := utils.CheckPasswordForUser(userID, req.OldPassword)
+       if err != nil {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": fmt.Sprintf("Error checking old password: %v", err),
+               })
+               return
+       } else if len(derivedKey) == 0 {
+               utils.JSONResponse(w, http.StatusOK, map[string]any{
+                       "success":                false,
+                       "message":                "Old password is incorrect",
+                       "password_incorrect":     true,
+                       "available_backup_codes": availableBackupCodes,
+               })
+               return
+       }
+
        // Get user data
        users, err := utils.GetUsers()
        if err != nil {
@@ -718,24 +724,6 @@ func ChangePassword(w http.ResponseWriter, r *http.Request) {
                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{
@@ -813,6 +801,9 @@ func ChangePassword(w http.ResponseWriter, r *http.Request) {
        user["salt"] = saltBase64
        user["enc_enc_key"] = encEncKey
 
+       // Remove backup codes if they exist
+       user["backup_codes"] = []any{}
+
        // Update users data
        for i, u := range usersList {
                if uMap, ok := u.(map[string]any); ok && uMap["user_id"] == userID {
@@ -882,58 +873,30 @@ func DeleteAccount(w http.ResponseWriter, r *http.Request) {
                return
        }
 
-       // Check if password is correct
-       users, err := utils.GetUsers()
-       if err != nil {
-               utils.JSONResponse(w, http.StatusOK, map[string]any{
-                       "success": false,
-                       "message": fmt.Sprintf("Error retrieving users: %v", err),
-               })
-               return
-       }
-       usersList, ok := users["users"].([]any)
-       if !ok {
+       derived_key, _, err := utils.CheckPasswordForUser(userID, req.Password)
+       if err != nil || len(derived_key) == 0 {
                utils.JSONResponse(w, http.StatusOK, map[string]any{
-                       "success": false,
-                       "message": "Users data is not in the correct format",
+                       "success":            false,
+                       "message":            "Error checking password",
+                       "password_incorrect": true,
                })
                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 {
+       // Get User data
+       users, err := utils.GetUsers()
+       if err != nil {
                utils.JSONResponse(w, http.StatusOK, map[string]any{
                        "success": false,
-                       "message": "User not found",
+                       "message": fmt.Sprintf("Error retrieving users: %v", err),
                })
                return
        }
-
-       password, ok := user["password"].(string)
+       usersList, ok := users["users"].([]any)
        if !ok {
                utils.JSONResponse(w, http.StatusOK, map[string]any{
                        "success": false,
-                       "message": "Current hashed password not found for user",
-               })
-               return
-       }
-
-       if !utils.VerifyPassword(req.Password, password) {
-               utils.JSONResponse(w, http.StatusOK, map[string]any{
-                       "success":            false,
-                       "message":            "Password is incorrect",
-                       "password_incorrect": true,
+                       "message": "Users data is not in the correct format",
                })
                return
        }
@@ -1004,34 +967,18 @@ func CreateBackupCodes(w http.ResponseWriter, r *http.Request) {
        }
 
        // Check if password is correct
-       correct, backupCodes, err := utils.CheckPasswordForUser(userID, req.Password)
-       if err != nil {
+       derivedKey, backup_codes, err := utils.CheckPasswordForUser(userID, req.Password)
+       if err != nil || len(derivedKey) == 0 {
                utils.Logger.Printf("Error checking password for user %d: %v", userID, err)
 
                utils.JSONResponse(w, http.StatusOK, map[string]any{
                        "success": false,
-                       "message": err,
-               })
-               return
-       } else if !correct {
-               utils.JSONResponse(w, http.StatusOK, map[string]any{
-                       "success":            false,
-                       "message":            "Password is incorrect",
-                       "password_incorrect": true,
+                       "message": "Error checking password",
                })
                return
        }
-       // otherwise, we have the correct password
 
-       // Get derived key from context
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
-       if !ok {
-               utils.JSONResponse(w, http.StatusUnauthorized, map[string]any{
-                       "success": false,
-                       "message": "User not authenticated",
-               })
-               return
-       }
+       // otherwise, we have the correct password
 
        // Generate backup codes
        codes, codeData, err := utils.GenerateBackupCodes(derivedKey)
@@ -1052,9 +999,14 @@ func CreateBackupCodes(w http.ResponseWriter, r *http.Request) {
                return
        }
 
+       available_backup_codes := len(codes)
+       if backup_codes == -1 {
+               available_backup_codes = -1
+       }
+
        utils.JSONResponse(w, http.StatusOK, map[string]any{
                "success":                true,
                "backup_codes":           codes,
-               "available_backup_codes": backupCodes,
+               "available_backup_codes": available_backup_codes,
        })
 }
index da535feac64ead3bf1a14d6ea8d1b5e911951a7c..8a3642c405a0b9bf6ef57bdd55faa943c9822c21 100644 (file)
@@ -409,20 +409,20 @@ func GetEncryptionKey(userID int, derivedKey string) (string, error) {
        return "", fmt.Errorf("user not found")
 }
 
-// CheckPasswordForUser checks if the provided password matches the user's password OR on of his backup codes
-// Returns true if the password matches, false otherwise
-// Return the amount of backup codes available for the user (-1 if password does not match)
-func CheckPasswordForUser(userID int, password string) (bool, int, error) {
+// CheckPasswordForUser checks if the provided password matches the user's password OR on of his backup codes.
+// Returns the derivedKey, if successfully validating password, otherwise empty string
+// Return the amount of backup codes available for the user (-1 if password does not match or if backup code was NOT used).
+func CheckPasswordForUser(userID int, password string) (string, int, error) {
        // Get users
        users, err := GetUsers()
        if err != nil {
-               return false, -1, fmt.Errorf("error retrieving users: %v", err)
+               return "", -1, fmt.Errorf("error retrieving users: %v", err)
        }
 
        // Find user
        usersList, ok := users["users"].([]any)
        if !ok {
-               return false, -1, fmt.Errorf("users.json is not in the correct format")
+               return "", -1, fmt.Errorf("users.json is not in the correct format")
        }
 
        for _, u := range usersList {
@@ -434,34 +434,64 @@ func CheckPasswordForUser(userID int, password string) (bool, int, error) {
                if id, ok := user["user_id"].(float64); ok && int(id) == userID {
                        passwordHash, ok := user["password"].(string)
                        if !ok {
-                               return false, -1, fmt.Errorf("user data is not in the correct format")
+                               return "", -1, fmt.Errorf("user data is not in the correct format")
                        }
 
                        if VerifyPassword(password, passwordHash) {
-                               return true, -1, nil
+                               // Calculate derived key
+                               derKey, err := DeriveKeyFromPassword(password, user["salt"].(string))
+                               if err != nil {
+                                       return "", -1, fmt.Errorf("error deriving key from password: %v", err)
+                               }
+
+                               return base64.StdEncoding.EncodeToString(derKey), -1, nil
                        }
 
                        // Check backup codes
                        backupCodes, ok := user["backup_codes"].([]any)
                        if !ok {
-                               return false, -1, fmt.Errorf("user backup codes are not in the correct format")
+                               return "", -1, nil
                        }
 
-                       for _, code := range backupCodes {
-                               codeStr, ok := code.(string)
+                       for i, code := range backupCodes {
+                               codeStr, ok := code.(map[string]any)["password"].(string)
                                if !ok {
+                                       Logger.Printf("Invalid backup code format for user %d: %v", userID, code)
                                        continue // Skip invalid codes
                                }
-                               if VerifyPassword(password, codeStr) {
-                                       return true, -1, nil
+
+                               if !VerifyPassword(password, codeStr) {
+                                       continue
+                               }
+
+                               // Password matched the code! Remove backup code
+                               backupCodes = append(backupCodes[:i], backupCodes[i+1:]...)
+
+                               // Update user data
+                               user["backup_codes"] = backupCodes
+                               if err := WriteUsers(users); err != nil {
+                                       return "", -1, fmt.Errorf("error saving updated user data: %v", err)
                                }
+
+                               // Calculate derived key
+                               tempKey, err := DeriveKeyFromPassword(password, code.(map[string]any)["salt"].(string))
+                               if err != nil {
+                                       return "", -1, fmt.Errorf("error deriving key from password: %v", err)
+                               }
+
+                               derKey, err := DecryptText(code.(map[string]any)["enc_derived_key"].(string), base64.URLEncoding.EncodeToString(tempKey))
+                               if err != nil {
+                                       return "", -1, fmt.Errorf("error decrypting derived key: %v", err)
+                               }
+
+                               return derKey, len(backupCodes), nil
                        }
 
-                       return false, -1, nil // Password does not match
+                       return "", -1, nil
                }
        }
 
-       return false, -1, fmt.Errorf("user not found")
+       return "", -1, nil
 }
 
 func CreatePasswordString() string {
index a233d56e888ce334ee2a7edc5077ef51b7e5837d..849d1a726c24e8e43a44e7b959a7e4c81db3f122 100644 (file)
                        .then((response) => {
                                if (response.data.success) {
                                        backupCodes = response.data.backup_codes;
-                               } else if (response.data.password_incorrect) {
-                                       console.error('Error creating backup codes: Password incorrect');
-                                       showBackupCodesPasswordIncorrect = true;
                                } else {
                                        console.error('Error creating backup codes');
                                        console.error(response.data);
                                <button class="btn btn-outline-secondary me-2" onclick={openSettingsModal}
                                        ><Fa icon={faSliders} /></button
                                >
-                               <button class="btn btn-outline-secondary" onclick={logout(null)}
+                               <button class="btn btn-outline-secondary" onclick={() => logout(null)}
                                        ><Fa icon={faRightFromBracket} /></button
                                >
                        </div>
                                                                                >
                                                                                        Backup-Codes generieren
                                                                                        {#if isGeneratingBackupCodes}
-                                                                                               <!-- svelte-ignore a11y_no_static_element_interactions -->
-                                                                                               <div class="spinner-border" role="status">
+                                                                                               <div class="spinner-border spinner-border-sm" role="status">
                                                                                                        <span class="visually-hidden">Loading...</span>
                                                                                                </div>
                                                                                        {/if}
                                                                                </button>
                                                                        </form>
-                                                                       {#if showBackupCodesPasswordIncorrect}
-                                                                               <div class="alert alert-danger mt-2" role="alert" transition:slide>
-                                                                                       Das eingegebene Passwort ist falsch!
-                                                                               </div>
-                                                                       {/if}
                                                                        {#if backupCodes.length > 0}
                                                                                <div class="alert alert-success alert-dismissible mt-3" transition:slide>
                                                                                        <h6>Deine Backup-Codes:</h6>
-                                                                                       Notiere dir die Codes, können nach dem Schließen dieses Fenstern nicht erneut angezeigt
-                                                                                       werden!
+                                                                                       <p>
+                                                                                               Notiere dir die Codes, sie können nach dem Schließen dieses Fenstern nicht
+                                                                                               erneut angezeigt werden!
+                                                                                       </p>
                                                                                        <button class="btn btn-secondary my-2" onclick={copyBackupCodes}>
                                                                                                <Fa icon={codesCopiedSuccess ? faCheck : faCopy} />
                                                                                                Codes kopieren
                                                                        {/if}
                                                                        {#if showBackupCodesError}
                                                                                <div class="alert alert-danger mt-2" role="alert" transition:slide>
-                                                                                       Fehler beim Erstellen der Backup-Codes!
+                                                                                       Fehler beim Erstellen der Backup-Codes! Vielleicht stimmt das Passwort nicht?
                                                                                </div>
                                                                        {/if}
                                                                </div>
index 9ffc4a5c269acd8a640302b79be32f4575f49a49..827e109533b55571af8d1abd42df4bc92f2b4f62 100644 (file)
@@ -10,6 +10,7 @@
        import { API_URL } from '$lib/APIurl.js';
        import trianglify from 'trianglify';
        import { alwaysShowSidenav } from '$lib/helpers.js';
+       import * as bootstrap from 'bootstrap';
 
        let { children } = $props();
        let inDuration = 150;
                return config;
        });
 
+       let available_backup_codes = $state(0);
+
        axios.interceptors.response.use(
                (response) => {
+                       if (response.data && response.data.available_backup_codes >= 0) {
+                               available_backup_codes = response.data.available_backup_codes;
+                               // show toast
+                               if (available_backup_codes < 6) {
+                                       let toast = new bootstrap.Toast(
+                                               document.getElementById('toastAvailableBackupCodesWarning')
+                                       );
+                                       toast.show();
+                               }
+                       }
                        return response;
                },
                (error) => {
                        </div>
                {/key}
        </div>
+
+       <div class="toast-container position-fixed bottom-0 end-0 p-3">
+               <div
+                       id="toastAvailableBackupCodesWarning"
+                       class="toast align-items-center {available_backup_codes > 3
+                               ? 'text-bg-warning'
+                               : 'text-bg-danger'}"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Noch {available_backup_codes} Backup-Codes verfügbar!</div>
+                       </div>
+               </div>
+       </div>
 </main>
 
 <style>
git clone https://git.99rst.org/PROJECT