// Package htpasswd contains utilities for manipulating .htpasswd files package htpasswd import ( "errors" "fmt" "os" "strings" ) // Passwds name => hash type Passwds map[string]string // Hasher interface implemented by hash algos type Hasher interface { Hash(password string) (string, error) Match(password, hashedPassword string) error Name() string Prefix() string } // ParseFile parses a .htpasswd file // and returns a Passwd type func ParseFile(file string) (Passwds, error) { htpasswdBytes, err := os.ReadFile(file) if err != nil { return nil, err } return Parse(htpasswdBytes) } // Parse parses a slice of bytes in htpasswd style func Parse(htpasswdBytes []byte) (Passwds, error) { lines := strings.Split(string(htpasswdBytes), "\n") passwords := make(map[string]string) var err error for lineNumber, line := range lines { line = strings.Trim(line, " ") if len(line) == 0 { // skipping empty lines continue } parts := strings.Split(line, ":") if ok, err := validLine(parts, lineNumber, line); !ok { return passwords, err } parts = trimParts(parts) _, exists := passwords[parts[0]] if exists { err = errors.New("invalid htpasswd file - user " + parts[0] + " defined more than once") return passwords, err } passwords[parts[0]] = parts[1] } return passwords, err } // CreateUser creates a record in the named file with // the named password and hash algorithm func CreateUser(file, user, passwd string, algo Hasher) error { pp, err := ParseFile(file) if err != nil { return err } err = pp.CreateUser(user, passwd, algo) if err != nil { return err } return pp.WriteFile(file) } // CreateUser will create a new user in the given Passwd object // using the given name, password and hashing algorithm func (pp Passwds) CreateUser(user, passwd string, algo Hasher) error { if _, exists := pp[user]; exists { return fmt.Errorf("user %s already exists", user) } h, err := algo.Hash(passwd) if err != nil { return err } pp[user] = h return nil } // UpdateUser will update the password for the named user // in the named file func UpdateUser(file, user, passwd string, algo Hasher) error { pp, err := ParseFile(file) if err != nil { return err } err = pp.UpdateUser(user, passwd, algo) if err != nil { return err } return pp.WriteFile(file) } // UpdateUser will update the password for the named user // using the given name, password and hashing algorithm func (pp Passwds) UpdateUser(user, passwd string, algo Hasher) error { if _, exists := pp[user]; !exists { return fmt.Errorf("user %s does not exist", user) } h, err := algo.Hash(passwd) if err != nil { return err } pp[user] = h return nil } // DeleteUser deletes the named user from the named file func DeleteUser(file, user string) error { pp, err := ParseFile(file) if err != nil { return err } err = pp.DeleteUser(user) if err != nil { return err } return pp.WriteFile(file) } // DeleteUser deletes the named user from the named file func (pp Passwds) DeleteUser(user string) error { if _, exists := pp[user]; !exists { return fmt.Errorf("user %s does not exist", user) } delete(pp, user) return nil } // VerifyUser will check if the given user and password are matching // with the content of the given file func VerifyUser(file, user, passwd string) error { pp, err := ParseFile(file) if err != nil { return err } return pp.VerifyUser(user, passwd) } // VerifyUser will check if the given user and password are matching // with the given Passwd object func (pp Passwds) VerifyUser(user, passwd string) error { if _, ok := pp[user]; !ok { return fmt.Errorf("user %s does not exist", user) } alg, err := identifyHash(pp[user]) if err != nil { return fmt.Errorf("cannot identify algo %v", alg) } return alg.Match(passwd, pp[user]) } // WriteFile will write the Passwds object to the given file func (pp Passwds) WriteFile(file string) error { return os.WriteFile(file, pp.Bytes(), os.ModePerm) } // Bytes will return the Passwd as a byte slice func (pp Passwds) Bytes() []byte { pass := []byte{} for name, hash := range pp { pass = append(pass, []byte(name+":"+hash+"\n")...) } return pass } func identifyHash(h string) (Hasher, error) { switch { case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"), strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"): return new(Bcrypt), nil case strings.HasPrefix(h, "$apr1$"): return new(Apr1), nil case strings.HasPrefix(h, "{SHA}"): return new(Sha), nil case strings.HasPrefix(h, "{SSHA}"): return new(Ssha), nil case strings.HasPrefix(h, "$5$"): return new(Sha256), nil case strings.HasPrefix(h, "$6$"): return new(Sha512), nil } return nil, fmt.Errorf("unsupported hash algorithm") } func validLine(parts []string, lineNumber int, line string) (bool, error) { var err error if len(parts) != 2 { err = errors.New(fmt.Sprintln("invalid line", lineNumber+1, "unexpected number of parts split by", ":", len(parts), "instead of 2 in\"", line, "\"")) return false, err } return true, nil } func trimParts(parts []string) []string { for i, part := range parts { parts[i] = strings.Trim(part, " ") } return parts }