2019-04-24 21:52:08 +00:00
package authentication
import (
"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
"github.com/authelia/authelia/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-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-03-06 01:38:02 +00:00
_ , err = crypt . Base64Encoding . DecodeString ( h . Salt )
2019-04-24 21:52:08 +00:00
if err != nil {
2020-04-09 01:05:17 +00:00
return nil , errors . New ( "Salt contains invalid base64 characters" )
2019-04-24 21:52:08 +00:00
}
2020-03-06 01:38:02 +00:00
if code == HashingAlgorithmSHA512 {
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
}
} else if code == HashingAlgorithmArgon2id {
version := parameters . GetInt ( "v" , 0 )
if version < 19 {
if version == 0 {
return nil , fmt . Errorf ( "Argon2id version parameter not found (%s)" , hash )
}
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
}
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 )
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
}
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
}
} else {
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.
//nolint:gocyclo // TODO: Consider refactoring/simplifying, time permitting.
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
}
2019-11-01 18:31:51 +00:00
if salt == "" {
2020-03-06 01:38:02 +00:00
if saltLength < 2 {
2020-04-09 01:05:17 +00:00
return "" , fmt . Errorf ( "Salt length input of %d is invalid, it must be 2 or higher" , saltLength )
2020-03-06 01:38:02 +00:00
} else if saltLength > 16 {
2020-04-09 01:05:17 +00:00
return "" , fmt . Errorf ( "Salt length input of %d is invalid, it must be 16 or lower" , saltLength )
2020-03-06 01:38:02 +00:00
}
} else if len ( salt ) > 16 {
2020-04-09 01:05:17 +00:00
return "" , fmt . Errorf ( "Salt input of %s is invalid (%d characters), it must be 16 or fewer characters" , salt , len ( salt ) )
2020-03-06 01:38:02 +00:00
} else if len ( salt ) < 2 {
2020-04-09 01:05:17 +00:00
return "" , fmt . Errorf ( "Salt input of %s is invalid (%d characters), it must be 2 or more characters" , salt , len ( salt ) )
2020-03-06 01:38:02 +00:00
} else if _ , err = crypt . Base64Encoding . DecodeString ( salt ) ; err != nil {
2020-04-09 01:05:17 +00:00
return "" , fmt . Errorf ( "Salt input of %s is invalid, only characters [a-zA-Z0-9+/] are valid for input" , salt )
2019-11-01 18:31:51 +00:00
}
2020-03-06 01:38:02 +00:00
if algorithm == HashingAlgorithmArgon2id {
2020-05-02 05:06:39 +00:00
// Caution: Increasing any of the values in the below block has a high chance in old passwords that cannot be verified.
2020-03-06 01:38:02 +00:00
if memory < 8 {
2020-04-09 01:05:17 +00:00
return "" , fmt . Errorf ( "Memory (argon2id) input of %d is invalid, it must be 8 or higher" , memory )
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 parallelism < 1 {
2020-04-09 01:05:17 +00:00
return "" , fmt . Errorf ( "Parallelism (argon2id) input of %d is invalid, it must be 1 or higher" , parallelism )
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 memory < parallelism * 8 {
2020-04-09 01:05:17 +00:00
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 )
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 keyLength < 16 {
2020-04-09 01:05:17 +00:00
return "" , fmt . Errorf ( "Key length (argon2id) input of %d is invalid, it must be 16 or higher" , keyLength )
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 iterations < 1 {
2020-04-09 01:05:17 +00:00
return "" , fmt . Errorf ( "Iterations (argon2id) input of %d is invalid, it must be 1 or more" , iterations )
2020-03-06 01:38:02 +00:00
}
}
if salt == "" {
salt = utils . RandomString ( saltLength , HashingPossibleSaltCharacters )
}
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 ) {
2019-12-27 16:55:00 +00:00
passwordHash , 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-03-06 01:38:02 +00:00
expectedHash , err := HashPassword ( password , passwordHash . Salt , passwordHash . Algorithm , passwordHash . Iterations , passwordHash . Memory , passwordHash . Parallelism , passwordHash . KeyLength , len ( passwordHash . Salt ) )
if err != nil {
return false , err
}
2020-05-05 19:35:32 +00:00
2020-03-06 01:38:02 +00:00
return hash == expectedHash , 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-01 22:32:09 +00:00
if algorithm == HashingAlgorithmArgon2id {
settings , _ = crypt . Argon2idSettings ( memory , iterations , parallelism , keyLength , salt )
} else if algorithm == HashingAlgorithmSHA512 {
settings = fmt . Sprintf ( "$6$rounds=%d$%s" , iterations , salt )
} else {
panic ( "invalid password hashing algorithm provided" )
}
2020-05-05 19:35:32 +00:00
2020-05-01 22:32:09 +00:00
return settings
}