Files

235 lines
5.0 KiB
Go
Raw Permalink Normal View History

// Package htpasswd contains utilities for manipulating .htpasswd files
package htpasswd
import (
"fmt"
"os"
"strings"
)
// Passwds name => hash
type Passwds map[string]string
2023-09-27 18:23:28 +03:00
// Hasher interface implemented by hash algos
type Hasher interface {
Hash(password string) (string, error)
Match(password, hashedPassword string) error
Name() string
Prefix() string
}
2023-09-29 18:40:53 +00:00
// ParseFile parses a .htpasswd file
// and returns a Passwd type
2023-09-29 18:40:53 +00:00
func ParseFile(file string) (Passwds, error) {
htpasswdBytes, err := os.ReadFile(file)
if err != nil {
return nil, err
}
2023-09-29 18:40:53 +00:00
return Parse(htpasswdBytes)
}
2023-09-29 18:40:53 +00:00
// 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 {
2023-09-29 19:14:05 +00:00
line = strings.TrimSpace(line)
if len(line) == 0 {
// skipping empty lines
continue
}
2023-09-29 19:14:05 +00:00
user, password, err := splitLine(line, lineNumber)
if err != nil {
return passwords, err
}
2023-09-29 19:14:05 +00:00
_, exists := passwords[user]
if exists {
2023-09-29 18:52:56 +00:00
err = &UserError{
2023-09-29 19:14:05 +00:00
Name: user,
2023-09-29 18:52:56 +00:00
Err: ErrExists,
}
return passwords, err
}
2023-09-29 19:14:05 +00:00
passwords[user] = password
}
2023-09-29 19:14:05 +00:00
return passwords, err
}
// CreateUser creates a record in the named file with
// the named password and hash algorithm
2023-09-27 18:23:28 +03:00
func CreateUser(file, user, passwd string, algo Hasher) error {
2023-09-29 18:40:53 +00:00
pp, err := ParseFile(file)
if err != nil {
return err
}
2023-09-27 18:23:28 +03:00
err = pp.CreateUser(user, passwd, algo)
if err != nil {
return err
}
2023-09-29 18:40:53 +00:00
return pp.WriteFile(file)
2023-09-27 18:23:28 +03:00
}
// 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 {
2023-09-29 18:52:56 +00:00
return &UserError{
Name: user,
Err: ErrExists,
}
}
2023-09-29 18:52:56 +00:00
2023-09-27 18:23:28 +03:00
h, err := algo.Hash(passwd)
if err != nil {
return err
}
pp[user] = h
2023-09-27 18:23:28 +03:00
return nil
}
// UpdateUser will update the password for the named user
// in the named file
2023-09-27 18:23:28 +03:00
func UpdateUser(file, user, passwd string, algo Hasher) error {
2023-09-29 18:40:53 +00:00
pp, err := ParseFile(file)
if err != nil {
return err
}
2023-09-27 18:23:28 +03:00
err = pp.UpdateUser(user, passwd, algo)
if err != nil {
return err
}
2023-09-29 18:40:53 +00:00
return pp.WriteFile(file)
2023-09-27 18:23:28 +03:00
}
// 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 {
2023-09-29 18:52:56 +00:00
return &UserError{
Name: user,
Err: ErrNotExists,
}
}
2023-09-29 18:52:56 +00:00
2023-09-27 18:23:28 +03:00
h, err := algo.Hash(passwd)
if err != nil {
return err
}
pp[user] = h
2023-09-27 18:23:28 +03:00
return nil
}
// DeleteUser deletes the named user from the named file
func DeleteUser(file, user string) error {
2023-09-29 18:40:53 +00:00
pp, err := ParseFile(file)
if err != nil {
return err
}
2023-09-27 18:23:28 +03:00
err = pp.DeleteUser(user)
if err != nil {
return err
}
2023-09-29 18:40:53 +00:00
return pp.WriteFile(file)
2023-09-27 18:23:28 +03:00
}
// DeleteUser deletes the named user from the named file
func (pp Passwds) DeleteUser(user string) error {
if _, exists := pp[user]; !exists {
2023-09-29 18:52:56 +00:00
return &UserError{
Name: user,
Err: ErrNotExists,
}
}
delete(pp, user)
2023-09-27 18:23:28 +03:00
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 {
2023-09-29 18:40:53 +00:00
pp, err := ParseFile(file)
if err != nil {
return err
}
2023-09-27 18:23:28 +03:00
return pp.VerifyUser(user, passwd)
}
2023-09-27 18:23:28 +03:00
// 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 {
2023-09-29 18:52:56 +00:00
return &UserError{
Name: user,
Err: ErrNotExists,
}
}
2023-09-29 18:52:56 +00:00
alg := identifyHash(pp[user])
if alg == nil {
return &UserError{
Name: user,
Err: ErrInvalidAlgorithm,
}
}
2023-09-29 18:52:56 +00:00
2023-09-27 18:23:28 +03:00
return alg.Match(passwd, pp[user])
}
2023-09-29 18:40:53 +00:00
// WriteFile will write the Passwds object to the given file
func (pp Passwds) WriteFile(file string) error {
2023-09-27 18:23:28 +03:00
return os.WriteFile(file, pp.Bytes(), os.ModePerm)
}
2023-09-27 18:23:28 +03:00
// 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
}
2023-09-29 18:52:56 +00:00
func identifyHash(h string) Hasher {
switch {
case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"),
strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"):
2023-09-29 18:52:56 +00:00
return new(Bcrypt)
case strings.HasPrefix(h, "$apr1$"):
2023-09-29 18:52:56 +00:00
return new(Apr1)
case strings.HasPrefix(h, "{SHA}"):
2023-09-29 18:52:56 +00:00
return new(Sha)
case strings.HasPrefix(h, "{SSHA}"):
2023-09-29 18:52:56 +00:00
return new(Ssha)
case strings.HasPrefix(h, "$5$"):
2023-09-29 18:52:56 +00:00
return new(Sha256)
case strings.HasPrefix(h, "$6$"):
2023-09-29 18:52:56 +00:00
return new(Sha512)
default:
return nil
}
}
2023-09-29 19:14:05 +00:00
func splitLine(line string, lineNumber int) (user, password string, err error) {
user, password, ok := strings.Cut(line, ":")
if !ok {
return "", "", fmt.Errorf("invalid line %v", lineNumber+1)
}
2023-09-29 19:14:05 +00:00
user = strings.TrimSpace(user)
password = strings.TrimSpace(password)
if h := identifyHash(password); h != nil {
return "", "", fmt.Errorf("invalid algorithm on line %v", lineNumber+1)
}
2023-09-29 19:14:05 +00:00
return user, password, nil
}