115 lines
3.1 KiB
Go
115 lines
3.1 KiB
Go
package hashing
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
// Inspired by: https://github.com/alexedwards/argon2id @ 2020-04-22T14:13:23ZZ
|
|
|
|
const (
|
|
// Argon2HashID represents the hash ID set in the (pseudo) modular crypt format used to store the hashed password and params in a single string.
|
|
Argon2HashID = "argon2id"
|
|
)
|
|
|
|
var (
|
|
// ErrInvalidArgon2Hash indicates the argon2id hash was malformed and could not be decoded.
|
|
ErrInvalidArgon2Hash = errors.New("invalid argon2id hash")
|
|
// ErrIncompatibleArgon2Version indicates the argon2id hash provided was generated with a different, incompatible argon2 version.
|
|
ErrIncompatibleArgon2Version = errors.New("incompatible argon2 version")
|
|
)
|
|
|
|
func HashPassword(password string, params *Argon2Params) (string, error) {
|
|
salt, err := generateSalt(params.SaltLength)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
key := argon2.IDKey([]byte(password), salt, params.Time, params.Memory, params.Threads, params.KeyLength)
|
|
|
|
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
|
b64Key := base64.RawStdEncoding.EncodeToString(key)
|
|
|
|
return fmt.Sprintf("$%s$v=%d$m=%d,t=%d,p=%d$%s$%s", Argon2HashID, argon2.Version, params.Memory, params.Time, params.Threads, b64Salt, b64Key), nil
|
|
}
|
|
|
|
func ComparePasswordAndHash(password string, hash string) (bool, error) {
|
|
params, salt, key, err := decodeArgon2Hash(hash)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
pKey := argon2.IDKey([]byte(password), salt, params.Time, params.Memory, params.Threads, params.KeyLength)
|
|
|
|
keyLen := len(key)
|
|
pKeyLen := len(pKey)
|
|
|
|
if keyLen > math.MaxInt32 || pKeyLen > math.MaxInt32 {
|
|
return false, ErrInvalidArgon2Hash
|
|
}
|
|
|
|
if subtle.ConstantTimeEq(int32(keyLen), int32(pKeyLen)) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
if subtle.ConstantTimeCompare(key, pKey) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
const (
|
|
argon2HashParts = 6
|
|
)
|
|
|
|
func decodeArgon2Hash(hash string) (*Argon2Params, []byte, []byte, error) {
|
|
vals := strings.Split(hash, "$") // splits into array of 6 values, with val[0] being empty --> length/indicies "offset" by one
|
|
if len(vals) != argon2HashParts {
|
|
return nil, nil, nil, ErrInvalidArgon2Hash
|
|
}
|
|
if vals[1] != Argon2HashID {
|
|
return nil, nil, nil, ErrInvalidArgon2Hash
|
|
}
|
|
|
|
var version int
|
|
_, err := fmt.Sscanf(vals[2], "v=%d", &version)
|
|
if err != nil {
|
|
return nil, nil, nil, ErrIncompatibleArgon2Version
|
|
}
|
|
if version != argon2.Version {
|
|
return nil, nil, nil, ErrIncompatibleArgon2Version
|
|
}
|
|
|
|
params := &Argon2Params{}
|
|
_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", ¶ms.Memory, ¶ms.Time, ¶ms.Threads)
|
|
if err != nil {
|
|
return nil, nil, nil, ErrInvalidArgon2Hash
|
|
}
|
|
|
|
salt, err := base64.RawStdEncoding.DecodeString(vals[4])
|
|
if err != nil {
|
|
return nil, nil, nil, ErrInvalidArgon2Hash
|
|
}
|
|
|
|
key, err := base64.RawStdEncoding.DecodeString(vals[5])
|
|
if err != nil {
|
|
return nil, nil, nil, ErrInvalidArgon2Hash
|
|
}
|
|
|
|
keyLength := len(key)
|
|
if keyLength > math.MaxInt32 {
|
|
return nil, nil, nil, ErrInvalidArgon2Hash
|
|
}
|
|
|
|
params.KeyLength = uint32(keyLength)
|
|
|
|
return params, salt, key, nil
|
|
}
|