218 lines
7.2 KiB
Go
218 lines
7.2 KiB
Go
package authentication
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/simia-tech/crypt"
|
|
|
|
"github.com/authelia/authelia/internal/utils"
|
|
)
|
|
|
|
// PasswordHash represents all characteristics of a password hash.
|
|
// Authelia only supports salted SHA512 or salted argon2id method, i.e., $6$ mode or $argon2id$ mode.
|
|
type PasswordHash struct {
|
|
Algorithm CryptAlgo
|
|
Iterations int
|
|
Salt string
|
|
Key string
|
|
KeyLength int
|
|
Memory int
|
|
Parallelism int
|
|
}
|
|
|
|
// ConfigAlgoToCryptoAlgo returns a CryptAlgo and nil error if valid, otherwise it returns argon2id and an error.
|
|
func ConfigAlgoToCryptoAlgo(fromConfig string) (CryptAlgo, error) {
|
|
switch fromConfig {
|
|
case argon2id:
|
|
return HashingAlgorithmArgon2id, nil
|
|
case sha512:
|
|
return HashingAlgorithmSHA512, nil
|
|
default:
|
|
return HashingAlgorithmArgon2id, errors.New("Invalid algorithm in configuration. It should be `argon2id` or `sha512`")
|
|
}
|
|
}
|
|
|
|
// ParseHash extracts all characteristics of a hash given its string representation.
|
|
func ParseHash(hash string) (passwordHash *PasswordHash, err error) {
|
|
parts := strings.Split(hash, "$")
|
|
|
|
// This error can be ignored as it's always nil.
|
|
c, parameters, salt, key, _ := crypt.DecodeSettings(hash)
|
|
code := CryptAlgo(c)
|
|
h := &PasswordHash{}
|
|
|
|
h.Salt = salt
|
|
h.Key = key
|
|
|
|
if h.Key != parts[len(parts)-1] {
|
|
return nil, fmt.Errorf("Hash key is not the last parameter, the hash is likely malformed (%s)", hash)
|
|
}
|
|
|
|
if h.Key == "" {
|
|
return nil, fmt.Errorf("Hash key contains no characters or the field length is invalid (%s)", hash)
|
|
}
|
|
|
|
_, err = crypt.Base64Encoding.DecodeString(h.Salt)
|
|
if err != nil {
|
|
return nil, errors.New("Salt contains invalid base64 characters")
|
|
}
|
|
|
|
switch code {
|
|
case HashingAlgorithmSHA512:
|
|
h.Iterations = parameters.GetInt("rounds", HashingDefaultSHA512Iterations)
|
|
h.Algorithm = HashingAlgorithmSHA512
|
|
|
|
if parameters["rounds"] != "" && parameters["rounds"] != strconv.Itoa(h.Iterations) {
|
|
return nil, fmt.Errorf("SHA512 iterations is not numeric (%s)", parameters["rounds"])
|
|
}
|
|
case HashingAlgorithmArgon2id:
|
|
version := parameters.GetInt("v", 0)
|
|
if version < 19 {
|
|
if version == 0 {
|
|
return nil, fmt.Errorf("Argon2id version parameter not found (%s)", hash)
|
|
}
|
|
|
|
return nil, fmt.Errorf("Argon2id versions less than v19 are not supported (hash is version %d)", version)
|
|
} else if version > 19 {
|
|
return nil, fmt.Errorf("Argon2id versions greater than v19 are not supported (hash is version %d)", version)
|
|
}
|
|
|
|
h.Algorithm = HashingAlgorithmArgon2id
|
|
h.Memory = parameters.GetInt("m", HashingDefaultArgon2idMemory)
|
|
h.Iterations = parameters.GetInt("t", HashingDefaultArgon2idTime)
|
|
h.Parallelism = parameters.GetInt("p", HashingDefaultArgon2idParallelism)
|
|
h.KeyLength = parameters.GetInt("k", HashingDefaultArgon2idKeyLength)
|
|
|
|
decodedKey, err := crypt.Base64Encoding.DecodeString(h.Key)
|
|
|
|
if err != nil {
|
|
return nil, errors.New("Hash key contains invalid base64 characters")
|
|
}
|
|
|
|
if len(decodedKey) != h.KeyLength {
|
|
return nil, fmt.Errorf("Argon2id key length parameter (%d) does not match the actual key length (%d)", h.KeyLength, len(decodedKey))
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("Authelia only supports salted SHA512 hashing ($6$) and salted argon2id ($argon2id$), not $%s$", code)
|
|
}
|
|
|
|
return h, nil
|
|
}
|
|
|
|
// HashPassword generate a salt and hash the password with the salt and a constant number of rounds.
|
|
func HashPassword(password, salt string, algorithm CryptAlgo, iterations, memory, parallelism, keyLength, saltLength int) (hash string, err error) {
|
|
var settings string
|
|
|
|
if algorithm != HashingAlgorithmArgon2id && algorithm != HashingAlgorithmSHA512 {
|
|
return "", fmt.Errorf("Hashing algorithm input of '%s' is invalid, only values of %s and %s are supported", algorithm, HashingAlgorithmArgon2id, HashingAlgorithmSHA512)
|
|
}
|
|
|
|
if algorithm == HashingAlgorithmArgon2id {
|
|
err := validateArgon2idSettings(memory, parallelism, iterations, keyLength)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
err = validateSalt(salt, saltLength)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if salt == "" {
|
|
salt = crypt.Base64Encoding.EncodeToString([]byte(utils.RandomString(saltLength, HashingPossibleSaltCharacters)))
|
|
}
|
|
|
|
settings = getCryptSettings(salt, algorithm, iterations, memory, parallelism, keyLength)
|
|
|
|
// This error can be ignored because we check for it before a user gets here.
|
|
hash, _ = crypt.Crypt(password, settings)
|
|
|
|
return hash, nil
|
|
}
|
|
|
|
// CheckPassword check a password against a hash.
|
|
func CheckPassword(password, hash string) (ok bool, err error) {
|
|
expectedHash, err := ParseHash(hash)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
passwordHashString, err := HashPassword(password, expectedHash.Salt, expectedHash.Algorithm, expectedHash.Iterations, expectedHash.Memory, expectedHash.Parallelism, expectedHash.KeyLength, len(expectedHash.Salt))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
passwordHash, err := ParseHash(passwordHashString)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return passwordHash.Key == expectedHash.Key, nil
|
|
}
|
|
|
|
func getCryptSettings(salt string, algorithm CryptAlgo, iterations, memory, parallelism, keyLength int) (settings string) {
|
|
switch algorithm {
|
|
case HashingAlgorithmArgon2id:
|
|
settings, _ = crypt.Argon2idSettings(memory, iterations, parallelism, keyLength, salt)
|
|
case HashingAlgorithmSHA512:
|
|
settings = fmt.Sprintf("$6$rounds=%d$%s", iterations, salt)
|
|
default:
|
|
panic("invalid password hashing algorithm provided")
|
|
}
|
|
|
|
return settings
|
|
}
|
|
|
|
// validateSalt checks the salt input and settings are valid and returns it and a nil error if they are, otherwise returns an error.
|
|
func validateSalt(salt string, saltLength int) error {
|
|
if salt == "" {
|
|
if saltLength < 8 {
|
|
return fmt.Errorf("Salt length input of %d is invalid, it must be 8 or higher", saltLength)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
decodedSalt, err := crypt.Base64Encoding.DecodeString(salt)
|
|
if err != nil {
|
|
return fmt.Errorf("Salt input of %s is invalid, only base64 strings are valid for input", salt)
|
|
}
|
|
|
|
if len(decodedSalt) < 8 {
|
|
return fmt.Errorf("Salt input of %s is invalid (%d characters), it must be 8 or more characters", decodedSalt, len(decodedSalt))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateArgon2idSettings checks the argon2id settings are valid.
|
|
func validateArgon2idSettings(memory, parallelism, iterations, keyLength int) error {
|
|
// Caution: Increasing any of the values in the below block has a high chance in old passwords that cannot be verified.
|
|
if memory < 8 {
|
|
return fmt.Errorf("Memory (argon2id) input of %d is invalid, it must be 8 or higher", memory)
|
|
}
|
|
|
|
if parallelism < 1 {
|
|
return fmt.Errorf("Parallelism (argon2id) input of %d is invalid, it must be 1 or higher", parallelism)
|
|
}
|
|
|
|
if memory < parallelism*8 {
|
|
return fmt.Errorf("Memory (argon2id) input of %d is invalid with a parallelism input of %d, it must be %d (parallelism * 8) or higher", memory, parallelism, parallelism*8)
|
|
}
|
|
|
|
if keyLength < 16 {
|
|
return fmt.Errorf("Key length (argon2id) input of %d is invalid, it must be 16 or higher", keyLength)
|
|
}
|
|
|
|
if iterations < 1 {
|
|
return fmt.Errorf("Iterations (argon2id) input of %d is invalid, it must be 1 or more", iterations)
|
|
}
|
|
|
|
// Caution: Increasing any of the values in the above block has a high chance in old passwords that cannot be verified.
|
|
return nil
|
|
}
|