88 lines
2.2 KiB
Go
88 lines
2.2 KiB
Go
package authn
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
const (
|
|
argonTime uint32 = 2
|
|
argonMemory uint32 = 32 * 1024
|
|
argonThreads uint8 = 1
|
|
argonKeyLen uint32 = 32
|
|
saltLen = 16
|
|
)
|
|
|
|
func HashPassword(password string) (string, error) {
|
|
if len(password) < 12 {
|
|
return "", errors.New("password must contain at least 12 characters")
|
|
}
|
|
if len(password) > 1024 {
|
|
return "", errors.New("password must not exceed 1024 bytes")
|
|
}
|
|
salt := make([]byte, saltLen)
|
|
if _, err := rand.Read(salt); err != nil {
|
|
return "", err
|
|
}
|
|
hash := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
|
|
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
|
|
argonMemory,
|
|
argonTime,
|
|
argonThreads,
|
|
base64.RawStdEncoding.EncodeToString(salt),
|
|
base64.RawStdEncoding.EncodeToString(hash),
|
|
), nil
|
|
}
|
|
|
|
func VerifyPassword(encoded, password string) bool {
|
|
if len(password) > 1024 {
|
|
return false
|
|
}
|
|
parts := strings.Split(encoded, "$")
|
|
if len(parts) != 6 || parts[1] != "argon2id" || parts[2] != "v=19" {
|
|
return false
|
|
}
|
|
params := strings.Split(parts[3], ",")
|
|
if len(params) != 3 {
|
|
return false
|
|
}
|
|
memory, ok := parseParam(params[0], "m=")
|
|
if !ok || memory < 8*1024 || memory > 256*1024 {
|
|
return false
|
|
}
|
|
timeCost, ok := parseParam(params[1], "t=")
|
|
if !ok || timeCost < 1 || timeCost > 10 {
|
|
return false
|
|
}
|
|
threads, ok := parseParam(params[2], "p=")
|
|
if !ok || threads < 1 || threads > 16 {
|
|
return false
|
|
}
|
|
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
|
if err != nil || len(salt) < 16 || len(salt) > 64 {
|
|
return false
|
|
}
|
|
expected, err := base64.RawStdEncoding.DecodeString(parts[5])
|
|
if err != nil || len(expected) < 16 || len(expected) > 64 {
|
|
return false
|
|
}
|
|
actual := argon2.IDKey([]byte(password), salt, uint32(timeCost), uint32(memory), uint8(threads), uint32(len(expected)))
|
|
return subtle.ConstantTimeCompare(actual, expected) == 1
|
|
}
|
|
|
|
func parseParam(value, prefix string) (uint64, bool) {
|
|
raw, ok := strings.CutPrefix(value, prefix)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.ParseUint(raw, 10, 32)
|
|
return n, err == nil
|
|
}
|