From 3fba0c26609f8924b008fce581b3bed6f2b926fe Mon Sep 17 00:00:00 2001 From: wanieru Date: Wed, 11 Jan 2023 18:12:55 +0100 Subject: [PATCH] Add support for hashed passwords (#876) * Add support for hashed passwords in ftpserver.json * Add option for config file to automatically hash plain-text passwords * Prevent hashing of anonymous user's plain-text password * Fix issue where ftpserver.json got truncated without being saved * Improve the way config is saved after hashing passwords * Change in-memory config when hashing plain-text passwords --- config-schema.json | 22 +++++++------- config/config.go | 60 +++++++++++++++++++++++++++++++++++++-- config/confpar/confpar.go | 1 + go.mod | 7 +++++ go.sum | 10 +++++++ 5 files changed, 88 insertions(+), 12 deletions(-) diff --git a/config-schema.json b/config-schema.json index e2510234..32b78aa8 100644 --- a/config-schema.json +++ b/config-schema.json @@ -51,6 +51,15 @@ 200 ] }, + "hash_plaintext_passwords": { + "type": "boolean", + "default": false, + "title": "Overwrite plain-text passwords with hashed equivalents", + "examples": [ + true, + false + ] + }, "passive_transfer_port_range": { "type": "object", "default": {}, @@ -183,22 +192,15 @@ "type": "string", "title": "The FTP user", "examples": [ - "test", - "dropbox", - "gdrive", - "s3", - "sftp" + "username" ] }, "pass": { "type": "string", "title": "The FTP password", "examples": [ - "test", - "dropbox", - "gdrive", - "s3", - "sftp" + "plaintext-password", + "$2a$10$jG7tuqIlcUDMl1m1Ytj1TunU7pk.ko8lj3nOGzZvkIeU/BsfPVBra" ] }, "fs": { diff --git a/config/config.go b/config/config.go index adc7274e..f0fd6e15 100644 --- a/config/config.go +++ b/config/config.go @@ -4,12 +4,15 @@ package config import ( "encoding/json" "errors" + "fmt" "os" log "github.com/fclairamb/go-log" + "github.com/tidwall/sjson" "github.com/fclairamb/ftpserver/config/confpar" "github.com/fclairamb/ftpserver/fs" + "golang.org/x/crypto/bcrypt" ) // ErrUnknownUser is returned when the provided user cannot be identified through our authentication mechanism @@ -82,9 +85,50 @@ func (c *Config) Load() error { c.Content = &content + if c.Content.HashPlaintextPasswords { + c.HashPlaintextPasswords() + } + return c.Prepare() } +func (c *Config) HashPlaintextPasswords() error { + + json, errReadFile := os.ReadFile(c.fileName) + if errReadFile != nil { + c.logger.Error("Cannot read config file!", "err", errReadFile) + return errReadFile + } + + save := false + for i, a := range c.Content.Accesses { + if a.User == "anonymous" && a.Pass == "*" { + continue + } + _, errCost := bcrypt.Cost([]byte(a.Pass)) + if errCost != nil { + //This password is not hashed + hash, errHash := bcrypt.GenerateFromPassword([]byte(a.Pass), 10) + if errHash == nil { + modified, errJsonSet := sjson.Set(string(json), "accesses."+fmt.Sprint(i)+".pass", string(hash)) + c.Content.Accesses[i].Pass = string(hash) + if errJsonSet == nil { + save = true + json = []byte(modified) + } + } + } + } + if save { + errWriteFile := os.WriteFile(c.fileName, json, 0644) + if errWriteFile != nil { + c.logger.Error("Cannot write config file!", "err", errWriteFile) + return errWriteFile + } + } + return nil +} + // Prepare the config before using it func (c *Config) Prepare() error { ct := c.Content @@ -116,8 +160,20 @@ func (c *Config) CheckAccesses() error { // GetAccess return a file system access given some credentials func (c *Config) GetAccess(user string, pass string) (*confpar.Access, error) { for _, a := range c.Content.Accesses { - if a.User == user && (a.Pass == pass || (a.User == "anonymous" && a.Pass == "*")) { - return a, nil + if a.User == user { + _, errCost := bcrypt.Cost([]byte(a.Pass)) + if errCost == nil { + //This user's password is bcrypted + errCompare := bcrypt.CompareHashAndPassword([]byte(a.Pass), []byte(pass)) + if errCompare == nil { + return a, nil + } + } else { + //This user's password is plain-text + if a.Pass == pass || (a.User == "anonymous" && a.Pass == "*") { + return a, nil + } + } } } diff --git a/config/confpar/confpar.go b/config/confpar/confpar.go index 36c1b619..f4bbc367 100644 --- a/config/confpar/confpar.go +++ b/config/confpar/confpar.go @@ -50,6 +50,7 @@ type Content struct { ListenAddress string `json:"listen_address"` // Address to listen on PublicHost string `json:"public_host"` // Public host to listen on MaxClients int `json:"max_clients"` // Maximum clients who can connect + HashPlaintextPasswords bool `json:"hash_plaintext_passwords"` // Overwrite plain-text passwords with hashed equivalents Accesses []*Access `json:"accesses"` // Accesses offered to users PassiveTransferPortRange *PortRange `json:"passive_transfer_port_range"` // Listen port range Logging Logging `json:"logging"` // Logging parameters diff --git a/go.mod b/go.mod index fee47cc3..487663b4 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,13 @@ require ( golang.org/x/oauth2 v0.4.0 ) +require ( + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect +) + require ( cloud.google.com/go/compute v1.7.0 // indirect github.com/dropbox/dropbox-sdk-go-unofficial v5.6.0+incompatible // indirect diff --git a/go.sum b/go.sum index c80f5b43..e47a91d5 100644 --- a/go.sum +++ b/go.sum @@ -639,6 +639,16 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=