parent
3ac832c8dd
commit
ced2e16f41
8 changed files with 104 additions and 8 deletions
|
@ -238,6 +238,14 @@ func Init() {
|
|||
},
|
||||
Algo: dataprovider.HashingAlgoBcrypt,
|
||||
},
|
||||
PasswordValidation: dataprovider.PasswordValidation{
|
||||
Admins: dataprovider.PasswordValidationRules{
|
||||
MinEntropy: 0,
|
||||
},
|
||||
Users: dataprovider.PasswordValidationRules{
|
||||
MinEntropy: 0,
|
||||
},
|
||||
},
|
||||
PasswordCaching: true,
|
||||
UpdateMode: 0,
|
||||
PreferDatabaseCredentials: false,
|
||||
|
@ -1050,6 +1058,8 @@ func setViperDefaults() {
|
|||
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.algo", globalConf.ProviderConf.PasswordHashing.Algo)
|
||||
viper.SetDefault("data_provider.password_validation.admins.min_entropy", globalConf.ProviderConf.PasswordValidation.Admins.MinEntropy)
|
||||
viper.SetDefault("data_provider.password_validation.users.min_entropy", globalConf.ProviderConf.PasswordValidation.Users.MinEntropy)
|
||||
viper.SetDefault("data_provider.password_caching", globalConf.ProviderConf.PasswordCaching)
|
||||
viper.SetDefault("data_provider.update_mode", globalConf.ProviderConf.UpdateMode)
|
||||
viper.SetDefault("data_provider.skip_natural_keys_validation", globalConf.ProviderConf.SkipNaturalKeysValidation)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
passwordvalidator "github.com/wagslane/go-password-validator"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/util"
|
||||
|
@ -68,6 +69,11 @@ type Admin struct {
|
|||
|
||||
func (a *Admin) checkPassword() error {
|
||||
if a.Password != "" && !util.IsStringPrefixInSlice(a.Password, internalHashPwdPrefixes) {
|
||||
if config.PasswordValidation.Admins.MinEntropy > 0 {
|
||||
if err := passwordvalidator.Validate(a.Password, config.PasswordValidation.Admins.MinEntropy); err != nil {
|
||||
return util.NewValidationError(err.Error())
|
||||
}
|
||||
}
|
||||
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
||||
pwd, err := bcrypt.GenerateFromPassword([]byte(a.Password), config.PasswordHashing.BcryptOptions.Cost)
|
||||
if err != nil {
|
||||
|
|
|
@ -39,6 +39,7 @@ import (
|
|||
"github.com/alexedwards/argon2id"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/rs/xid"
|
||||
passwordvalidator "github.com/wagslane/go-password-validator"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
@ -171,6 +172,23 @@ type PasswordHashing struct {
|
|||
Algo string `json:"algo" mapstructure:"algo"`
|
||||
}
|
||||
|
||||
// PasswordValidationRules defines the password validation rules
|
||||
type PasswordValidationRules struct {
|
||||
// MinEntropy defines the minimum password entropy.
|
||||
// 0 means disabled, any password will be accepted.
|
||||
// Take a look at the following link for more details
|
||||
// https://github.com/wagslane/go-password-validator#what-entropy-value-should-i-use
|
||||
MinEntropy float64 `json:"min_entropy" mapstructure:"min_entropy"`
|
||||
}
|
||||
|
||||
// PasswordValidation defines the password validation rules for admins and protocol users
|
||||
type PasswordValidation struct {
|
||||
// Password validation rules for SFTPGo admin users
|
||||
Admins PasswordValidationRules `json:"admins" mapstructure:"admins"`
|
||||
// Password validation rules for SFTPGo protocol users
|
||||
Users PasswordValidationRules `json:"users" mapstructure:"users"`
|
||||
}
|
||||
|
||||
// UserActions defines the action to execute on user create, update, delete.
|
||||
type UserActions struct {
|
||||
// Valid values are add, update, delete. Empty slice to disable
|
||||
|
@ -301,6 +319,8 @@ type Config struct {
|
|||
// folder name. These keys are used in URIs for REST API and Web admin. By default only unreserved URI
|
||||
// characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~".
|
||||
SkipNaturalKeysValidation bool `json:"skip_natural_keys_validation" mapstructure:"skip_natural_keys_validation"`
|
||||
// PasswordValidation defines the password validation rules
|
||||
PasswordValidation PasswordValidation `json:"password_validation" mapstructure:"password_validation"`
|
||||
// Verifying argon2 passwords has a high memory and computational cost,
|
||||
// by enabling, in memory, password caching you reduce this cost.
|
||||
PasswordCaching bool `json:"password_caching" mapstructure:"password_caching"`
|
||||
|
@ -1424,6 +1444,11 @@ func validateBaseParams(user *User) error {
|
|||
|
||||
func createUserPasswordHash(user *User) error {
|
||||
if user.Password != "" && !user.IsPasswordHashed() {
|
||||
if config.PasswordValidation.Users.MinEntropy > 0 {
|
||||
if err := passwordvalidator.Validate(user.Password, config.PasswordValidation.Users.MinEntropy); err != nil {
|
||||
return util.NewValidationError(err.Error())
|
||||
}
|
||||
}
|
||||
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
||||
pwd, err := bcrypt.GenerateFromPassword([]byte(user.Password), config.PasswordHashing.BcryptOptions.Cost)
|
||||
if err != nil {
|
||||
|
|
|
@ -193,6 +193,11 @@ The configuration file contains the following sections:
|
|||
- `bcrypt_options`, struct containing the options for bcrypt hashing algorithm
|
||||
- `cost`, integer between 4 and 31. Default: 10
|
||||
- `algo`, string. Algorithm to use for hashing passwords. Available algorithms: `argon2id`, `bcrypt`. For bcrypt hashing we use the `$2a$` prefix. Default: `bcrypt`
|
||||
- `password_validation` struct. It defines the password validation rules for admins and protocol users.
|
||||
- `admins`, struct. It defines the password validation rules for SFTPGo admins.
|
||||
- `min_entropy`, float. Defines the minimum password entropy. Take a looke [here](https://github.com/wagslane/go-password-validator#what-entropy-value-should-i-use) for more details. `0` means disabled, any password will be accepted. Default: `0`.
|
||||
- `users`, struct. It defines the password validation rules for SFTPGo protocol users.
|
||||
- `min_entropy`, float. Default: `0`.
|
||||
- `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`.
|
||||
|
|
7
go.mod
7
go.mod
|
@ -7,7 +7,7 @@ require (
|
|||
github.com/Azure/azure-storage-blob-go v0.14.0
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8
|
||||
github.com/aws/aws-sdk-go v1.40.15
|
||||
github.com/aws/aws-sdk-go v1.40.16
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.1.1
|
||||
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
||||
github.com/fatih/color v1.12.0 // indirect
|
||||
|
@ -53,6 +53,7 @@ require (
|
|||
github.com/spf13/viper v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
github.com/yl2chen/cidranger v1.0.2
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
go.uber.org/automaxprocs v1.4.0
|
||||
|
@ -62,8 +63,8 @@ require (
|
|||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
|
||||
google.golang.org/api v0.52.0
|
||||
google.golang.org/genproto v0.0.0-20210804223703-f1db76f3300d // indirect
|
||||
google.golang.org/grpc v1.39.0
|
||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67 // indirect
|
||||
google.golang.org/grpc v1.39.1
|
||||
google.golang.org/protobuf v1.27.1
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
|
13
go.sum
13
go.sum
|
@ -118,8 +118,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
|
|||
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.38.68/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.40.15 h1:aqQCwW8meVzLCacWX8NEPg8bBkL0ZlcMSbhwrsg6eNE=
|
||||
github.com/aws/aws-sdk-go v1.40.15/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.40.16 h1:Tgg7i9ee2j6ir2EfejPDJBB3PyfUM4dPlvmMLtvJVfo=
|
||||
github.com/aws/aws-sdk-go v1.40.16/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go-v2 v1.7.0/go.mod h1:tb9wi5s61kTDA5qCkcDbt3KRVV74GGslQkl/DRdX/P4=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.5.0/go.mod h1:acH3+MQoiMzozT/ivU+DbRg7Ooo2298RdRaWcOv+4vM=
|
||||
github.com/aws/smithy-go v1.5.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
|
@ -713,6 +713,8 @@ github.com/tklauser/numcpus v0.2.3 h1:nQ0QYpiritP6ViFhrKYsiv6VVxOpum2Gks5GhnJbS/
|
|||
github.com/tklauser/numcpus v0.2.3/go.mod h1:vpEPS/JC+oZGGQ/My/vJnNsvMDQL6PwOqt8dsCw5j+E=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
|
||||
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
||||
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
@ -1085,8 +1087,8 @@ google.golang.org/genproto v0.0.0-20210624174822-c5cf32407d0a/go.mod h1:SzzZ/N+n
|
|||
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||
google.golang.org/genproto v0.0.0-20210721163202-f1cecdd8b78a/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210804223703-f1db76f3300d h1:Y9fT4WNRxuD0qofEPeWJwNC5kYLBcSXx0m91zyCMzYY=
|
||||
google.golang.org/genproto v0.0.0-20210804223703-f1db76f3300d/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67 h1:VmMSf20ssFK0+u1dscyTH9bU4/M4y+X/xNfkvD6kGtM=
|
||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
|
@ -1112,8 +1114,9 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
|
|||
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI=
|
||||
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.39.1 h1:f37vZbBVTiJ6jKG5mWz8ySOBxNqy6ViPgyhSdVnxF3E=
|
||||
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
|
|
|
@ -628,6 +628,44 @@ func TestChangeAdminPassword(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPasswordValidations(t *testing.T) {
|
||||
if config.GetProviderConf().Driver == dataprovider.MemoryDataProviderName {
|
||||
t.Skip("this test is not supported with the memory provider")
|
||||
}
|
||||
err := dataprovider.Close()
|
||||
assert.NoError(t, err)
|
||||
err = config.LoadConfig(configDir, "")
|
||||
providerConf := config.GetProviderConf()
|
||||
assert.NoError(t, err)
|
||||
providerConf.PasswordValidation.Admins.MinEntropy = 50
|
||||
providerConf.PasswordValidation.Users.MinEntropy = 70
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
a := getTestAdmin()
|
||||
a.Username = altAdminUsername
|
||||
a.Password = altAdminPassword
|
||||
|
||||
_, resp, err := httpdtest.AddAdmin(a, http.StatusBadRequest)
|
||||
assert.NoError(t, err, string(resp))
|
||||
assert.Contains(t, string(resp), "insecure password")
|
||||
|
||||
_, resp, err = httpdtest.AddUser(getTestUser(), http.StatusBadRequest)
|
||||
assert.NoError(t, err, string(resp))
|
||||
assert.Contains(t, string(resp), "insecure password")
|
||||
|
||||
err = dataprovider.Close()
|
||||
assert.NoError(t, err)
|
||||
err = config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
providerConf = config.GetProviderConf()
|
||||
providerConf.CredentialsPath = credentialsPath
|
||||
err = os.RemoveAll(credentialsPath)
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAdminPasswordHashing(t *testing.T) {
|
||||
if config.GetProviderConf().Driver == dataprovider.MemoryDataProviderName {
|
||||
t.Skip("this test is not supported with the memory provider")
|
||||
|
|
|
@ -179,6 +179,14 @@
|
|||
},
|
||||
"algo": "bcrypt"
|
||||
},
|
||||
"password_validation": {
|
||||
"admins": {
|
||||
"min_entropy": 0
|
||||
},
|
||||
"users": {
|
||||
"min_entropy": 0
|
||||
}
|
||||
},
|
||||
"password_caching": true,
|
||||
"update_mode": 0,
|
||||
"skip_natural_keys_validation": false,
|
||||
|
|
Loading…
Reference in a new issue