You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

143 lines
3.4 KiB

package sftpd
import (
"crypto/subtle"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
)
// UserStore interface for user management.
type UserStore interface {
GetUser(username string) (*User, error)
ValidatePassword(username string, password []byte) bool
ValidatePublicKey(username string, keyData string) bool
GetUserPermissions(username string, path string) []string
}
// User represents an SFTP user with authentication and permission details.
type User struct {
Username string
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
}
// FileUserStore implements UserStore using a JSON file.
type FileUserStore struct {
filePath string
users map[string]*User
mu sync.RWMutex
}
// NewFileUserStore creates a new user store from a JSON file.
func NewFileUserStore(filePath string) (*FileUserStore, error) {
store := &FileUserStore{
filePath: filePath,
users: make(map[string]*User),
}
if err := store.loadUsers(); err != nil {
return nil, err
}
return store, nil
}
// loadUsers loads users from the JSON file.
func (s *FileUserStore) loadUsers() error {
s.mu.Lock()
defer s.mu.Unlock()
// Check if file exists
if _, err := os.Stat(s.filePath); os.IsNotExist(err) {
return fmt.Errorf("user store file not found: %s", s.filePath)
}
data, err := os.ReadFile(s.filePath)
if err != nil {
return fmt.Errorf("failed to read user store file: %v", err)
}
var users []*User
if err := json.Unmarshal(data, &users); err != nil {
return fmt.Errorf("failed to parse user store file: %v", err)
}
for _, user := range users {
s.users[user.Username] = user
}
return nil
}
// GetUser returns a user by username.
func (s *FileUserStore) GetUser(username string) (*User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[username]
if !ok {
return nil, fmt.Errorf("user not found: %s", username)
}
return user, nil
}
// ValidatePassword checks if the password is valid for the user.
func (s *FileUserStore) ValidatePassword(username string, password []byte) bool {
user, err := s.GetUser(username)
if err != nil {
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 *FileUserStore) 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 *FileUserStore) 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 strings.HasPrefix(path, p) && len(p) > len(bestMatch) {
bestMatch = p
bestPerms = perms
}
}
return bestPerms
}