From ced2e16f415e61be73192f7320f1c24778807b4a Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Fri, 6 Aug 2021 18:56:07 +0200 Subject: [PATCH] add support for password validation rules Fixes #494 --- config/config.go | 10 ++++++++++ dataprovider/admin.go | 6 ++++++ dataprovider/dataprovider.go | 25 ++++++++++++++++++++++++ docs/full-configuration.md | 5 +++++ go.mod | 7 ++++--- go.sum | 13 +++++++----- httpd/httpd_test.go | 38 ++++++++++++++++++++++++++++++++++++ sftpgo.json | 8 ++++++++ 8 files changed, 104 insertions(+), 8 deletions(-) diff --git a/config/config.go b/config/config.go index 2ab611c3..6899044c 100644 --- a/config/config.go +++ b/config/config.go @@ -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) diff --git a/dataprovider/admin.go b/dataprovider/admin.go index 2d97ab09..18fa4190 100644 --- a/dataprovider/admin.go +++ b/dataprovider/admin.go @@ -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 { diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index a21e87ef..733b79ef 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -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 { diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 55c685ff..bd89e2a3 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -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`. diff --git a/go.mod b/go.mod index 7c13bab5..3d52f22a 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 61ede630..d52d8ea8 100644 --- a/go.sum +++ b/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= diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index c79915d8..eefcb2cb 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -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") diff --git a/sftpgo.json b/sftpgo.json index 47d9c172..74a516f3 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -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,