// Package htpasswd contains utilities for manipulating .htpasswd files package htpasswd import ( "bytes" "crypto/rand" "crypto/sha1" "encoding/base64" "errors" "fmt" "os" "strings" "github.com/GehirnInc/crypt/apr1_crypt" "golang.org/x/crypto/bcrypt" ) // Passwds name => hash type Passwds map[string]string // HashAlgorithm enum for hashing algorithms type HashAlgorithm string const ( // HashAPR1 Apache MD5 crypt HashAPR1 HashAlgorithm = "apr1" // HashBCrypt bcrypt HashBCrypt HashAlgorithm = "bcrypt" // HashSHA SHA HashSHA HashAlgorithm = "sha" // HashSSHA Salted SHA HashSSHA HashAlgorithm = "ssha" ) // ParseHtpasswdFile parses a .htpasswd file // and returns a Passwd type func ParseHtpasswdFile(file string) (Passwds, error) { htpasswdBytes, err := os.ReadFile(file) if err != nil { return nil, err } return ParseHtpasswd(htpasswdBytes) } // ParseHtpasswd parses a slice of bytes in htpasswd style func ParseHtpasswd(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 htpasswords file - user " + parts[0] + " defined more than once") return passwords, err } passwords[parts[0]] = parts[1] } return passwords, err } 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 } // CreateUser creates a record in the named file with // the named password and hash algorithm func CreateUser(file, user, passwd string, algo HashAlgorithm) error { pp, err := ParseHtpasswdFile(file) if err != nil { return err } if _, exists := pp[user]; exists { return fmt.Errorf("user %s already exists", user) } h, err := createHash(passwd, algo) if err != nil { return err } pp[user] = h return os.WriteFile(file, pp.toByte(), os.ModePerm) } // UpdateUser will update the password for the named user // in the named file func UpdateUser(file, user, passwd string, algo HashAlgorithm) error { pp, err := ParseHtpasswdFile(file) if err != nil { return err } if _, exists := pp[user]; !exists { return fmt.Errorf("user %s does not exist", user) } h, err := createHash(passwd, algo) if err != nil { return err } pp[user] = h return os.WriteFile(file, pp.toByte(), os.ModePerm) } // DeleteUser deletes the named user from the named file func DeleteUser(file, user string) error { pp, err := ParseHtpasswdFile(file) if err != nil { return err } if _, exists := pp[user]; !exists { return fmt.Errorf("user %s does not exist", user) } delete(pp, user) return os.WriteFile(file, pp.toByte(), os.ModePerm) } // 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 := ParseHtpasswdFile(file) if err != nil { return err } return pp.Verify(user, passwd) } // Verify will check if the given user and password are matching // with the given Passwd object content func (pp Passwds) Verify(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 verifyPass(passwd, pp[user], alg) } func verifyPass(pass, hash string, alg HashAlgorithm) error { switch alg { case HashAPR1: return verifyAPR1(pass, hash) case HashBCrypt: return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)) case HashSHA: return verifySHA(pass, hash) case HashSSHA: return verifySSHA(pass, hash) } return fmt.Errorf("unsupported hash algorithm %v", alg) } func verifyAPR1(password, hashedPassword string) error { return apr1_crypt.New().Verify(hashedPassword, []byte(password)) } func verifySHA(password, hashedPassword string) error { eppS := hashedPassword[5:] hash, err := base64.StdEncoding.DecodeString(eppS) if err != nil { return fmt.Errorf("cannot base64 decode") } sha := sha1.New() _, err = sha.Write([]byte(password)) if err != nil { return err } sum := sha.Sum(nil) if !bytes.Equal(sum, hash) { return fmt.Errorf("wrong password") } return nil } func verifySSHA(password, hashedPassword string) error { eppS := hashedPassword[6:] hash, err := base64.StdEncoding.DecodeString(eppS) if err != nil { return fmt.Errorf("cannot base64 decode") } salt := hash[len(hash)-4:] sha := sha1.New() _, err = sha.Write([]byte(password)) if err != nil { return err } _, err = sha.Write(salt) if err != nil { return err } sum := sha.Sum(nil) if !bytes.Equal(sum, hash[:len(hash)-4]) { return fmt.Errorf("wrong password") } return nil } func (pp Passwds) toByte() []byte { pass := []byte{} for name, hash := range pp { pass = append(pass, []byte(name+":"+hash+"\n")...) } return pass } func identifyHash(h string) (HashAlgorithm, error) { switch { case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"), strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"): return HashBCrypt, nil case strings.HasPrefix(h, "$apr1$"): return HashAPR1, nil case strings.HasPrefix(h, "{SHA}"): return HashSHA, nil case strings.HasPrefix(h, "{SSHA}"): return HashSSHA, nil } return "", fmt.Errorf("unsupported hash algorithm") } func createHash(passwd string, algo HashAlgorithm) (string, error) { hash := "" prefix := "" var err error switch algo { case HashAPR1: hash, err = hashApr1(passwd) case HashBCrypt: hash, err = hashBcrypt(passwd) case HashSHA: prefix = "{SHA}" hash, err = hashSha(passwd) case HashSSHA: prefix = "{SSHA}" hash, err = hashSSha(passwd) } if err != nil { return "", err } return prefix + hash, nil } func hashApr1(passwd string) (string, error) { return apr1_crypt.New().Generate([]byte(passwd), nil) } func hashBcrypt(passwd string) (string, error) { passwordBytes, err := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost) if err != nil { return "", err } return string(passwordBytes), nil } func hashSha(passwd string) (string, error) { s := sha1.New() _, err := s.Write([]byte(passwd)) if err != nil { return "", err } passwordSum := []byte(s.Sum(nil)) return base64.StdEncoding.EncodeToString(passwordSum), nil } func hashSSha(passwd string) (string, error) { s := sha1.New() _, err := s.Write([]byte(passwd)) if err != nil { return "", err } salt := make([]byte, 4) _, err = rand.Read(salt) if err != nil { return "", err } _, err = s.Write([]byte(salt)) if err != nil { return "", err } passwordSum := []byte(s.Sum(nil)) ret := append(passwordSum, salt...) return base64.StdEncoding.EncodeToString(ret), nil }