use bcrypt as default password hashing algo
argon2id has a high memory cost and, if not properly tuned, it can lead to resource starvation. Advanced users can still configure and use argon2id. Passwords stored as argon2id will continue to work
This commit is contained in:
parent
74b51f0ad3
commit
46998252e5
10 changed files with 70 additions and 27 deletions
|
@ -12,8 +12,10 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
|
@ -665,3 +667,43 @@ func TestCachedFs(t *testing.T) {
|
|||
err = os.Remove(user.HomeDir)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func BenchmarkBcryptHashing(b *testing.B) {
|
||||
bcryptPassword := "bcryptpassword"
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := bcrypt.GenerateFromPassword([]byte(bcryptPassword), 10)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompareBcryptPassword(b *testing.B) {
|
||||
bcryptPassword := "$2a$10$lPDdnDimJZ7d5/GwL6xDuOqoZVRXok6OHHhivCnanWUtcgN0Zafki"
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(bcryptPassword), []byte("password"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkArgon2Hashing(b *testing.B) {
|
||||
argonPassword := "argon2password"
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := argon2id.CreateHash(argonPassword, argon2id.DefaultParams)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompareArgon2Password(b *testing.B) {
|
||||
argon2Password := "$argon2id$v=19$m=65536,t=1,p=2$aOoAOdAwvzhOgi7wUFjXlw$wn/y37dBWdKHtPXHR03nNaKHWKPXyNuVXOknaU+YZ+s"
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := argon2id.ComparePasswordAndHash("password", argon2Password)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1987,13 +1987,13 @@ func TestUserPasswordHashing(t *testing.T) {
|
|||
err = config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
providerConf := config.GetProviderConf()
|
||||
providerConf.PasswordHashingAlgo = dataprovider.HashingAlgoBcrypt
|
||||
providerConf.PasswordHashing.Algo = dataprovider.HashingAlgoArgon2ID
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
currentUser, err := dataprovider.UserExists(user.Username)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(currentUser.Password, "$argon2id$"))
|
||||
assert.True(t, strings.HasPrefix(currentUser.Password, "$2a$"))
|
||||
|
||||
conn, client, err := getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -2014,7 +2014,7 @@ func TestUserPasswordHashing(t *testing.T) {
|
|||
|
||||
currentUser, err = dataprovider.UserExists(user.Username)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(currentUser.Password, "$2a$"))
|
||||
assert.True(t, strings.HasPrefix(currentUser.Password, "$argon2id$"))
|
||||
|
||||
conn, client, err = getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
|
|
|
@ -223,8 +223,8 @@ func Init() {
|
|||
BcryptOptions: dataprovider.BcryptOptions{
|
||||
Cost: 10,
|
||||
},
|
||||
Algo: dataprovider.HashingAlgoBcrypt,
|
||||
},
|
||||
PasswordHashingAlgo: dataprovider.HashingAlgoArgon2ID,
|
||||
PasswordCaching: true,
|
||||
UpdateMode: 0,
|
||||
PreferDatabaseCredentials: false,
|
||||
|
@ -940,11 +940,11 @@ func setViperDefaults() {
|
|||
viper.SetDefault("data_provider.post_login_scope", globalConf.ProviderConf.PostLoginScope)
|
||||
viper.SetDefault("data_provider.check_password_hook", globalConf.ProviderConf.CheckPasswordHook)
|
||||
viper.SetDefault("data_provider.check_password_scope", globalConf.ProviderConf.CheckPasswordScope)
|
||||
viper.SetDefault("data_provider.password_hashing.bcrypt_options.cost", globalConf.ProviderConf.PasswordHashing.BcryptOptions.Cost)
|
||||
viper.SetDefault("data_provider.password_hashing.argon2_options.memory", globalConf.ProviderConf.PasswordHashing.Argon2Options.Memory)
|
||||
viper.SetDefault("data_provider.password_hashing.argon2_options.iterations", globalConf.ProviderConf.PasswordHashing.Argon2Options.Iterations)
|
||||
viper.SetDefault("data_provider.password_hashing.argon2_options.parallelism", globalConf.ProviderConf.PasswordHashing.Argon2Options.Parallelism)
|
||||
viper.SetDefault("data_provider.password_hashing.bcrypt_options.cost", globalConf.ProviderConf.PasswordHashing.BcryptOptions.Cost)
|
||||
viper.SetDefault("data_provider.password_hashing_algo", globalConf.ProviderConf.PasswordHashingAlgo)
|
||||
viper.SetDefault("data_provider.password_hashing.algo", globalConf.ProviderConf.PasswordHashing.Algo)
|
||||
viper.SetDefault("data_provider.update_mode", globalConf.ProviderConf.UpdateMode)
|
||||
viper.SetDefault("data_provider.skip_natural_keys_validation", globalConf.ProviderConf.SkipNaturalKeysValidation)
|
||||
viper.SetDefault("data_provider.delayed_quota_update", globalConf.ProviderConf.DelayedQuotaUpdate)
|
||||
|
|
|
@ -66,7 +66,7 @@ type Admin struct {
|
|||
|
||||
func (a *Admin) checkPassword() error {
|
||||
if a.Password != "" && !strings.HasPrefix(a.Password, argonPwdPrefix) {
|
||||
if config.PasswordHashingAlgo == HashingAlgoBcrypt {
|
||||
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
||||
pwd, err := bcrypt.GenerateFromPassword([]byte(a.Password), config.PasswordHashing.BcryptOptions.Cost)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -158,8 +158,10 @@ type Argon2Options struct {
|
|||
|
||||
// PasswordHashing defines the configuration for password hashing
|
||||
type PasswordHashing struct {
|
||||
Argon2Options Argon2Options `json:"argon2_options" mapstructure:"argon2_options"`
|
||||
BcryptOptions BcryptOptions `json:"bcrypt_options" mapstructure:"bcrypt_options"`
|
||||
Argon2Options Argon2Options `json:"argon2_options" mapstructure:"argon2_options"`
|
||||
// Algorithm to use for hashing passwords. Available algorithms: argon2id, bcrypt. Default: bcrypt
|
||||
Algo string `json:"algo" mapstructure:"algo"`
|
||||
}
|
||||
|
||||
// UserActions defines the action to execute on user create, update, delete.
|
||||
|
@ -287,8 +289,7 @@ type Config struct {
|
|||
// PreferDatabaseCredentials indicates whether credential files (currently used for Google
|
||||
// Cloud Storage) should be stored in the database instead of in the directory specified by
|
||||
// CredentialsPath.
|
||||
PasswordHashingAlgo string `json:"password_hashing_algo" mapstructure:"password_hashing_algo"`
|
||||
PreferDatabaseCredentials bool `json:"prefer_database_credentials" mapstructure:"prefer_database_credentials"`
|
||||
PreferDatabaseCredentials bool `json:"prefer_database_credentials" mapstructure:"prefer_database_credentials"`
|
||||
// SkipNaturalKeysValidation allows to use any UTF-8 character for natural keys as username, admin name,
|
||||
// folder name. These keys are used in URIs for REST API and Web admin. By default only unreserved URI
|
||||
// characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~".
|
||||
|
@ -461,7 +462,7 @@ func Initialize(cnf Config, basePath string, checkAdmins bool) error {
|
|||
KeyLength: 32,
|
||||
}
|
||||
|
||||
if config.PasswordHashingAlgo == HashingAlgoBcrypt {
|
||||
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
||||
if config.PasswordHashing.BcryptOptions.Cost > bcrypt.MaxCost {
|
||||
err = fmt.Errorf("invalid bcrypt cost %v, max allowed %v", config.PasswordHashing.BcryptOptions.Cost, bcrypt.MaxCost)
|
||||
logger.WarnToConsole("Unable to initialize data provider: %v", err)
|
||||
|
@ -1520,7 +1521,7 @@ func validateBaseParams(user *User) error {
|
|||
|
||||
func createUserPasswordHash(user *User) error {
|
||||
if user.Password != "" && !user.IsPasswordHashed() {
|
||||
if config.PasswordHashingAlgo == HashingAlgoBcrypt {
|
||||
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
||||
pwd, err := bcrypt.GenerateFromPassword([]byte(user.Password), config.PasswordHashing.BcryptOptions.Cost)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -174,8 +174,8 @@ type User struct {
|
|||
// 0 means no expiration
|
||||
ExpirationDate int64 `json:"expiration_date"`
|
||||
// Password used for password authentication.
|
||||
// For users created using SFTPGo REST API the password is be stored using argon2id hashing algo.
|
||||
// Checking passwords stored with bcrypt, pbkdf2, md5crypt and sha512crypt is supported too.
|
||||
// For users created using SFTPGo REST API the password is be stored using bcrypt or argon2id hashing algo.
|
||||
// Checking passwords stored with pbkdf2, md5crypt and sha512crypt is supported too.
|
||||
Password string `json:"password,omitempty"`
|
||||
// PublicKeys used for public key authentication. At least one between password and a public key is mandatory
|
||||
PublicKeys []string `json:"public_keys,omitempty"`
|
||||
|
|
|
@ -194,15 +194,15 @@ The configuration file contains the following sections:
|
|||
- `post_login_scope`, defines the scope for the post-login hook. 0 means notify both failed and successful logins. 1 means notify failed logins. 2 means notify successful logins.
|
||||
- `check_password_hook`, string. Absolute path to an external program or an HTTP URL to invoke to check the user provided password. See [Check password hook](./check-password-hook.md) for more details. Leave empty to disable.
|
||||
- `check_password_scope`, defines the scope for the check password hook. 0 means all protocols, 1 means SSH, 2 means FTP, 4 means WebDAV. You can combine the scopes, for example 6 means FTP and WebDAV.
|
||||
- `password_hashing`, struct. It contains the configuration parameters to be used to generate the password hash. SFTPGo can verify passwords in several formats and uses, by default, the `argon2id` algorithm to hash passwords in plain-text before storing them inside the data provider. These options allow you to customize how the hash is generated.
|
||||
- `password_hashing`, struct. It contains the configuration parameters to be used to generate the password hash. SFTPGo can verify passwords in several formats and uses, by default, the `bcrypt` algorithm to hash passwords in plain-text before storing them inside the data provider. These options allow you to customize how the hash is generated.
|
||||
- `argon2_options`, struct containing the options for argon2id hashing algorithm. The `memory` and `iterations` parameters control the computational cost of hashing the password. The higher these figures are, the greater the cost of generating the hash and the longer the runtime. It also follows that the greater the cost will be for any attacker trying to guess the password. If the code is running on a machine with multiple cores, then you can decrease the runtime without reducing the cost by increasing the `parallelism` parameter. This controls the number of threads that the work is spread across.
|
||||
- `memory`, unsigned integer. The amount of memory used by the algorithm (in kibibytes). Default: 65536.
|
||||
- `iterations`, unsigned integer. The number of iterations over the memory. Default: 1.
|
||||
- `parallelism`. unsigned 8 bit integer. The number of threads (or lanes) used by the algorithm. Default: 2.
|
||||
- `bcrypt_options`, struct containing the options for bcrypt hashing algorithm
|
||||
- `cost`, integer between 4 and 31. Default: 10
|
||||
- `password_hashing_algo`, string. Algorithm to use for hashing passwords. Available algorithms: `argon2id`, `bcrypt`. Default: `argon2id`
|
||||
- `password_caching`, boolean. Verifying argon2id passwords has a high memory and computational cost, verifying bcrypt passwords has a high CPU cost, by enabling, in memory, password caching you reduce these costs. Default: `true`
|
||||
- `algo`, string. Algorithm to use for hashing passwords. Available algorithms: `argon2id`, `bcrypt`. For bcrypt hashing we use the `$s2a$` prefix. Default: `bcrypt`
|
||||
- `password_caching`, boolean. Verifying argon2id passwords has a high memory and computational cost, verifying bcrypt passwords has a high computational cost, by enabling, in memory, password caching you reduce these costs. Default: `true`
|
||||
- `update_mode`, integer. Defines how the database will be initialized/updated. 0 means automatically. 1 means manually using the initprovider sub-command.
|
||||
- `skip_natural_keys_validation`, boolean. If `true` you can use any UTF-8 character for natural keys as username, admin name, folder name. These keys are used in URIs for REST API and Web admin. If `false` only unreserved URI characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~". Default: `false`.
|
||||
- `delayed_quota_update`, integer. This configuration parameter defines the number of seconds to accumulate quota updates. If there are a lot of close uploads, accumulating quota updates can save you many queries to the data provider. If you want to track quotas, a scheduled quota update is recommended in any case, the stored quota size may be incorrect for several reasons, such as an unexpected shutdown, temporary provider failures, file copied outside of SFTPGo, and so on. 0 means immediate quota update.
|
||||
|
|
|
@ -291,8 +291,8 @@ func TestInitialization(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBasicUserHandling(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
user, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err, string(resp))
|
||||
user.MaxSessions = 10
|
||||
user.QuotaSize = 4096
|
||||
user.QuotaFiles = 2
|
||||
|
@ -383,13 +383,13 @@ func TestAdminPasswordHashing(t *testing.T) {
|
|||
err = config.LoadConfig(configDir, "")
|
||||
providerConf := config.GetProviderConf()
|
||||
assert.NoError(t, err)
|
||||
providerConf.PasswordHashingAlgo = dataprovider.HashingAlgoBcrypt
|
||||
providerConf.PasswordHashing.Algo = dataprovider.HashingAlgoArgon2ID
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
currentAdmin, err := dataprovider.AdminExists(defaultTokenAuthUser)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(currentAdmin.Password, "$argon2id$"))
|
||||
assert.True(t, strings.HasPrefix(currentAdmin.Password, "$2a$"))
|
||||
|
||||
a := getTestAdmin()
|
||||
a.Username = altAdminUsername
|
||||
|
@ -400,7 +400,7 @@ func TestAdminPasswordHashing(t *testing.T) {
|
|||
|
||||
newAdmin, err := dataprovider.AdminExists(altAdminUsername)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(newAdmin.Password, "$2a$"))
|
||||
assert.True(t, strings.HasPrefix(newAdmin.Password, "$argon2id$"))
|
||||
|
||||
token, _, err := httpdtest.GetToken(altAdminUsername, altAdminPassword)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -1753,7 +1753,7 @@ components:
|
|||
password:
|
||||
type: string
|
||||
format: password
|
||||
description: password or public key/SSH user certificate are mandatory. If the password has no known hashing algo prefix it will be stored, by default, using argon2id. You can send a password hashed as bcrypt or pbkdf2 and it will be stored as is. For security reasons this field is omitted when you search/get users
|
||||
description: password or public key/SSH user certificate are mandatory. If the password has no known hashing algo prefix it will be stored, by default, using bcrypt, argon2id is supported too. You can send a password hashed as bcrypt ($s2a$ prefix), argon2id, pbkdf2 or unix crypt and it will be stored as is. For security reasons this field is omitted when you search/get users
|
||||
public_keys:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
@ -161,16 +161,16 @@
|
|||
"check_password_hook": "",
|
||||
"check_password_scope": 0,
|
||||
"password_hashing": {
|
||||
"bcrypt_options": {
|
||||
"cost": 10
|
||||
},
|
||||
"argon2_options": {
|
||||
"memory": 65536,
|
||||
"iterations": 1,
|
||||
"parallelism": 2
|
||||
},
|
||||
"bcrypt_options": {
|
||||
"cost": 10
|
||||
}
|
||||
"algo": "bcrypt"
|
||||
},
|
||||
"password_hashing_algo": "argon2id",
|
||||
"password_caching": true,
|
||||
"update_mode": 0,
|
||||
"skip_natural_keys_validation": false
|
||||
|
|
Loading…
Reference in a new issue