Sushi/password/password.go
2025-08-15 14:23:09 -05:00

80 lines
1.7 KiB
Go

package password
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
const (
argonTime = 1
argonMemory = 64 * 1024
argonThreads = 4
argonKeyLen = 32
)
// HashPassword creates an argon2id hash of the password
func HashPassword(password string) string {
salt := make([]byte, 16)
rand.Read(salt)
hash := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version, argonMemory, argonTime, argonThreads, b64Salt, b64Hash)
return encoded
}
// VerifyPassword checks if a password matches the hash
func VerifyPassword(password, encodedHash string) (bool, error) {
parts := strings.Split(encodedHash, "$")
if len(parts) != 6 {
return false, fmt.Errorf("invalid hash format")
}
if parts[1] != "argon2id" {
return false, fmt.Errorf("invalid hash variant")
}
var version int
_, err := fmt.Sscanf(parts[2], "v=%d", &version)
if err != nil {
return false, err
}
if version != argon2.Version {
return false, fmt.Errorf("incompatible argon2 version")
}
var m, t, p uint32
_, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p)
if err != nil {
return false, err
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return false, err
}
expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return false, err
}
hash := argon2.IDKey([]byte(password), salt, t, m, uint8(p), uint32(len(expectedHash)))
if subtle.ConstantTimeCompare(hash, expectedHash) == 1 {
return true, nil
}
return false, nil
}