mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-07 06:40:54 +03:00
add support for checking pbkdf2 passwords
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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) &&
|
if len(user.Password) > 0 && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) {
|
||||||
!strings.HasPrefix(user.Password, bcryptPwdPrefix) {
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -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,
|
"upload_bandwidth":upload_bandwidth,"download_bandwidth":download_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=""):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user