add support for checking pbkdf2 passwords

This commit is contained in:
Nicola Murino 2019-08-17 15:20:49 +02:00
parent 9d342cb125
commit 133f2e8601
4 changed files with 70 additions and 6 deletions

View file

@ -219,7 +219,7 @@ sftpgo serve
For each account the following properties can be configured:
- `username`
- `password` used for password authentication. For users created using SFTPGo REST API the password will be stored using argon2id hashing algo. SFTPGo supports checking passwords stored with bcrypt too.
- `password` used for password authentication. For users created using SFTPGo REST API the password will be stored using argon2id hashing algo. SFTPGo supports checking passwords stored with bcrypt and pbkdf2 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`
- `public_keys` array of public keys. At least one public key or the password is mandatory.
- `home_dir` The user cannot upload or download files outside this directory. Must be an absolute path
- `uid`, `gid`. If sftpgo runs as root system user then the created files and directories will be assigned to this system uid/gid. Ignored on windows and if sftpgo runs as non root user: in this case files and directories for all SFTP users will be owned by the system user that runs sftpgo.

View file

@ -4,13 +4,21 @@
package dataprovider
import (
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"hash"
"path/filepath"
"strconv"
"strings"
"github.com/alexedwards/argon2id"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/ssh"
"github.com/drakkan/sftpgo/logger"
@ -30,6 +38,9 @@ const (
logSender = "dataProvider"
argonPwdPrefix = "$argon2id$"
bcryptPwdPrefix = "$2a$"
pbkdf2SHA1Prefix = "$pbkdf2-sha1$"
pbkdf2SHA256Prefix = "$pbkdf2-sha256$"
pbkdf2SHA512Prefix = "$pbkdf2-sha512$"
manageUsersDisabledError = "please set manage_users to 1 in sftpgo.conf to enable this method"
trackQuotaDisabledError = "please enable track_quota in sftpgo.conf to use this method"
)
@ -42,6 +53,8 @@ var (
sqlPlaceholders []string
validPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermDelete, PermRename,
PermCreateDirs, PermCreateSymlinks}
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
)
// Config provider configuration
@ -237,8 +250,7 @@ func validateUser(user *User) error {
return &ValidationError{err: fmt.Sprintf("Invalid permission: %v", p)}
}
}
if len(user.Password) > 0 && !strings.HasPrefix(user.Password, argonPwdPrefix) &&
!strings.HasPrefix(user.Password, bcryptPwdPrefix) {
if len(user.Password) > 0 && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) {
pwd, err := argon2id.CreateHash(user.Password, argon2id.DefaultParams)
if err != nil {
return err
@ -272,6 +284,12 @@ func checkUserAndPass(user User, password string) (User, error) {
return user, err
}
match = true
} else if utils.IsStringPrefixInSlice(user.Password, pbkdfPwdPrefixes) {
match, err = comparePbkdf2PasswordAndHash(password, user.Password)
if err != nil {
logger.Warn(logSender, "error comparing password with pbkdf2 sha256 hash: %v", err)
return user, err
}
}
if !match {
err = errors.New("Invalid credentials")
@ -296,6 +314,37 @@ func checkUserAndPubKey(user User, pubKey string) (User, error) {
return user, errors.New("Invalid credentials")
}
func comparePbkdf2PasswordAndHash(password, hashedPassword string) (bool, error) {
vals := strings.Split(hashedPassword, "$")
if len(vals) != 5 {
return false, fmt.Errorf("pbkdf2: hash is not in the correct format")
}
var hashFunc func() hash.Hash
var hashSize int
if strings.HasPrefix(hashedPassword, pbkdf2SHA256Prefix) {
hashSize = sha256.Size
hashFunc = sha256.New
} else if strings.HasPrefix(hashedPassword, pbkdf2SHA512Prefix) {
hashSize = sha512.Size
hashFunc = sha512.New
} else if strings.HasPrefix(hashedPassword, pbkdf2SHA1Prefix) {
hashSize = sha1.Size
hashFunc = sha1.New
} 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 := vals[4]
df := pbkdf2.Key([]byte(password), []byte(salt), iterations, hashSize, hashFunc)
buf := make([]byte, base64.StdEncoding.EncodedLen(len(df)))
base64.StdEncoding.Encode(buf, df)
return subtle.ConstantTimeCompare(buf, []byte(expected)) == 1, nil
}
func getSSLMode() string {
if config.Driver == PGSSQLDataProviderName {
if config.SSLMode == 0 {

View file

@ -60,14 +60,17 @@ class SFTPGoApiRequests:
def buildUserObject(self, user_id=0, username="", password="", public_keys="", home_dir="", uid=0,
gid=0, max_sessions=0, quota_size=0, quota_files=0, permissions=[], upload_bandwidth=0,
download_bandwidth=0):
user = {"id":user_id, "username":username, "home_dir":home_dir, "uid":uid, "gid":gid,
user = {"id":user_id, "username":username, "uid":uid, "gid":gid,
"max_sessions":max_sessions, "quota_size":quota_size, "quota_files":quota_files,
"permissions":permissions, "upload_bandwidth":upload_bandwidth,
"download_bandwidth":download_bandwidth}
"upload_bandwidth":upload_bandwidth,"download_bandwidth":download_bandwidth}
if password:
user.update({"password":password})
if public_keys:
user.update({"public_keys":public_keys})
if home_dir:
user.update({"home_dir":home_dir})
if permissions:
user.update({"permissions":permissions})
return user
def getUsers(self, limit=100, offset=0, order="ASC", username=""):

View file

@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/drakkan/sftpgo/logger"
@ -22,6 +23,17 @@ func IsStringInSlice(obj string, list []string) bool {
return false
}
// IsStringPrefixInSlice searches a string prefix in a slice and returns true
// if a matching prefix is found
func IsStringPrefixInSlice(obj string, list []string) bool {
for _, v := range list {
if strings.HasPrefix(obj, v) {
return true
}
}
return false
}
// GetTimeAsMsSinceEpoch returns unix timestamp as milliseconds from a time struct
func GetTimeAsMsSinceEpoch(t time.Time) int64 {
return t.UnixNano() / 1000000