浏览代码

add support for checking pbkdf2 passwords

Nicola Murino 5 年之前
父节点
当前提交
133f2e8601
共有 4 个文件被更改,包括 70 次插入6 次删除
  1. 1 1
      README.md
  2. 51 2
      dataprovider/dataprovider.go
  3. 6 3
      scripts/sftpgo_api_cli.py
  4. 12 0
      utils/utils.go

+ 1 - 1
README.md

@@ -219,7 +219,7 @@ sftpgo serve
 For each account the following properties can be configured:
 For each account the following properties can be configured:
 
 
 - `username`
 - `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.
 - `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
 - `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.
 - `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.

+ 51 - 2
dataprovider/dataprovider.go

@@ -4,13 +4,21 @@
 package dataprovider
 package dataprovider
 
 
 import (
 import (
+	"crypto/sha1"
+	"crypto/sha256"
+	"crypto/sha512"
+	"crypto/subtle"
+	"encoding/base64"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"hash"
 	"path/filepath"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"strings"
 
 
 	"github.com/alexedwards/argon2id"
 	"github.com/alexedwards/argon2id"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/crypto/bcrypt"
+	"golang.org/x/crypto/pbkdf2"
 	"golang.org/x/crypto/ssh"
 	"golang.org/x/crypto/ssh"
 
 
 	"github.com/drakkan/sftpgo/logger"
 	"github.com/drakkan/sftpgo/logger"
@@ -30,6 +38,9 @@ const (
 	logSender                = "dataProvider"
 	logSender                = "dataProvider"
 	argonPwdPrefix           = "$argon2id$"
 	argonPwdPrefix           = "$argon2id$"
 	bcryptPwdPrefix          = "$2a$"
 	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"
 	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"
 	trackQuotaDisabledError  = "please enable track_quota in sftpgo.conf to use this method"
 )
 )
@@ -42,6 +53,8 @@ var (
 	sqlPlaceholders    []string
 	sqlPlaceholders    []string
 	validPerms         = []string{PermAny, PermListItems, PermDownload, PermUpload, PermDelete, PermRename,
 	validPerms         = []string{PermAny, PermListItems, PermDownload, PermUpload, PermDelete, PermRename,
 		PermCreateDirs, PermCreateSymlinks}
 		PermCreateDirs, PermCreateSymlinks}
+	hashPwdPrefixes  = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
+	pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
 )
 )
 
 
 // Config provider configuration
 // Config provider configuration
@@ -237,8 +250,7 @@ func validateUser(user *User) error {
 			return &ValidationError{err: fmt.Sprintf("Invalid permission: %v", p)}
 			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)
 		pwd, err := argon2id.CreateHash(user.Password, argon2id.DefaultParams)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -272,6 +284,12 @@ func checkUserAndPass(user User, password string) (User, error) {
 			return user, err
 			return user, err
 		}
 		}
 		match = true
 		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 {
 	if !match {
 		err = errors.New("Invalid credentials")
 		err = errors.New("Invalid credentials")
@@ -296,6 +314,37 @@ func checkUserAndPubKey(user User, pubKey string) (User, error) {
 	return user, errors.New("Invalid credentials")
 	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 {
 func getSSLMode() string {
 	if config.Driver == PGSSQLDataProviderName {
 	if config.Driver == PGSSQLDataProviderName {
 		if config.SSLMode == 0 {
 		if config.SSLMode == 0 {

+ 6 - 3
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,
 	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,
 					gid=0, max_sessions=0, quota_size=0, quota_files=0, permissions=[], upload_bandwidth=0,
 					download_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,
 			"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:
 		if password:
 			user.update({"password":password})
 			user.update({"password":password})
 		if public_keys:
 		if public_keys:
 			user.update({"public_keys":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
 		return user
 
 
 	def getUsers(self, limit=100, offset=0, order="ASC", username=""):
 	def getUsers(self, limit=100, offset=0, order="ASC", username=""):

+ 12 - 0
utils/utils.go

@@ -5,6 +5,7 @@ import (
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"runtime"
 	"runtime"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/drakkan/sftpgo/logger"
 	"github.com/drakkan/sftpgo/logger"
@@ -22,6 +23,17 @@ func IsStringInSlice(obj string, list []string) bool {
 	return false
 	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
 // GetTimeAsMsSinceEpoch returns unix timestamp as milliseconds from a time struct
 func GetTimeAsMsSinceEpoch(t time.Time) int64 {
 func GetTimeAsMsSinceEpoch(t time.Time) int64 {
 	return t.UnixNano() / 1000000
 	return t.UnixNano() / 1000000