2019-04-24 21:52:08 +00:00
package authentication
import (
2021-03-11 01:08:49 +00:00
"crypto/subtle"
2019-04-24 21:52:08 +00:00
"errors"
"fmt"
"strconv"
"strings"
2019-11-01 18:31:51 +00:00
"github.com/simia-tech/crypt"
2020-04-05 12:37:21 +00:00
2021-08-11 01:04:35 +00:00
"github.com/authelia/authelia/v4/internal/utils"
2019-11-01 18:31:51 +00:00
)
2019-04-24 21:52:08 +00:00
// PasswordHash represents all characteristics of a password hash.
2020-05-02 05:06:39 +00:00
// Authelia only supports salted SHA512 or salted argon2id method, i.e., $6$ mode or $argon2id$ mode.
2019-04-24 21:52:08 +00:00
type PasswordHash struct {
2020-05-03 04:06:09 +00:00
Algorithm CryptAlgo
2020-03-06 01:38:02 +00:00
Iterations int
Salt string
Key string
KeyLength int
Memory int
Parallelism int
2019-04-24 21:52:08 +00:00
}
2020-05-08 03:38:22 +00:00
// 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`" )
}
}
2020-05-02 05:06:39 +00:00
// ParseHash extracts all characteristics of a hash given its string representation.
2020-03-06 01:38:02 +00:00
func ParseHash ( hash string ) ( passwordHash * PasswordHash , err error ) {
2019-04-24 21:52:08 +00:00
parts := strings . Split ( hash , "$" )
2020-05-02 05:06:39 +00:00
// This error can be ignored as it's always nil.
2020-05-03 04:06:09 +00:00
c , parameters , salt , key , _ := crypt . DecodeSettings ( hash )
code := CryptAlgo ( c )
2020-03-06 01:38:02 +00:00
h := & PasswordHash { }
2019-12-27 16:55:00 +00:00
2020-03-06 01:38:02 +00:00
h . Salt = salt
h . Key = key
2019-04-24 21:52:08 +00:00
2020-03-06 01:38:02 +00:00
if h . Key != parts [ len ( parts ) - 1 ] {
2020-04-09 01:05:17 +00:00
return nil , fmt . Errorf ( "Hash key is not the last parameter, the hash is likely malformed (%s)" , hash )
2020-03-06 01:38:02 +00:00
}
2020-05-05 19:35:32 +00:00
2020-03-06 01:38:02 +00:00
if h . Key == "" {
return nil , fmt . Errorf ( "Hash key contains no characters or the field length is invalid (%s)" , hash )
2019-04-24 21:52:08 +00:00
}
2020-05-06 00:52:06 +00:00
switch code {
case HashingAlgorithmSHA512 :
2020-03-06 01:38:02 +00:00
h . Iterations = parameters . GetInt ( "rounds" , HashingDefaultSHA512Iterations )
h . Algorithm = HashingAlgorithmSHA512
2020-05-05 19:35:32 +00:00
2020-03-06 01:38:02 +00:00
if parameters [ "rounds" ] != "" && parameters [ "rounds" ] != strconv . Itoa ( h . Iterations ) {
2020-04-09 01:05:17 +00:00
return nil , fmt . Errorf ( "SHA512 iterations is not numeric (%s)" , parameters [ "rounds" ] )
2020-03-06 01:38:02 +00:00
}
2020-05-06 00:52:06 +00:00
case HashingAlgorithmArgon2id :
2021-11-05 03:49:45 +00:00
_ , err = crypt . Base64Encoding . DecodeString ( h . Salt )
if err != nil {
return nil , errors . New ( "Salt contains invalid base64 characters" )
}
2020-03-06 01:38:02 +00:00
version := parameters . GetInt ( "v" , 0 )
if version < 19 {
if version == 0 {
return nil , fmt . Errorf ( "Argon2id version parameter not found (%s)" , hash )
}
2020-05-06 00:52:06 +00:00
2020-04-09 01:05:17 +00:00
return nil , fmt . Errorf ( "Argon2id versions less than v19 are not supported (hash is version %d)" , version )
2020-03-06 01:38:02 +00:00
} else if version > 19 {
2020-04-09 01:05:17 +00:00
return nil , fmt . Errorf ( "Argon2id versions greater than v19 are not supported (hash is version %d)" , version )
2020-03-06 01:38:02 +00:00
}
2020-05-06 00:52:06 +00:00
2020-03-06 01:38:02 +00:00
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 )
2019-04-24 21:52:08 +00:00
2020-03-06 01:38:02 +00:00
decodedKey , err := crypt . Base64Encoding . DecodeString ( h . Key )
2020-05-06 00:52:06 +00:00
2020-03-06 01:38:02 +00:00
if err != nil {
2020-04-09 01:05:17 +00:00
return nil , errors . New ( "Hash key contains invalid base64 characters" )
2020-03-06 01:38:02 +00:00
}
2020-05-06 00:52:06 +00:00
2020-03-06 01:38:02 +00:00
if len ( decodedKey ) != h . KeyLength {
2020-04-09 01:05:17 +00:00
return nil , fmt . Errorf ( "Argon2id key length parameter (%d) does not match the actual key length (%d)" , h . KeyLength , len ( decodedKey ) )
2020-03-06 01:38:02 +00:00
}
2020-05-06 00:52:06 +00:00
default :
2020-03-06 01:38:02 +00:00
return nil , fmt . Errorf ( "Authelia only supports salted SHA512 hashing ($6$) and salted argon2id ($argon2id$), not $%s$" , code )
2019-04-24 21:52:08 +00:00
}
2020-05-05 19:35:32 +00:00
2020-03-06 01:38:02 +00:00
return h , nil
2019-04-24 21:52:08 +00:00
}
2020-05-02 05:06:39 +00:00
// HashPassword generate a salt and hash the password with the salt and a constant number of rounds.
2020-05-03 04:06:09 +00:00
func HashPassword ( password , salt string , algorithm CryptAlgo , iterations , memory , parallelism , keyLength , saltLength int ) ( hash string , err error ) {
2020-03-06 01:38:02 +00:00
var settings string
if algorithm != HashingAlgorithmArgon2id && algorithm != HashingAlgorithmSHA512 {
2020-04-09 01:05:17 +00:00
return "" , fmt . Errorf ( "Hashing algorithm input of '%s' is invalid, only values of %s and %s are supported" , algorithm , HashingAlgorithmArgon2id , HashingAlgorithmSHA512 )
2020-03-06 01:38:02 +00:00
}
if algorithm == HashingAlgorithmArgon2id {
2020-05-08 03:38:22 +00:00
err := validateArgon2idSettings ( memory , parallelism , iterations , keyLength )
if err != nil {
return "" , err
2020-03-06 01:38:02 +00:00
}
2020-05-08 03:38:22 +00:00
}
2020-05-05 19:35:32 +00:00
2021-11-05 03:49:45 +00:00
if algorithm != HashingAlgorithmSHA512 {
err = validateSalt ( salt , saltLength )
if err != nil {
return "" , err
}
2020-03-06 01:38:02 +00:00
}
if salt == "" {
2020-05-14 05:55:03 +00:00
salt = crypt . Base64Encoding . EncodeToString ( [ ] byte ( utils . RandomString ( saltLength , HashingPossibleSaltCharacters ) ) )
2020-03-06 01:38:02 +00:00
}
2020-05-05 19:35:32 +00:00
2020-05-01 22:32:09 +00:00
settings = getCryptSettings ( salt , algorithm , iterations , memory , parallelism , keyLength )
2020-03-06 01:38:02 +00:00
2020-05-02 05:06:39 +00:00
// This error can be ignored because we check for it before a user gets here.
2020-03-06 01:38:02 +00:00
hash , _ = crypt . Crypt ( password , settings )
2020-05-05 19:35:32 +00:00
2020-03-06 01:38:02 +00:00
return hash , nil
2019-04-24 21:52:08 +00:00
}
2020-05-02 05:06:39 +00:00
// CheckPassword check a password against a hash.
2020-03-06 01:38:02 +00:00
func CheckPassword ( password , hash string ) ( ok bool , err error ) {
2020-05-14 05:55:03 +00:00
expectedHash , err := ParseHash ( hash )
2019-04-24 21:52:08 +00:00
if err != nil {
return false , err
}
2020-05-05 19:35:32 +00:00
2020-05-14 05:55:03 +00:00
passwordHashString , err := HashPassword ( password , expectedHash . Salt , expectedHash . Algorithm , expectedHash . Iterations , expectedHash . Memory , expectedHash . Parallelism , expectedHash . KeyLength , len ( expectedHash . Salt ) )
2020-03-06 01:38:02 +00:00
if err != nil {
return false , err
}
2020-05-05 19:35:32 +00:00
2020-05-14 05:55:03 +00:00
passwordHash , err := ParseHash ( passwordHashString )
if err != nil {
return false , err
}
2021-03-11 01:08:49 +00:00
return subtle . ConstantTimeCompare ( [ ] byte ( passwordHash . Key ) , [ ] byte ( expectedHash . Key ) ) == 1 , nil
2019-04-24 21:52:08 +00:00
}
2020-05-01 22:32:09 +00:00
2020-05-03 04:06:09 +00:00
func getCryptSettings ( salt string , algorithm CryptAlgo , iterations , memory , parallelism , keyLength int ) ( settings string ) {
2020-05-06 00:52:06 +00:00
switch algorithm {
case HashingAlgorithmArgon2id :
2020-05-01 22:32:09 +00:00
settings , _ = crypt . Argon2idSettings ( memory , iterations , parallelism , keyLength , salt )
2020-05-06 00:52:06 +00:00
case HashingAlgorithmSHA512 :
2020-05-01 22:32:09 +00:00
settings = fmt . Sprintf ( "$6$rounds=%d$%s" , iterations , salt )
2020-05-06 00:52:06 +00:00
default :
2020-05-01 22:32:09 +00:00
panic ( "invalid password hashing algorithm provided" )
}
2020-05-05 19:35:32 +00:00
2020-05-01 22:32:09 +00:00
return settings
}
2020-05-08 03:38:22 +00:00
// 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 == "" {
2020-05-14 05:55:03 +00:00
if saltLength < 8 {
return fmt . Errorf ( "Salt length input of %d is invalid, it must be 8 or higher" , saltLength )
2020-05-08 03:38:22 +00:00
}
2020-05-14 05:55:03 +00:00
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 ) )
2020-05-08 03:38:22 +00:00
}
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
}