// Get user ID from context
userID, ok := r.Context().Value(utils.UserIDKey).(int)
if !ok {
- utils.JSONResponse(w, http.StatusOK, map[string]any{
+ utils.JSONResponse(w, http.StatusUnauthorized, map[string]any{
"success": false,
"message": "User not authenticated",
})
// Get derived key from context
derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
if !ok {
- utils.JSONResponse(w, http.StatusOK, map[string]any{
+ utils.JSONResponse(w, http.StatusUnauthorized, map[string]any{
"success": false,
"message": "User not authenticated",
})
// Get user ID from context
userID, ok := r.Context().Value(utils.UserIDKey).(int)
if !ok {
- utils.JSONResponse(w, http.StatusOK, map[string]any{
+ utils.JSONResponse(w, http.StatusUnauthorized, map[string]any{
"success": false,
"message": "User not authenticated",
})
"success": true,
})
}
+
+type CreateBackupCodesRequest struct {
+ Password string `json:"password"`
+}
+
+// CreateBackupCodes creates 6 random backup codes for the user
+// Those are storing the encrypted derived key from the original password!
+func CreateBackupCodes(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.StatusUnauthorized, map[string]any{
+ "success": false,
+ "message": "User not authenticated",
+ })
+ return
+ }
+
+ // Parse request body
+ var req CreateBackupCodesRequest
+ 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
+ }
+
+ // Check if password is correct
+ correct, backupCodes, err := utils.CheckPasswordForUser(userID, req.Password)
+ if err != nil {
+ 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,
+ })
+ 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
+ }
+
+ // Generate backup codes
+ codes, codeData, err := utils.GenerateBackupCodes(derivedKey)
+ if err != nil {
+ utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+ "success": false,
+ "message": fmt.Sprintf("Error generating backup codes: %v", err),
+ })
+ return
+ }
+
+ // Save backup codes to file
+ if err := utils.SaveBackupCodes(userID, codeData); err != nil {
+ utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+ "success": false,
+ "message": fmt.Sprintf("Error saving backup codes: %v", err),
+ })
+ return
+ }
+
+ utils.JSONResponse(w, http.StatusOK, map[string]any{
+ "success": true,
+ "backup_codes": codes,
+ "available_backup_codes": backupCodes,
+ })
+}
mux.HandleFunc("POST /users/saveUserSettings", middleware.RequireAuth(handlers.SaveUserSettings))
mux.HandleFunc("POST /users/changePassword", middleware.RequireAuth(handlers.ChangePassword))
mux.HandleFunc("POST /users/deleteAccount", middleware.RequireAuth(handlers.DeleteAccount))
+ mux.HandleFunc("POST /users/createBackupCodes", middleware.RequireAuth(handlers.CreateBackupCodes))
mux.HandleFunc("POST /logs/saveLog", middleware.RequireAuth(handlers.SaveLog))
mux.HandleFunc("GET /logs/getLog", middleware.RequireAuth(handlers.GetLog))
return nil
}
+
+// saves the hash, salt and encrypted derived key of the backup codes to the users.json file
+func SaveBackupCodes(userID int, codes []map[string]any) error {
+ // Get the current users
+ users, err := GetUsers()
+ if err != nil {
+ return fmt.Errorf("error getting users: %v", err)
+ }
+
+ // Find the user with the given ID in the users array
+ usersList, ok := users["users"].([]any)
+ if !ok {
+ return fmt.Errorf("invalid users format, 'users' is not an array")
+ }
+
+ var foundUser 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 {
+ foundUser = uMap
+ break
+ }
+ }
+
+ if foundUser == nil {
+ return fmt.Errorf("user with ID %d does not exist", userID)
+ }
+
+ // Save the backup codes to the user's data
+ foundUser["backup_codes"] = codes
+
+ // Write the updated users back to the file
+ if err := WriteUsers(users); err != nil {
+ return fmt.Errorf("error writing users: %v", err)
+ }
+
+ return nil
+}
"encoding/base64"
"fmt"
"io"
+ "math/big"
"runtime"
"strings"
"time"
KeyLength uint32
}
-// HashPassword hashes a password using Argon2
-func HashPassword(password string) (string, error) {
- config := &Argon2Configuration{
+func getArgon2Configuration() *Argon2Configuration {
+ return &Argon2Configuration{
TimeCost: 5,
MemoryCost: 64 * 1024,
Threads: uint8(runtime.NumCPU()),
KeyLength: 32,
}
+}
+
+// HashPassword hashes a password using Argon2
+func HashPassword(password string) (string, error) {
+ config := getArgon2Configuration()
// Generate a random salt
salt := make([]byte, 16)
return nil, err
}
- // Derive key
+ // Derive key (don't use config from above, as a variable amount of thread will lead to different results)
key := argon2.IDKey([]byte(password), salt, 2, 64*1024, 4, 32)
return key, nil
}
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) {
+ // Get users
+ users, err := GetUsers()
+ if err != nil {
+ return false, -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")
+ }
+
+ for _, u := range usersList {
+ user, ok := u.(map[string]any)
+ if !ok {
+ continue
+ }
+
+ 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")
+ }
+
+ if VerifyPassword(password, passwordHash) {
+ return true, -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")
+ }
+
+ for _, code := range backupCodes {
+ codeStr, ok := code.(string)
+ if !ok {
+ continue // Skip invalid codes
+ }
+ if VerifyPassword(password, codeStr) {
+ return true, -1, nil
+ }
+ }
+
+ return false, -1, nil // Password does not match
+ }
+ }
+
+ return false, -1, fmt.Errorf("user not found")
+}
+
+func CreatePasswordString() string {
+ var chars string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+-_*!?#$%&(){}[]=@~"
+ password := make([]byte, 10)
+
+ for i := range password {
+ n, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
+ if err != nil {
+ panic(fmt.Sprintf("Failed to generate random index: %v", err))
+ }
+ password[i] = chars[n.Int64()]
+ }
+
+ return string(password)
+}
+
+// GenerateBackupCodes generates 6 backup codes for a user
+// With those backup-codes, the derived key gets encrypted
+func GenerateBackupCodes(derived_key string) ([]string, []map[string]any, error) {
+ backupCodes := make([]string, 6)
+ codeData := make([]map[string]any, 6)
+ for i := range 6 {
+ // Initialize the map for this index
+ codeData[i] = make(map[string]any)
+
+ // Generate a random backup code (=password (!= uuid))
+ code := CreatePasswordString()
+
+ // create hash
+ hash, err := HashPassword(code)
+ if err != nil {
+ return nil, nil, fmt.Errorf("error hashing backup code: %v", err)
+ }
+
+ // Generate a random salt
+ salt := make([]byte, 16)
+ if _, err := rand.Read(salt); err != nil {
+ return nil, nil, fmt.Errorf("error generating salt: %v", err)
+ }
+ // Convert salt to base64
+ saltBase64 := base64.StdEncoding.EncodeToString(salt)
+
+ // Create derived encryption key to later encrypt the original derived key
+ intermediateKey, err := DeriveKeyFromPassword(code, saltBase64)
+ if err != nil {
+ return nil, nil, fmt.Errorf("error deriving key from password: %v", err)
+ }
+
+ // Encrypt the derived key with the intermediate key from the backup code
+ encDerivedKey, err := EncryptText(derived_key, base64.URLEncoding.EncodeToString(intermediateKey))
+ if err != nil {
+ return nil, nil, fmt.Errorf("error encrypting derived key: %v", err)
+ }
+
+ backupCodes[i] = code
+ codeData[i]["password"] = hash
+ codeData[i]["salt"] = saltBase64
+ codeData[i]["enc_derived_key"] = encDerivedKey
+ }
+
+ return backupCodes, codeData, nil
+}
faPencil,
faSliders,
faTriangleExclamation,
- faTrash
+ faTrash,
+ faCopy,
+ faCheck
} from '@fortawesome/free-solid-svg-icons';
import Tag from '$lib/Tag.svelte';
import SelectTimezone from '$lib/SelectTimezone.svelte';
});
}, 400);
});
+
+ document.getElementById('settingsModal').addEventListener('hidden.bs.modal', function () {
+ backupCodes = [];
+ });
});
function logout(errorCode) {
deleteAccountPassword = '';
});
}
+
+ let backupCodesPassword = $state('');
+ let isGeneratingBackupCodes = $state(false);
+ let backupCodes = $state([]);
+ let showBackupCodesPasswordIncorrect = $state(false);
+ let codesCopiedSuccess = $state(false);
+ let showBackupCodesError = $state(false);
+
+ function createBackupCodes() {
+ if (isGeneratingBackupCodes) return;
+ isGeneratingBackupCodes = true;
+
+ showBackupCodesPasswordIncorrect = false;
+ showBackupCodesError = false;
+ backupCodes = [];
+
+ axios
+ .post(API_URL + '/users/createBackupCodes', {
+ password: backupCodesPassword
+ })
+ .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);
+ showBackupCodesError = true;
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+ const toast = new bootstrap.Toast(document.getElementById('toastErrorCreateBackupCodes'));
+ toast.show();
+ })
+ .finally(() => {
+ isGeneratingBackupCodes = false;
+ });
+ }
+
+ function copyBackupCodes() {
+ if (backupCodes.length === 0) return;
+
+ const codesText = backupCodes.join('\n');
+ navigator.clipboard.writeText(codesText).then(
+ () => {
+ // Show success checkmark for 3 seconds
+ codesCopiedSuccess = true;
+ setTimeout(() => {
+ codesCopiedSuccess = false;
+ }, 3000);
+ },
+ (err) => {
+ console.error('Failed to copy backup codes: ', err);
+ }
+ );
+ }
</script>
<div class="d-flex flex-column h-100">
<h3 class="text-primary">#️⃣ Tags</h3>
<div>
Hier können Tags bearbeitet oder auch vollständig aus DailyTxT gelöscht werden.
+ {#if $tags.length === 0}
+ <div class="alert alert-info my-2" role="alert">
+ Es sind noch keine Tags vorhanden. Erstelle einen neuen Tag im Schreibmodus.
+ </div>
+ {/if}
<div class="d-flex flex-column tagColumn mt-1">
{#each $tags as tag}
<Tag
<div id="data">
<h3 class="text-primary">📁 Daten</h3>
- <div id="export"><h5>Export</h5></div>
- <div id="import"><h5>Import</h5></div>
+ <div><h5>Export</h5></div>
+ <div><h5>Import</h5></div>
</div>
<div id="security">
<h3 class="text-primary">🔒 Sicherheit</h3>
- <div id="password">
+ <div>
<h5>Password ändern</h5>
<form onsubmit={changePassword}>
<div class="form-floating mb-3">
{#if changingPasswordSuccess}
<div class="alert alert-success mt-2" role="alert" transition:slide>
Das Passwort wurde erfolgreich geändert!<br />
- Backup-Keys wurden ungültig gemacht (sofern vorhanden), und müssen neu erstellt
+ Backup-Codes wurden ungültig gemacht (sofern vorhanden), und müssen neu erstellt
werden.
</div>
{/if}
</div>
{/if}
</div>
- <div id="backupkeys"><h5>Backup-Keys</h5></div>
- <div id="username"><h5>Username ändern</h5></div>
- <div id="deleteaccount">
+ <div>
+ <h5>Backup-Codes</h5>
+ <ul>
+ <li>
+ Backup-Codes funktionieren wie Einmal-Passwörter. Sie können immer anstelle
+ des Passworts verwendet werden, allerdings sind sie jeweils nur einmal gültig
+ und werden anschließend gelöscht.
+ </li>
+ <li>
+ Es werden immer 6 Codes generiert, welche die vorherigen Codes (sofern
+ vorhanden) ersetzen.
+ </li>
+ <li>
+ Du musst dir die Codes nach der Erstellung direkt notieren, sie können nicht
+ erneut angezeigt werden!
+ </li>
+ </ul>
+
+ <form onsubmit={createBackupCodes}>
+ <div class="form-floating mb-3">
+ <input
+ type="password"
+ class="form-control"
+ id="currentPassword"
+ placeholder="Aktuelles Passwort"
+ bind:value={backupCodesPassword}
+ />
+ <label for="currentPassword">Passwort bestätigen</label>
+ </div>
+ <button
+ class="btn btn-primary"
+ onclick={createBackupCodes}
+ data-sveltekit-noscroll
+ >
+ Backup-Codes generieren
+ {#if isGeneratingBackupCodes}
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
+ <div class="spinner-border" 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!
+ <button class="btn btn-secondary my-2" onclick={copyBackupCodes}>
+ <Fa icon={codesCopiedSuccess ? faCheck : faCopy} />
+ Codes kopieren
+ </button>
+ <ul class="list-group">
+ {#each backupCodes as code}
+ <li class="list-group-item backupCode">
+ <code>{code}</code>
+ </li>
+ {/each}
+ </ul>
+ </div>
+ {/if}
+ {#if showBackupCodesError}
+ <div class="alert alert-danger mt-2" role="alert" transition:slide>
+ Fehler beim Erstellen der Backup-Codes!
+ </div>
+ {/if}
+ </div>
+ <div><h5>Username ändern</h5></div>
+ <div>
<h5>Konto löschen</h5>
<p>
Dies löscht dein Konto und alle damit verbundenen Daten. Dies kann nicht
</div>
<style>
+ .backupCode {
+ font-size: 15pt;
+ }
+
.footer-unsaved-changes {
background-color: orange;
color: black;