package db import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "time" "github.com/google/uuid" "golang.org/x/crypto/argon2" "gorm.io/gorm" ) type User struct { ID uint `gorm:"primaryKey"` User string `gorm:"uniqueIndex"` Password string Admin bool CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt } func NewUser(db *gorm.DB, username string, password string, admin bool) error { // Generate a new UUID for the user id, err := uuid.NewRandom() if err != nil { return err } // Hash the password using argon2id hashedPassword, err := HashPassword(password) if err != nil { return err } // Create a new user record user := &User{ ID: uint(id.ID()), User: username, Password: hashedPassword, Admin: admin, } return db.Create(user).Error } func Authenticate(db *gorm.DB, username string, password string) (*User, error) { // Get the user record by username var user User if err := db.Where("user = ?", username).First(&user).Error; err != nil { return nil, errors.New("user not found") } // Compare the hashed password with the provided password valid, err := comparePassword(user.Password, password) if err != nil { return nil, err } if !valid { return nil, errors.New("invalid password") } return &user, nil } const ( pwdSaltSize = 16 pwdHashSize = 32 pwdParams = "m=65536,t=2,p=1" pwdAlgo = "argon2id" ) func HashPassword(password string) (string, error) { // Generate a salt for the password hash salt := make([]byte, pwdSaltSize) if _, err := rand.Read(salt); err != nil { return "", err } // Hash the password using argon2id hash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 1, pwdHashSize) // Encode the salt and hash as a string in PHC format encodedSalt := base64.RawStdEncoding.EncodeToString(salt) encodedHash := base64.RawStdEncoding.EncodeToString(hash) return fmt.Sprintf("$%s$%s$%s$%s", pwdAlgo, pwdParams, encodedSalt, encodedHash), nil } var errInvalidDBPasswordFormat = errors.New("invalid password format in db") func comparePassword(hashedPassword string, password string) (bool, error) { // Extract the salt and hash from the hashed password string fields := strings.Split(hashedPassword, "$") if len(fields) != 5 || fields[1] != pwdAlgo || fields[2] != pwdParams { return false, errInvalidDBPasswordFormat } encodedSalt, encodedHash := fields[3], fields[4] // Decode the salt and hash from base64 salt, err := base64.RawStdEncoding.DecodeString(encodedSalt) if err != nil { return false, err } hash, err := base64.RawStdEncoding.DecodeString(encodedHash) if err != nil { return false, err } // Hash the password using the extracted salt and parameters computedHash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 1, pwdHashSize) // Compare the computed hash with the stored hash return subtle.ConstantTimeCompare(hash, computedHash) == 1, nil } // DeleteUser deletes a user with the specified username from the database. func DeleteUser(db *gorm.DB, username string) error { // Find the user by username var user User if err := db.Where("user = ?", username).First(&user).Error; err != nil { return err } // Delete the user if err := db.Delete(&user).Error; err != nil { return err } return nil } func ChangeUser(db *gorm.DB, username string, updatedUser *User) error { // Retrieve the existing user var existingUser User if err := db.Where("user = ?", username).First(&existingUser).Error; err != nil { return err } // Update the user fields existingUser.User = updatedUser.User existingUser.Password = updatedUser.Password existingUser.Admin = updatedUser.Admin // Save the updated user to the database if err := db.Save(&existingUser).Error; err != nil { return err } return nil } func CreateAdminUser(db *gorm.DB, adminUsername string) error { // Check if the admin user already exists var admins []User if err := db.Unscoped().Limit(1).Where("user = ?", adminUsername).Find(&admins).Error; err != nil { return err } if len(admins) > 0 { // already exists return nil } // Generate a random 24-char password passwordBytes := make([]byte, 24) if _, err := rand.Read(passwordBytes); err != nil { return err } password := base64.URLEncoding.EncodeToString(passwordBytes) // Create the admin user if err := NewUser(db, adminUsername, password, true); err != nil { return err } // Print out the generated password fmt.Printf("Generated password for admin user %s: %s\n", adminUsername, password) return nil }