diff --git a/weed/sftpd/user/filestore.go b/weed/sftpd/user/filestore.go index 5bd0b0513..f740d442c 100644 --- a/weed/sftpd/user/filestore.go +++ b/weed/sftpd/user/filestore.go @@ -154,8 +154,14 @@ func (s *FileStore) ValidatePassword(username string, password []byte) bool { return false } - // Compare plaintext password using constant time comparison for security - return subtle.ConstantTimeCompare([]byte(user.Password), password) == 1 + if user.CheckPassword(string(password)) { + // If legacy password was migrated to bcrypt, persist the change + if user.HashedPassword != "" && user.Password == "" { + _ = s.saveUsers() + } + return true + } + return false } // ValidatePublicKey checks if the public key is valid for the user @@ -250,8 +256,8 @@ func (s *FileStore) CreateUser(username, password string) (*User, error) { // Create new user user := NewUser(username) - // Store plaintext password - user.Password = password + // Hash password with bcrypt + user.SetPassword(password) // Add default permissions user.Permissions[user.HomeDir] = []string{"all"} diff --git a/weed/sftpd/user/user.go b/weed/sftpd/user/user.go index 9edaf1a6b..4b1035fad 100644 --- a/weed/sftpd/user/user.go +++ b/weed/sftpd/user/user.go @@ -4,17 +4,20 @@ package user import ( "math/rand/v2" "path/filepath" + + "golang.org/x/crypto/bcrypt" ) // User represents an SFTP user with authentication and permission details type User struct { - Username string // Username for authentication - Password string // Plaintext password - PublicKeys []string // Authorized public keys - HomeDir string // User's home directory - Permissions map[string][]string // path -> permissions (read, write, list, etc.) - Uid uint32 // User ID for file ownership - Gid uint32 // Group ID for file ownership + Username string `json:"Username"` + HashedPassword string `json:"HashedPassword"` // bcrypt hash + Password string `json:"Password,omitempty"` // deprecated: plaintext, migrated on next save + PublicKeys []string `json:"PublicKeys,omitempty"` + HomeDir string `json:"HomeDir"` + Permissions map[string][]string `json:"Permissions,omitempty"` + Uid uint32 `json:"Uid"` + Gid uint32 `json:"Gid"` } // NewUser creates a new user with default settings @@ -33,9 +36,29 @@ func NewUser(username string) *User { } } -// SetPassword sets a plaintext password for the user +// SetPassword hashes and stores the password using bcrypt func (u *User) SetPassword(password string) { - u.Password = password + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + // bcrypt only errors on passwords > 72 bytes; truncate if needed + hash, _ = bcrypt.GenerateFromPassword([]byte(password[:72]), bcrypt.DefaultCost) + } + u.HashedPassword = string(hash) + u.Password = "" // clear any legacy plaintext +} + +// CheckPassword verifies a password against the stored hash. +// It transparently handles legacy plaintext passwords by upgrading them on match. +func (u *User) CheckPassword(password string) bool { + if u.HashedPassword != "" { + return bcrypt.CompareHashAndPassword([]byte(u.HashedPassword), []byte(password)) == nil + } + // Legacy plaintext migration path + if u.Password != "" && u.Password == password { + u.SetPassword(password) // upgrade to bcrypt + return true + } + return false } // AddPublicKey adds a public key to the user