153 lines
4.6 KiB
Go
153 lines
4.6 KiB
Go
package authconfig
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
const (
|
|
argon2MemoryKiB uint32 = 65536
|
|
argon2Passes uint32 = 3
|
|
argon2Parallel uint8 = 1
|
|
argon2SaltBytes = 16
|
|
argon2KeyBytes uint32 = 32
|
|
|
|
argon2MaximumMemoryKiB uint32 = 256 * 1024
|
|
argon2MaximumPasses uint32 = 10
|
|
argon2MaximumParallel uint8 = 4
|
|
argon2MinimumSaltBytes = 16
|
|
argon2MaximumSaltBytes = 64
|
|
argon2MinimumKeyBytes uint32 = 16
|
|
argon2MaximumKeyBytes uint32 = 64
|
|
maximumPHCBytes = 256
|
|
)
|
|
|
|
type argon2Parameters struct {
|
|
memory uint32
|
|
passes uint32
|
|
parallelism uint8
|
|
salt []byte
|
|
digest []byte
|
|
}
|
|
|
|
// HashPassword derives a fixed-parameter Argon2id v19 PHC string. random must be a
|
|
// cryptographically secure source in production; it is injected only to permit the shared vector.
|
|
func HashPassword(password []byte, random io.Reader) (string, error) {
|
|
if !validPassword(password) {
|
|
return "", errInvalidAuthenticationConfig
|
|
}
|
|
if random == nil {
|
|
random = rand.Reader
|
|
}
|
|
salt := make([]byte, argon2SaltBytes)
|
|
if _, err := io.ReadFull(random, salt); err != nil {
|
|
return "", errInvalidAuthenticationConfig
|
|
}
|
|
digest := argon2.IDKey(password, salt, argon2Passes, argon2MemoryKiB, argon2Parallel, argon2KeyBytes)
|
|
return "$argon2id$v=19$m=65536,t=3,p=1$" + base64.RawStdEncoding.EncodeToString(salt) + "$" + base64.RawStdEncoding.EncodeToString(digest), nil
|
|
}
|
|
|
|
// VerifyPassword accepts only bounded, canonical Argon2id v19 PHC strings.
|
|
func VerifyPassword(password []byte, encoded string) bool {
|
|
if !validPassword(password) {
|
|
return false
|
|
}
|
|
parameters, ok := parsePHC(encoded)
|
|
if !ok {
|
|
return false
|
|
}
|
|
derived := argon2.IDKey(password, parameters.salt, parameters.passes, parameters.memory, parameters.parallelism, uint32(len(parameters.digest)))
|
|
return subtle.ConstantTimeCompare(derived, parameters.digest) == 1
|
|
}
|
|
|
|
func validPassword(password []byte) bool {
|
|
return len(password) >= passwordMinBytes && len(password) <= passwordMaxBytes && utf8.Valid(password)
|
|
}
|
|
|
|
func parsePHC(encoded string) (argon2Parameters, bool) {
|
|
if len(encoded) == 0 || len(encoded) > maximumPHCBytes {
|
|
return argon2Parameters{}, false
|
|
}
|
|
parts := strings.Split(encoded, "$")
|
|
if len(parts) != 6 || parts[0] != "" || parts[1] != "argon2id" || parts[2] != "v=19" {
|
|
return argon2Parameters{}, false
|
|
}
|
|
parameters, ok := parsePHCParameters(parts[3])
|
|
if !ok {
|
|
return argon2Parameters{}, false
|
|
}
|
|
salt, ok := decodePHCBase64(parts[4], argon2MinimumSaltBytes, argon2MaximumSaltBytes)
|
|
if !ok {
|
|
return argon2Parameters{}, false
|
|
}
|
|
digest, ok := decodePHCBase64(parts[5], int(argon2MinimumKeyBytes), int(argon2MaximumKeyBytes))
|
|
if !ok {
|
|
return argon2Parameters{}, false
|
|
}
|
|
parameters.salt = salt
|
|
parameters.digest = digest
|
|
return parameters, true
|
|
}
|
|
|
|
func parsePHCParameters(value string) (argon2Parameters, bool) {
|
|
parts := strings.Split(value, ",")
|
|
if len(parts) != 3 || !strings.HasPrefix(parts[0], "m=") || !strings.HasPrefix(parts[1], "t=") || !strings.HasPrefix(parts[2], "p=") {
|
|
return argon2Parameters{}, false
|
|
}
|
|
memory, ok := parseDecimal(parts[0][2:], uint64(argon2MaximumMemoryKiB))
|
|
if !ok || memory < 8 {
|
|
return argon2Parameters{}, false
|
|
}
|
|
passes, ok := parseDecimal(parts[1][2:], uint64(argon2MaximumPasses))
|
|
if !ok || passes == 0 {
|
|
return argon2Parameters{}, false
|
|
}
|
|
parallelism, ok := parseDecimal(parts[2][2:], uint64(argon2MaximumParallel))
|
|
if !ok || parallelism == 0 || memory < 8*parallelism {
|
|
return argon2Parameters{}, false
|
|
}
|
|
return argon2Parameters{memory: uint32(memory), passes: uint32(passes), parallelism: uint8(parallelism)}, true
|
|
}
|
|
|
|
func parseDecimal(value string, maximum uint64) (uint64, bool) {
|
|
if value == "" || (len(value) > 1 && value[0] == '0') || len(value) > 10 {
|
|
return 0, false
|
|
}
|
|
for _, character := range value {
|
|
if character < '0' || character > '9' {
|
|
return 0, false
|
|
}
|
|
}
|
|
parsed, err := strconv.ParseUint(value, 10, 64)
|
|
if err != nil || parsed > maximum {
|
|
return 0, false
|
|
}
|
|
return parsed, true
|
|
}
|
|
|
|
func decodePHCBase64(value string, minimum, maximum int) ([]byte, bool) {
|
|
if value == "" || strings.ContainsRune(value, '=') || len(value) > base64.RawStdEncoding.EncodedLen(maximum) || len(value) < base64.RawStdEncoding.EncodedLen(minimum)-1 {
|
|
return nil, false
|
|
}
|
|
decoded, err := base64.RawStdEncoding.DecodeString(value)
|
|
if err != nil || len(decoded) < minimum || len(decoded) > maximum {
|
|
return nil, false
|
|
}
|
|
return decoded, true
|
|
}
|
|
|
|
func validatePasswordHash(encoded string) error {
|
|
if _, ok := parsePHC(encoded); !ok {
|
|
return errors.New("invalid password hash")
|
|
}
|
|
return nil
|
|
}
|