Skip to content

Commit 3fba0c2

Browse files
authored
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
1 parent 99e4ede commit 3fba0c2

File tree

5 files changed

+88
-12
lines changed

5 files changed

+88
-12
lines changed

config-schema.json

+12-10
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@
5151
200
5252
]
5353
},
54+
"hash_plaintext_passwords": {
55+
"type": "boolean",
56+
"default": false,
57+
"title": "Overwrite plain-text passwords with hashed equivalents",
58+
"examples": [
59+
true,
60+
false
61+
]
62+
},
5463
"passive_transfer_port_range": {
5564
"type": "object",
5665
"default": {},
@@ -183,22 +192,15 @@
183192
"type": "string",
184193
"title": "The FTP user",
185194
"examples": [
186-
"test",
187-
"dropbox",
188-
"gdrive",
189-
"s3",
190-
"sftp"
195+
"username"
191196
]
192197
},
193198
"pass": {
194199
"type": "string",
195200
"title": "The FTP password",
196201
"examples": [
197-
"test",
198-
"dropbox",
199-
"gdrive",
200-
"s3",
201-
"sftp"
202+
"plaintext-password",
203+
"$2a$10$jG7tuqIlcUDMl1m1Ytj1TunU7pk.ko8lj3nOGzZvkIeU/BsfPVBra"
202204
]
203205
},
204206
"fs": {

config/config.go

+58-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ package config
44
import (
55
"encoding/json"
66
"errors"
7+
"fmt"
78
"os"
89

910
log "github.com/fclairamb/go-log"
11+
"github.com/tidwall/sjson"
1012

1113
"github.com/fclairamb/ftpserver/config/confpar"
1214
"github.com/fclairamb/ftpserver/fs"
15+
"golang.org/x/crypto/bcrypt"
1316
)
1417

1518
// ErrUnknownUser is returned when the provided user cannot be identified through our authentication mechanism
@@ -82,9 +85,50 @@ func (c *Config) Load() error {
8285

8386
c.Content = &content
8487

88+
if c.Content.HashPlaintextPasswords {
89+
c.HashPlaintextPasswords()
90+
}
91+
8592
return c.Prepare()
8693
}
8794

95+
func (c *Config) HashPlaintextPasswords() error {
96+
97+
json, errReadFile := os.ReadFile(c.fileName)
98+
if errReadFile != nil {
99+
c.logger.Error("Cannot read config file!", "err", errReadFile)
100+
return errReadFile
101+
}
102+
103+
save := false
104+
for i, a := range c.Content.Accesses {
105+
if a.User == "anonymous" && a.Pass == "*" {
106+
continue
107+
}
108+
_, errCost := bcrypt.Cost([]byte(a.Pass))
109+
if errCost != nil {
110+
//This password is not hashed
111+
hash, errHash := bcrypt.GenerateFromPassword([]byte(a.Pass), 10)
112+
if errHash == nil {
113+
modified, errJsonSet := sjson.Set(string(json), "accesses."+fmt.Sprint(i)+".pass", string(hash))
114+
c.Content.Accesses[i].Pass = string(hash)
115+
if errJsonSet == nil {
116+
save = true
117+
json = []byte(modified)
118+
}
119+
}
120+
}
121+
}
122+
if save {
123+
errWriteFile := os.WriteFile(c.fileName, json, 0644)
124+
if errWriteFile != nil {
125+
c.logger.Error("Cannot write config file!", "err", errWriteFile)
126+
return errWriteFile
127+
}
128+
}
129+
return nil
130+
}
131+
88132
// Prepare the config before using it
89133
func (c *Config) Prepare() error {
90134
ct := c.Content
@@ -116,8 +160,20 @@ func (c *Config) CheckAccesses() error {
116160
// GetAccess return a file system access given some credentials
117161
func (c *Config) GetAccess(user string, pass string) (*confpar.Access, error) {
118162
for _, a := range c.Content.Accesses {
119-
if a.User == user && (a.Pass == pass || (a.User == "anonymous" && a.Pass == "*")) {
120-
return a, nil
163+
if a.User == user {
164+
_, errCost := bcrypt.Cost([]byte(a.Pass))
165+
if errCost == nil {
166+
//This user's password is bcrypted
167+
errCompare := bcrypt.CompareHashAndPassword([]byte(a.Pass), []byte(pass))
168+
if errCompare == nil {
169+
return a, nil
170+
}
171+
} else {
172+
//This user's password is plain-text
173+
if a.Pass == pass || (a.User == "anonymous" && a.Pass == "*") {
174+
return a, nil
175+
}
176+
}
121177
}
122178
}
123179

config/confpar/confpar.go

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type Content struct {
5050
ListenAddress string `json:"listen_address"` // Address to listen on
5151
PublicHost string `json:"public_host"` // Public host to listen on
5252
MaxClients int `json:"max_clients"` // Maximum clients who can connect
53+
HashPlaintextPasswords bool `json:"hash_plaintext_passwords"` // Overwrite plain-text passwords with hashed equivalents
5354
Accesses []*Access `json:"accesses"` // Accesses offered to users
5455
PassiveTransferPortRange *PortRange `json:"passive_transfer_port_range"` // Listen port range
5556
Logging Logging `json:"logging"` // Logging parameters

go.mod

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ require (
1717
golang.org/x/oauth2 v0.4.0
1818
)
1919

20+
require (
21+
github.com/tidwall/gjson v1.14.4 // indirect
22+
github.com/tidwall/match v1.1.1 // indirect
23+
github.com/tidwall/pretty v1.2.1 // indirect
24+
github.com/tidwall/sjson v1.2.5 // indirect
25+
)
26+
2027
require (
2128
cloud.google.com/go/compute v1.7.0 // indirect
2229
github.com/dropbox/dropbox-sdk-go-unofficial v5.6.0+incompatible // indirect

go.sum

+10
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,16 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
639639
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
640640
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
641641
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
642+
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
643+
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
644+
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
645+
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
646+
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
647+
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
648+
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
649+
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
650+
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
651+
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
642652
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
643653
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
644654
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

0 commit comments

Comments
 (0)