add support for checking pbkdf2 passwords with base64 encoded salt
This way we can import the default passwords format used in 389ds. See TestPasswordsHashPbkdf2Sha256_389DS test case to learn how to convert 389ds passwords
This commit is contained in:
parent
9b06e0a3b7
commit
37357b2d63
3 changed files with 91 additions and 34 deletions
|
@ -57,19 +57,20 @@ const (
|
|||
// MemoryDataProviderName name for memory provider
|
||||
MemoryDataProviderName = "memory"
|
||||
|
||||
argonPwdPrefix = "$argon2id$"
|
||||
bcryptPwdPrefix = "$2a$"
|
||||
pbkdf2SHA1Prefix = "$pbkdf2-sha1$"
|
||||
pbkdf2SHA256Prefix = "$pbkdf2-sha256$"
|
||||
pbkdf2SHA512Prefix = "$pbkdf2-sha512$"
|
||||
md5cryptPwdPrefix = "$1$"
|
||||
md5cryptApr1PwdPrefix = "$apr1$"
|
||||
sha512cryptPwdPrefix = "$6$"
|
||||
manageUsersDisabledError = "please set manage_users to 1 in your configuration to enable this method"
|
||||
trackQuotaDisabledError = "please enable track_quota in your configuration to use this method"
|
||||
operationAdd = "add"
|
||||
operationUpdate = "update"
|
||||
operationDelete = "delete"
|
||||
argonPwdPrefix = "$argon2id$"
|
||||
bcryptPwdPrefix = "$2a$"
|
||||
pbkdf2SHA1Prefix = "$pbkdf2-sha1$"
|
||||
pbkdf2SHA256Prefix = "$pbkdf2-sha256$"
|
||||
pbkdf2SHA512Prefix = "$pbkdf2-sha512$"
|
||||
pbkdf2SHA256B64SaltPrefix = "$pbkdf2-b64salt-sha256$"
|
||||
md5cryptPwdPrefix = "$1$"
|
||||
md5cryptApr1PwdPrefix = "$apr1$"
|
||||
sha512cryptPwdPrefix = "$6$"
|
||||
manageUsersDisabledError = "please set manage_users to 1 in your configuration to enable this method"
|
||||
trackQuotaDisabledError = "please enable track_quota in your configuration to use this method"
|
||||
operationAdd = "add"
|
||||
operationUpdate = "update"
|
||||
operationDelete = "delete"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -88,15 +89,16 @@ var (
|
|||
provider Provider
|
||||
sqlPlaceholders []string
|
||||
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
|
||||
pbkdf2SHA512Prefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
|
||||
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
|
||||
unixPwdPrefixes = []string{md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
|
||||
logSender = "dataProvider"
|
||||
availabilityTicker *time.Ticker
|
||||
availabilityTickerDone chan bool
|
||||
errWrongPassword = errors.New("password does not match")
|
||||
errNoInitRequired = errors.New("initialization is not required for this data provider")
|
||||
credentialsDirPath string
|
||||
pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
|
||||
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix}
|
||||
pbkdfPwdB64SaltPrefixes = []string{pbkdf2SHA256B64SaltPrefix}
|
||||
unixPwdPrefixes = []string{md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
|
||||
logSender = "dataProvider"
|
||||
availabilityTicker *time.Ticker
|
||||
availabilityTickerDone chan bool
|
||||
errWrongPassword = errors.New("password does not match")
|
||||
errNoInitRequired = errors.New("initialization is not required for this data provider")
|
||||
credentialsDirPath string
|
||||
)
|
||||
|
||||
type schemaVersion struct {
|
||||
|
@ -995,8 +997,25 @@ func comparePbkdf2PasswordAndHash(password, hashedPassword string) (bool, error)
|
|||
if len(vals) != 5 {
|
||||
return false, fmt.Errorf("pbkdf2: hash is not in the correct format")
|
||||
}
|
||||
iterations, err := strconv.Atoi(vals[2])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
expected, err := base64.StdEncoding.DecodeString(vals[4])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var salt []byte
|
||||
if utils.IsStringPrefixInSlice(hashedPassword, pbkdfPwdB64SaltPrefixes) {
|
||||
salt, err = base64.StdEncoding.DecodeString(vals[3])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
salt = []byte(vals[3])
|
||||
}
|
||||
var hashFunc func() hash.Hash
|
||||
if strings.HasPrefix(hashedPassword, pbkdf2SHA256Prefix) {
|
||||
if strings.HasPrefix(hashedPassword, pbkdf2SHA256Prefix) || strings.HasPrefix(hashedPassword, pbkdf2SHA256B64SaltPrefix) {
|
||||
hashFunc = sha256.New
|
||||
} else if strings.HasPrefix(hashedPassword, pbkdf2SHA512Prefix) {
|
||||
hashFunc = sha512.New
|
||||
|
@ -1005,16 +1024,7 @@ func comparePbkdf2PasswordAndHash(password, hashedPassword string) (bool, error)
|
|||
} else {
|
||||
return false, fmt.Errorf("pbkdf2: invalid or unsupported hash format %v", vals[1])
|
||||
}
|
||||
iterations, err := strconv.Atoi(vals[2])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
salt := vals[3]
|
||||
expected, err := base64.StdEncoding.DecodeString(vals[4])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
df := pbkdf2.Key([]byte(password), []byte(salt), iterations, len(expected), hashFunc)
|
||||
df := pbkdf2.Key([]byte(password), salt, iterations, len(expected), hashFunc)
|
||||
return subtle.ConstantTimeCompare(df, expected) == 1, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
For each account, the following properties can be configured:
|
||||
|
||||
- `username`
|
||||
- `password` used for password authentication. For users created using SFTPGo REST API, if the password has no known hashing algo prefix, it will be stored using argon2id. SFTPGo supports checking passwords stored with bcrypt, pbkdf2, md5crypt and sha512crypt too. For pbkdf2 the supported format is `$<algo>$<iterations>$<salt>$<hashed pwd base64 encoded>`, where algo is `pbkdf2-sha1` or `pbkdf2-sha256` or `pbkdf2-sha512`. For example the `pbkdf2-sha256` of the word `password` using 150000 iterations and `E86a9YMX3zC7` as salt must be stored as `$pbkdf2-sha256$150000$E86a9YMX3zC7$R5J62hsSq+pYw00hLLPKBbcGXmq7fj5+/M0IFoYtZbo=`. For bcrypt the format must be the one supported by golang's [crypto/bcrypt](https://godoc.org/golang.org/x/crypto/bcrypt) package, for example the password `secret` with cost `14` must be stored as `$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK`. For md5crypt and sha512crypt we support the format used in `/etc/shadow` with the `$1$` and `$6$` prefix, this is useful if you are migrating from Unix system user accounts. We support Apache md5crypt (`$apr1$` prefix) too. Using the REST API you can send a password hashed as bcrypt, pbkdf2, md5crypt or sha512crypt and it will be stored as is.
|
||||
- `password` used for password authentication. For users created using SFTPGo REST API, if the password has no known hashing algo prefix, it will be stored using argon2id. SFTPGo supports checking passwords stored with bcrypt, pbkdf2, md5crypt and sha512crypt too. For pbkdf2 the supported format is `$<algo>$<iterations>$<salt>$<hashed pwd base64 encoded>`, where algo is `pbkdf2-sha1` or `pbkdf2-sha256` or `pbkdf2-sha512` or `$pbkdf2-b64salt-sha256$`. For example the `pbkdf2-sha256` of the word `password` using 150000 iterations and `E86a9YMX3zC7` as salt must be stored as `$pbkdf2-sha256$150000$E86a9YMX3zC7$R5J62hsSq+pYw00hLLPKBbcGXmq7fj5+/M0IFoYtZbo=`. In pbkdf2 variant with `b64salt` the salt is base64 encoded. For bcrypt the format must be the one supported by golang's [crypto/bcrypt](https://godoc.org/golang.org/x/crypto/bcrypt) package, for example the password `secret` with cost `14` must be stored as `$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK`. For md5crypt and sha512crypt we support the format used in `/etc/shadow` with the `$1$` and `$6$` prefix, this is useful if you are migrating from Unix system user accounts. We support Apache md5crypt (`$apr1$` prefix) too. Using the REST API you can send a password hashed as bcrypt, pbkdf2, md5crypt or sha512crypt and it will be stored as is.
|
||||
- `public_keys` array of public keys. At least one public key or the password is mandatory.
|
||||
- `status` 1 means "active", 0 "inactive". An inactive account cannot login.
|
||||
- `expiration_date` expiration date as unix timestamp in milliseconds. An expired account cannot login. 0 means no expiration.
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash"
|
||||
|
@ -2749,6 +2751,51 @@ func TestPasswordsHashPbkdf2Sha512(t *testing.T) {
|
|||
os.RemoveAll(user.GetHomeDir())
|
||||
}
|
||||
|
||||
func TestPasswordsHashPbkdf2Sha256_389DS(t *testing.T) {
|
||||
pbkdf389dsPwd := "{PBKDF2_SHA256}AAAIAMZIKG4ie44zJY4HOXI+upFR74PzWLUQV63jg+zzkbEjCK3N4qW583WF7EdcpeoOMQ4HY3aWEXB6lnXhXJixbJkU4vVSJkL6YCbU3TrD0qn1uUUVSkaIgAOtmZENitwbhYhiWfEzGyAtFqkFd75P5xhWJEog9XhQKYrR0f7S3WGGZq03JRcLJ460xpU97bE/sWRn7sshgkWzLuyrs0I+XRKmK7FJeaA9zd+1m44Y3IVmZ2YLdKATzjRHAIgpBC6i1TWOcpKJT1+feP1C9hrxH8vU9baw9thNiO8jSHaZlwb//KpJFe0ahVnG/1ubiG8cO0+CCqDqXVJR6Vr4QZxHP+4pwooW+4TP/L+HFdyA1y6z4gKfqYnBsmb3sD1R1TbxfH4btTdvgZAnBk9CmR3QASkFXxeTYsrmNd5+9IAHc6dm"
|
||||
pbkdf389dsPwd = pbkdf389dsPwd[15:]
|
||||
hashBytes, err := base64.StdEncoding.DecodeString(pbkdf389dsPwd)
|
||||
if err != nil {
|
||||
t.Errorf("unable to decode 389ds password: %v", err)
|
||||
}
|
||||
iterBytes := hashBytes[0:4]
|
||||
var iterations int32
|
||||
binary.Read(bytes.NewBuffer(iterBytes), binary.BigEndian, &iterations)
|
||||
salt := hashBytes[4:68]
|
||||
targetKey := hashBytes[68:]
|
||||
key := base64.StdEncoding.EncodeToString(targetKey)
|
||||
pbkdf2Pwd := fmt.Sprintf("$pbkdf2-b64salt-sha256$%v$%v$%v", iterations, base64.StdEncoding.EncodeToString(salt), key)
|
||||
pbkdf2ClearPwd := "password"
|
||||
usePubKey := false
|
||||
u := getTestUser(usePubKey)
|
||||
u.Password = pbkdf2Pwd
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
user.Password = pbkdf2ClearPwd
|
||||
client, err := getSftpClient(user, usePubKey)
|
||||
if err != nil {
|
||||
t.Errorf("unable to login with pkkdf2 sha256 password: %v", err)
|
||||
} else {
|
||||
defer client.Close()
|
||||
_, err = client.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("unable to get working dir with pkkdf2 sha256 password: %v", err)
|
||||
}
|
||||
}
|
||||
user.Password = pbkdf2Pwd
|
||||
_, err = getSftpClient(user, usePubKey)
|
||||
if err == nil {
|
||||
t.Errorf("login with wrong password must fail")
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
}
|
||||
os.RemoveAll(user.GetHomeDir())
|
||||
}
|
||||
|
||||
func TestPasswordsHashBcrypt(t *testing.T) {
|
||||
bcryptPwd := "$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK"
|
||||
bcryptClearPwd := "secret"
|
||||
|
|
Loading…
Add table
Reference in a new issue