add support for checking pbkdf2 passwords
This commit is contained in:
parent
9d342cb125
commit
133f2e8601
4 changed files with 70 additions and 6 deletions
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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=""):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue