Files
seaweedfs/weed/sftpd/user/filestore.go
T
Chris Lu 3cdfe648eb sftp: reject an empty password (#11095)
ValidatePassword compared the stored and supplied passwords with
subtle.ConstantTimeCompare, which returns 1 for two zero-length slices.
A user provisioned for public-key-only auth has an empty stored password,
so an empty supplied password authenticated as that user whenever
"password" was among the enabled auth methods (the default).

Treat an empty stored or supplied password as a non-match.

Claude-Session: https://claude.ai/code/session_011QqNaxZwnpHgMoAZNp3RkY
2026-09-02 11:33:25 -07:00

277 lines
6.5 KiB
Go

package user
import (
"crypto/subtle"
"encoding/json"
"fmt"
"os"
"path"
"sync"
"golang.org/x/crypto/ssh"
)
// FileStore implements Store using a JSON file
type FileStore struct {
filePath string
users map[string]*User
mu sync.RWMutex
}
// Store defines the interface for user storage and retrieval
type Store interface {
// GetUser retrieves a user by username
GetUser(username string) (*User, error)
// ValidatePassword checks if the password is valid for the user
ValidatePassword(username string, password []byte) bool
// ValidatePublicKey checks if the public key is valid for the user
ValidatePublicKey(username string, keyData string) bool
// GetUserPermissions returns the permissions for a user on a path
GetUserPermissions(username string, path string) []string
// SaveUser saves or updates a user
SaveUser(user *User) error
// DeleteUser removes a user
DeleteUser(username string) error
// ListUsers returns all usernames
ListUsers() ([]string, error)
}
// UserNotFoundError is returned when a user is not found
type UserNotFoundError struct {
Username string
}
func (e *UserNotFoundError) Error() string {
return fmt.Sprintf("user not found: %s", e.Username)
}
// NewFileStore creates a new user store from a JSON file
func NewFileStore(filePath string) (*FileStore, error) {
store := &FileStore{
filePath: filePath,
users: make(map[string]*User),
}
// Create the file if it doesn't exist
if _, err := os.Stat(filePath); os.IsNotExist(err) {
// Create an empty users array
if err := os.WriteFile(filePath, []byte("[]"), 0600); err != nil {
return nil, fmt.Errorf("failed to create user store file: %w", err)
}
}
if err := store.loadUsers(); err != nil {
return nil, err
}
return store, nil
}
// loadUsers loads users from the JSON file
func (s *FileStore) loadUsers() error {
s.mu.Lock()
defer s.mu.Unlock()
data, err := os.ReadFile(s.filePath)
if err != nil {
return fmt.Errorf("failed to read user store file: %w", err)
}
var users []*User
if err := json.Unmarshal(data, &users); err != nil {
return fmt.Errorf("failed to parse user store file: %w", err)
}
// Clear existing users and add the loaded ones
s.users = make(map[string]*User)
for _, user := range users {
// Process public keys to ensure they're in the correct format
for i, keyData := range user.PublicKeys {
// Try to parse the key as an authorized key format
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyData))
if err == nil {
// If successful, store the marshaled binary format
user.PublicKeys[i] = string(pubKey.Marshal())
}
}
// Clean HomeDir to handle trailing slashes and normalize path
if user.HomeDir != "" {
user.HomeDir = path.Clean(user.HomeDir)
}
s.users[user.Username] = user
}
return nil
}
// saveUsers saves users to the JSON file
func (s *FileStore) saveUsers() error {
s.mu.RLock()
defer s.mu.RUnlock()
// Convert map to slice for JSON serialization
var users []*User
for _, user := range s.users {
users = append(users, user)
}
data, err := json.MarshalIndent(users, "", " ")
if err != nil {
return fmt.Errorf("failed to serialize users: %w", err)
}
if err := os.WriteFile(s.filePath, data, 0600); err != nil {
return fmt.Errorf("failed to write user store file: %w", err)
}
return nil
}
// GetUser returns a user by username
func (s *FileStore) GetUser(username string) (*User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[username]
if !ok {
return nil, &UserNotFoundError{Username: username}
}
return user, nil
}
// ValidatePassword checks if the password is valid for the user
func (s *FileStore) ValidatePassword(username string, password []byte) bool {
user, err := s.GetUser(username)
if err != nil {
return false
}
// An empty stored or supplied password is never a match: a public-key-only
// user has no password, and equal-length zero slices would otherwise compare equal.
if len(user.Password) == 0 || len(password) == 0 {
return false
}
// Compare plaintext password using constant time comparison for security
return subtle.ConstantTimeCompare([]byte(user.Password), password) == 1
}
// ValidatePublicKey checks if the public key is valid for the user
func (s *FileStore) ValidatePublicKey(username string, keyData string) bool {
user, err := s.GetUser(username)
if err != nil {
return false
}
for _, key := range user.PublicKeys {
if subtle.ConstantTimeCompare([]byte(key), []byte(keyData)) == 1 {
return true
}
}
return false
}
// GetUserPermissions returns the permissions for a user on a path
func (s *FileStore) GetUserPermissions(username string, path string) []string {
user, err := s.GetUser(username)
if err != nil {
return nil
}
// Check exact path match first
if perms, ok := user.Permissions[path]; ok {
return perms
}
// Check parent directories
var bestMatch string
var bestPerms []string
for p, perms := range user.Permissions {
if len(p) > len(bestMatch) && os.IsPathSeparator(p[len(p)-1]) && path[:len(p)] == p {
bestMatch = p
bestPerms = perms
}
}
return bestPerms
}
// SaveUser saves or updates a user
func (s *FileStore) SaveUser(user *User) error {
s.mu.Lock()
s.users[user.Username] = user
s.mu.Unlock()
return s.saveUsers()
}
// DeleteUser removes a user
func (s *FileStore) DeleteUser(username string) error {
s.mu.Lock()
_, exists := s.users[username]
if !exists {
s.mu.Unlock()
return &UserNotFoundError{Username: username}
}
delete(s.users, username)
s.mu.Unlock()
return s.saveUsers()
}
// ListUsers returns all usernames
func (s *FileStore) ListUsers() ([]string, error) {
s.mu.RLock()
defer s.mu.RUnlock()
usernames := make([]string, 0, len(s.users))
for username := range s.users {
usernames = append(usernames, username)
}
return usernames, nil
}
// CreateUser creates a new user with the given username and password
func (s *FileStore) CreateUser(username, password string) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
// Check if user already exists
if _, exists := s.users[username]; exists {
return nil, fmt.Errorf("user already exists: %s", username)
}
// Create new user
user := NewUser(username)
// Store plaintext password
user.Password = password
// Add default permissions
user.Permissions[user.HomeDir] = []string{"all"}
// Save the user
s.users[username] = user
if err := s.saveUsers(); err != nil {
return nil, err
}
return user, nil
}
// Reload reloads users from the file, useful for HUP signal handling
func (s *FileStore) Reload() error {
return s.loadUsers()
}