Files
ThothII/tools/tht/internal/authconfig/password.go

147 lines
4.4 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
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 the exact canonical Argon2id v19 policy.
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], argon2SaltBytes, argon2SaltBytes)
if !ok {
return argon2Parameters{}, false
}
digest, ok := decodePHCBase64(parts[5], int(argon2KeyBytes), int(argon2KeyBytes))
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(argon2MemoryKiB))
if !ok || memory != uint64(argon2MemoryKiB) {
return argon2Parameters{}, false
}
passes, ok := parseDecimal(parts[1][2:], uint64(argon2Passes))
if !ok || passes != uint64(argon2Passes) {
return argon2Parameters{}, false
}
parallelism, ok := parseDecimal(parts[2][2:], uint64(argon2Parallel))
if !ok || parallelism != uint64(argon2Parallel) {
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
}
encoding := base64.RawStdEncoding.Strict()
decoded, err := encoding.DecodeString(value)
if err != nil || len(decoded) < minimum || len(decoded) > maximum || encoding.EncodeToString(decoded) != value {
return nil, false
}
return decoded, true
}
func validatePasswordHash(encoded string) error {
if _, ok := parsePHC(encoded); !ok {
return errors.New("invalid password hash")
}
return nil
}