check and update the password hashing algorithm on user login
also add ldap md5 variant as per-user request Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
77f3400161
commit
5a40f998ae
10 changed files with 172 additions and 19 deletions
|
@ -653,6 +653,30 @@ func (p *BoltProvider) deleteUser(user *User) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (p *BoltProvider) updateUserPassword(username, password string) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var u []byte
|
||||
if u = bucket.Get([]byte(username)); u == nil {
|
||||
return util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
|
||||
}
|
||||
var user User
|
||||
err = json.Unmarshal(u, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = password
|
||||
buf, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(username), buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *BoltProvider) dumpUsers() ([]User, error) {
|
||||
users := make([]User, 0, 100)
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
|
@ -81,6 +82,7 @@ const (
|
|||
md5cryptPwdPrefix = "$1$"
|
||||
md5cryptApr1PwdPrefix = "$apr1$"
|
||||
sha512cryptPwdPrefix = "$6$"
|
||||
md5LDAPPwdPrefix = "{MD5}"
|
||||
trackQuotaDisabledError = "please enable track_quota in your configuration to use this method"
|
||||
operationAdd = "add"
|
||||
operationUpdate = "update"
|
||||
|
@ -144,7 +146,8 @@ var (
|
|||
sqlPlaceholders []string
|
||||
internalHashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix}
|
||||
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
|
||||
pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
|
||||
pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, md5LDAPPwdPrefix,
|
||||
sha512cryptPwdPrefix}
|
||||
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix}
|
||||
pbkdfPwdB64SaltPrefixes = []string{pbkdf2SHA256B64SaltPrefix}
|
||||
unixPwdPrefixes = []string{md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
|
||||
|
@ -624,6 +627,7 @@ type Provider interface {
|
|||
addUser(user *User) error
|
||||
updateUser(user *User) error
|
||||
deleteUser(user *User) error
|
||||
updateUserPassword(username, password string) error
|
||||
getUsers(limit int, offset int, order string) ([]User, error)
|
||||
dumpUsers() ([]User, error)
|
||||
getRecentlyUpdatedUsers(after int64) ([]User, error)
|
||||
|
@ -2166,6 +2170,21 @@ func validateBaseParams(user *User) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func hashPlainPassword(plainPwd string) (string, error) {
|
||||
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
||||
pwd, err := bcrypt.GenerateFromPassword([]byte(plainPwd), config.PasswordHashing.BcryptOptions.Cost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("bcrypt hashing error: %w", err)
|
||||
}
|
||||
return string(pwd), nil
|
||||
}
|
||||
pwd, err := argon2id.CreateHash(plainPwd, argon2Params)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("argon2ID hashing error: %w", err)
|
||||
}
|
||||
return pwd, nil
|
||||
}
|
||||
|
||||
func createUserPasswordHash(user *User) error {
|
||||
if user.Password != "" && !user.IsPasswordHashed() {
|
||||
if config.PasswordValidation.Users.MinEntropy > 0 {
|
||||
|
@ -2173,19 +2192,11 @@ func createUserPasswordHash(user *User) error {
|
|||
return util.NewValidationError(err.Error())
|
||||
}
|
||||
}
|
||||
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
||||
pwd, err := bcrypt.GenerateFromPassword([]byte(user.Password), config.PasswordHashing.BcryptOptions.Cost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = string(pwd)
|
||||
} else {
|
||||
pwd, err := argon2id.CreateHash(user.Password, argon2Params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = pwd
|
||||
hashedPwd, err := hashPlainPassword(user.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = hashedPwd
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -2271,18 +2282,21 @@ func isPasswordOK(user *User, password string) (bool, error) {
|
|||
}
|
||||
|
||||
match := false
|
||||
updatePwd := true
|
||||
var err error
|
||||
if strings.HasPrefix(user.Password, bcryptPwdPrefix) {
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return match, ErrInvalidCredentials
|
||||
}
|
||||
match = true
|
||||
updatePwd = config.PasswordHashing.Algo != HashingAlgoBcrypt
|
||||
} else if strings.HasPrefix(user.Password, argonPwdPrefix) {
|
||||
match, err = argon2id.ComparePasswordAndHash(password, user.Password)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error comparing password with argon hash: %v", err)
|
||||
return match, err
|
||||
}
|
||||
updatePwd = config.PasswordHashing.Algo != HashingAlgoArgon2ID
|
||||
} else if util.IsStringPrefixInSlice(user.Password, pbkdfPwdPrefixes) {
|
||||
match, err = comparePbkdf2PasswordAndHash(password, user.Password)
|
||||
if err != nil {
|
||||
|
@ -2293,13 +2307,32 @@ func isPasswordOK(user *User, password string) (bool, error) {
|
|||
if err != nil {
|
||||
return match, err
|
||||
}
|
||||
} else if strings.HasPrefix(user.Password, md5LDAPPwdPrefix) {
|
||||
h := md5.New()
|
||||
h.Write([]byte(password))
|
||||
match = fmt.Sprintf("%s%x", md5LDAPPwdPrefix, h.Sum(nil)) == user.Password
|
||||
}
|
||||
if err == nil && match {
|
||||
cachedPasswords.Add(user.Username, password)
|
||||
if updatePwd {
|
||||
convertUserPassword(user.Username, password)
|
||||
}
|
||||
}
|
||||
return match, err
|
||||
}
|
||||
|
||||
func convertUserPassword(username, plainPwd string) {
|
||||
hashedPwd, err := hashPlainPassword(plainPwd)
|
||||
if err == nil {
|
||||
err = provider.updateUserPassword(username, hashedPwd)
|
||||
}
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "unable to convert password for user %s: %v", username, err)
|
||||
} else {
|
||||
providerLog(logger.LevelDebug, "password converted for user %s", username)
|
||||
}
|
||||
}
|
||||
|
||||
func checkUserAndTLSCertificate(user *User, protocol string, tlsCert *x509.Certificate) (User, error) {
|
||||
err := user.CheckLoginConditions()
|
||||
if err != nil {
|
||||
|
|
|
@ -367,6 +367,22 @@ func (p *MemoryProvider) deleteUser(user *User) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) updateUserPassword(username, password string) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return errMemoryProviderClosed
|
||||
}
|
||||
|
||||
user, err := p.userExistsInternal(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = password
|
||||
p.dbHandle.users[username] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) dumpUsers() ([]User, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
|
|
|
@ -247,6 +247,10 @@ func (p *MySQLProvider) deleteUser(user *User) error {
|
|||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) updateUserPassword(username, password string) error {
|
||||
return sqlCommonUpdateUserPassword(username, password, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) dumpUsers() ([]User, error) {
|
||||
return sqlCommonDumpUsers(p.dbHandle)
|
||||
}
|
||||
|
|
|
@ -223,6 +223,10 @@ func (p *PGSQLProvider) deleteUser(user *User) error {
|
|||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) updateUserPassword(username, password string) error {
|
||||
return sqlCommonUpdateUserPassword(username, password, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) dumpUsers() ([]User, error) {
|
||||
return sqlCommonDumpUsers(p.dbHandle)
|
||||
}
|
||||
|
|
|
@ -838,6 +838,21 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
|
|||
})
|
||||
}
|
||||
|
||||
func sqlCommonUpdateUserPassword(username, password string, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getUpdateUserPasswordQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.ExecContext(ctx, password, username)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonUpdateUser(user *User, dbHandle *sql.DB) error {
|
||||
err := ValidateUser(user)
|
||||
if err != nil {
|
||||
|
|
|
@ -197,6 +197,10 @@ func (p *SQLiteProvider) deleteUser(user *User) error {
|
|||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) updateUserPassword(username, password string) error {
|
||||
return sqlCommonUpdateUserPassword(username, password, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) dumpUsers() ([]User, error) {
|
||||
return sqlCommonDumpUsers(p.dbHandle)
|
||||
}
|
||||
|
|
|
@ -365,6 +365,10 @@ func getUpdateUserQuery() string {
|
|||
sqlPlaceholders[20], sqlPlaceholders[21], sqlPlaceholders[22])
|
||||
}
|
||||
|
||||
func getUpdateUserPasswordQuery() string {
|
||||
return fmt.Sprintf(`UPDATE %v SET password=%v WHERE username = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
||||
func getDeleteUserQuery() string {
|
||||
return fmt.Sprintf(`DELETE FROM %v WHERE id = %v`, sqlTableUsers, sqlPlaceholders[0])
|
||||
}
|
||||
|
|
|
@ -405,7 +405,7 @@ You can select `sha256-simd` setting the environment variable `SFTPGO_MINIO_SHA2
|
|||
|
||||
</details>
|
||||
|
||||
<details><summary><font size=5> Binding to privileged ports</font></summary>
|
||||
<details><summary><font size=5>Binding to privileged ports</font></summary>
|
||||
|
||||
On Linux, if you want to use Internet domain privileged ports (port numbers less than 1024) instead of running the SFTPGo service as root user you can set the `cap_net_bind_service` capability on the `sftpgo` binary. To set the capability you can use the following command:
|
||||
|
||||
|
@ -429,6 +429,27 @@ sudo iptables -t nat -A OUTPUT -d <ip> -p tcp --dport 22 -m addrtype --dst-t
|
|||
|
||||
</details>
|
||||
|
||||
<details><summary><font size=5>Supported Password Hashing Algorithms</font></summary>
|
||||
|
||||
SFTPGo can verify passwords in several formats and uses, by default, the `bcrypt` algorithm to hash passwords in plain-text before storing them inside the data provider. Each hashing algorithm is identified by a prefix.
|
||||
Supported hash algorithms:
|
||||
|
||||
- bcrypt, prefix `$2a$`
|
||||
- argon2id, prefix `$argon2id$`
|
||||
- PBKDF2 sha1, prefix `$pbkdf2-sha1$`
|
||||
- PBKDF2 sha256, prefix `$pbkdf2-sha256$`
|
||||
- PBKDF2 sha512, prefix `$pbkdf2-sha512$`
|
||||
- PBKDF2 sha256 with base64 salt, prefix `$pbkdf2-b64salt-sha256$`
|
||||
- MD5 crypt, prefix `$1$`
|
||||
- MD5 crypt APR1, prefix `$apr1$`
|
||||
- SHA512 crypt, prefix `$6$`
|
||||
- LDAP md5, prefix `{MD5}`
|
||||
|
||||
If you set a password with one of these prefixes it will not be encrypted.
|
||||
When users log in, if their passwords are stored with anything other than the preferred algorithm, SFTPGo will automatically upgrade the algorithm to the preferred one.
|
||||
|
||||
</details>
|
||||
|
||||
## Telemetry Server
|
||||
|
||||
The telemetry server exposes the following endpoints:
|
||||
|
|
|
@ -6919,20 +6919,29 @@ func TestOverwriteDirWithFile(t *testing.T) {
|
|||
|
||||
func TestHashedPasswords(t *testing.T) {
|
||||
usePubKey := false
|
||||
plainPwd := "password"
|
||||
pwdMapping := make(map[string]string)
|
||||
pwdMapping["$pbkdf2-sha1$150000$DveVjgYUD05R$X6ydQZdyMeOvpgND2nqGR/0GGic="] = "password" //nolint:goconst
|
||||
pwdMapping["$pbkdf2-sha256$150000$E86a9YMX3zC7$R5J62hsSq+pYw00hLLPKBbcGXmq7fj5+/M0IFoYtZbo="] = "password"
|
||||
pwdMapping["$pbkdf2-sha512$150000$dsu7T5R3IaVQ$1hFXPO1ntRBcoWkSLKw+s4sAP09Xtu4Ya7CyxFq64jM9zdUg8eRJVr3NcR2vQgb0W9HHvZaILHsL4Q/Vr6arCg=="] = "password"
|
||||
pwdMapping["$1$b5caebda$VODr/nyhGWgZaY8sJ4x05."] = "password"
|
||||
pwdMapping["$argon2id$v=19$m=65536,t=3,p=2$xtcO/oRkC8O2Tn+mryl2mw$O7bn24f2kuSGRMi9s5Cm61Wqd810px1jDsAasrGWkzQ"] = plainPwd
|
||||
pwdMapping["$pbkdf2-sha1$150000$DveVjgYUD05R$X6ydQZdyMeOvpgND2nqGR/0GGic="] = plainPwd
|
||||
pwdMapping["$pbkdf2-sha256$150000$E86a9YMX3zC7$R5J62hsSq+pYw00hLLPKBbcGXmq7fj5+/M0IFoYtZbo="] = plainPwd
|
||||
pwdMapping["$pbkdf2-sha512$150000$dsu7T5R3IaVQ$1hFXPO1ntRBcoWkSLKw+s4sAP09Xtu4Ya7CyxFq64jM9zdUg8eRJVr3NcR2vQgb0W9HHvZaILHsL4Q/Vr6arCg=="] = plainPwd
|
||||
pwdMapping["$1$b5caebda$VODr/nyhGWgZaY8sJ4x05."] = plainPwd
|
||||
pwdMapping["$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK"] = "secret"
|
||||
pwdMapping["$6$459ead56b72e44bc$uog86fUxscjt28BZxqFBE2pp2QD8P/1e98MNF75Z9xJfQvOckZnQ/1YJqiq1XeytPuDieHZvDAMoP7352ELkO1"] = "secret"
|
||||
pwdMapping["$apr1$OBWLeSme$WoJbB736e7kKxMBIAqilb1"] = "password"
|
||||
pwdMapping["$apr1$OBWLeSme$WoJbB736e7kKxMBIAqilb1"] = plainPwd
|
||||
pwdMapping["{MD5}5f4dcc3b5aa765d61d8327deb882cf99"] = plainPwd
|
||||
|
||||
for pwd, clearPwd := range pwdMapping {
|
||||
u := getTestUser(usePubKey)
|
||||
u.Password = pwd
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
user.Password = ""
|
||||
userGetInitial, _, err := httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
user, err = dataprovider.UserExists(user.Username)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, pwd, user.Password)
|
||||
user.Password = clearPwd
|
||||
conn, client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err, "unable to login with password %#v", pwd) {
|
||||
|
@ -6946,6 +6955,25 @@ func TestHashedPasswords(t *testing.T) {
|
|||
client.Close()
|
||||
conn.Close()
|
||||
}
|
||||
// the password must converted to bcrypt and we should still be able to login
|
||||
user, err = dataprovider.UserExists(user.Username)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(user.Password, "$2a$"))
|
||||
// update the user to invalidate the cached password and force a new check
|
||||
user.Password = ""
|
||||
userGet, _, err := httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
userGetInitial.LastLogin = userGet.LastLogin
|
||||
userGetInitial.UpdatedAt = userGet.UpdatedAt
|
||||
assert.Equal(t, userGetInitial, userGet)
|
||||
// login should still work
|
||||
user.Password = clearPwd
|
||||
conn, client, err = getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err, "unable to login with password %#v", pwd) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
assert.NoError(t, checkBasicSFTP(client))
|
||||
}
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
|
|
Loading…
Reference in a new issue