From 8e949f837bce8ba735c011be8137e414c2cbcd88 Mon Sep 17 00:00:00 2001 From: Yorick van Pelt Date: Sun, 30 Apr 2023 18:16:52 +0200 Subject: [PATCH] Use PHC hashes for password storage --- handlers.go | 3 +++ internal/db/user.go | 52 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/handlers.go b/handlers.go index 0c273d5..35cb9e3 100644 --- a/handlers.go +++ b/handlers.go @@ -338,6 +338,9 @@ func (rl *rushlink) authenticateUser(w http.ResponseWriter, r *http.Request, sho // Authentication failed, return a 401 Unauthorized response w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) w.WriteHeader(http.StatusUnauthorized) + if err != nil { + log.Printf("authentication failure: %s", err) + } return nil } return user diff --git a/internal/db/user.go b/internal/db/user.go index 67e1942..4c757d7 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "errors" "fmt" + "strings" "time" "github.com/google/uuid" @@ -54,37 +55,64 @@ func Authenticate(db *gorm.DB, username string, password string) (*User, error) } // Compare the hashed password with the provided password - if !comparePassword(user.Password, 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, 16) + 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, 64) + hash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 1, pwdHashSize) - // Encode the salt and hash as a string - return string(salt) + string(hash), nil + // 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 } -func comparePassword(hashedPassword string, password string) bool { - // Decode the salt and hash from the hashed password string - salt := []byte(hashedPassword)[:16] - hash := []byte(hashedPassword)[16:] +func comparePassword(hashedPassword string, password string) (bool, error) { + // Extract the salt and hash from the hashed password string + fields := strings.Split(hashedPassword, "$")[1:] + if len(fields) != 4 || fields[0] != pwdAlgo || fields[1] != pwdParams { + return false, errors.New("invalid password format in db") + } + encodedSalt, encodedHash := fields[2], fields[3] - // Hash the password using the same salt and parameters - computedHash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 1, 64) + // 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 bytes.Equal(hash, computedHash) + // todo constant time? + return bytes.Equal(hash, computedHash), nil } // DeleteUser deletes a user with the specified username from the database.