security: Zufallspasswörter beim Erststart, kryptographisch sichere JTI-Generierung

- seedDefaultUsers: generiert kryptographisch zufällige Passwörter (crypto/rand)
  statt hartkodiertes "archivmailrockz" — Passwörter werden einmalig im Terminal
  angezeigt und können danach nicht wiederhergestellt werden
- generateJTI: verwendet crypto/rand (16 Byte, hex) statt time.UnixNano XOR deadbeef

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-17 01:19:24 +01:00
parent 7e165c8eed
commit bb963a796f
25 changed files with 471 additions and 111 deletions
+38 -4
View File
@@ -2,6 +2,8 @@ package main
import (
"context"
"crypto/rand"
"encoding/hex"
"flag"
"fmt"
"log/slog"
@@ -321,6 +323,8 @@ func runIntegrityCheck(ctx context.Context, store *storage.Store, logger *slog.L
}
// seedDefaultUsers creates default admin and auditor accounts if no users exist yet.
// Passwords are randomly generated and printed once to stdout — there is no way to
// recover them afterwards; they must be changed immediately after the first login.
func seedDefaultUsers(users *userstore.Store, logger *slog.Logger) error {
all, err := users.List("")
if err != nil {
@@ -329,16 +333,46 @@ func seedDefaultUsers(users *userstore.Store, logger *slog.Logger) error {
if len(all) > 0 {
return nil // already seeded
}
adminPw, err := randomPassword()
if err != nil {
return fmt.Errorf("generate admin password: %w", err)
}
auditorPw, err := randomPassword()
if err != nil {
return fmt.Errorf("generate auditor password: %w", err)
}
defaults := []userstore.CreateUserRequest{
{Username: "admin", Email: "admin@archivmail.local", Password: "archivmailrockz", Role: userstore.RoleAdmin},
{Username: "auditor", Email: "auditor@archivmail.local", Password: "archivmailrockz", Role: userstore.RoleAuditor},
{Username: "admin", Email: "admin@archivmail.local", Password: adminPw, Role: userstore.RoleAdmin},
{Username: "auditor", Email: "auditor@archivmail.local", Password: auditorPw, Role: userstore.RoleAuditor},
}
for _, req := range defaults {
if _, err := users.Create(req); err != nil {
return fmt.Errorf("create default user %s: %w", req.Username, err)
}
logger.Info("created default user", "username", req.Username, "role", req.Role)
}
logger.Warn("default users created — change passwords immediately!", "admin", "admin", "auditor", "auditor")
// Print credentials prominently — this is the only time they are visible.
fmt.Println()
fmt.Println("╔══════════════════════════════════════════════════════════════╗")
fmt.Println("║ ARCHIVMAIL — ERSTMALIGE EINRICHTUNG ║")
fmt.Println("║ Initiale Zugangsdaten (NUR EINMAL ANGEZEIGT): ║")
fmt.Printf( "║ admin : %-50s ║\n", adminPw)
fmt.Printf( "║ auditor : %-50s ║\n", auditorPw)
fmt.Println("║ Passwörter sofort nach dem ersten Login ändern! ║")
fmt.Println("╚══════════════════════════════════════════════════════════════╝")
fmt.Println()
logger.Warn("default users created — change passwords immediately!")
return nil
}
// randomPassword generates a cryptographically random 16-byte hex password.
func randomPassword() (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}