From 133f2e8601d2336658dfdd80624b5e9b313e117c Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sat, 17 Aug 2019 15:20:49 +0200 Subject: [PATCH] add support for checking pbkdf2 passwords --- README.md | 2 +- dataprovider/dataprovider.go | 53 ++++++++++++++++++++++++++++++++++-- scripts/sftpgo_api_cli.py | 9 ++++-- utils/utils.go | 12 ++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8f61be18..f4729697 100644 --- a/README.md +++ b/README.md @@ -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 `$$$$`, 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. diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 4d68003f..17f9695d 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -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 { diff --git a/scripts/sftpgo_api_cli.py b/scripts/sftpgo_api_cli.py index bc635bf5..7af1b967 100755 --- a/scripts/sftpgo_api_cli.py +++ b/scripts/sftpgo_api_cli.py @@ -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=""): diff --git a/utils/utils.go b/utils/utils.go index a2ea4f9e..a2a5b82d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -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