0fb985026f
Signed-off-by: Alejandro Mery <amery@jpi.io>
235 lines
5.0 KiB
Go
235 lines
5.0 KiB
Go
// Package htpasswd contains utilities for manipulating .htpasswd files
|
|
package htpasswd
|
|
|
|
import (
|
|
"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.TrimSpace(line)
|
|
if len(line) == 0 {
|
|
// skipping empty lines
|
|
continue
|
|
}
|
|
|
|
user, password, err := splitLine(line, lineNumber)
|
|
if err != nil {
|
|
return passwords, err
|
|
}
|
|
|
|
_, exists := passwords[user]
|
|
if exists {
|
|
err = &UserError{
|
|
Name: user,
|
|
Err: ErrExists,
|
|
}
|
|
return passwords, err
|
|
}
|
|
|
|
passwords[user] = password
|
|
}
|
|
|
|
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 &UserError{
|
|
Name: user,
|
|
Err: ErrExists,
|
|
}
|
|
}
|
|
|
|
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 &UserError{
|
|
Name: user,
|
|
Err: ErrNotExists,
|
|
}
|
|
}
|
|
|
|
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 &UserError{
|
|
Name: user,
|
|
Err: ErrNotExists,
|
|
}
|
|
}
|
|
|
|
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 &UserError{
|
|
Name: user,
|
|
Err: ErrNotExists,
|
|
}
|
|
}
|
|
|
|
alg := identifyHash(pp[user])
|
|
if alg == nil {
|
|
return &UserError{
|
|
Name: user,
|
|
Err: ErrInvalidAlgorithm,
|
|
}
|
|
}
|
|
|
|
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 {
|
|
switch {
|
|
case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"),
|
|
strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"):
|
|
return new(Bcrypt)
|
|
case strings.HasPrefix(h, "$apr1$"):
|
|
return new(Apr1)
|
|
case strings.HasPrefix(h, "{SHA}"):
|
|
return new(Sha)
|
|
case strings.HasPrefix(h, "{SSHA}"):
|
|
return new(Ssha)
|
|
case strings.HasPrefix(h, "$5$"):
|
|
return new(Sha256)
|
|
case strings.HasPrefix(h, "$6$"):
|
|
return new(Sha512)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
user = strings.TrimSpace(user)
|
|
password = strings.TrimSpace(password)
|
|
|
|
if h := identifyHash(password); h != nil {
|
|
return "", "", fmt.Errorf("invalid algorithm on line %v", lineNumber+1)
|
|
}
|
|
|
|
return user, password, nil
|
|
}
|