Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02235c36bc | |||
| 7b8b0e6700 | |||
| 5941fb013c | |||
| 47d0d5d0a4 | |||
| afac3a836f |
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"hasher",
|
||||||
|
"htpasswd",
|
||||||
|
"Passwds"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
module asciigoat.org/httools
|
module asciigoat.org/httools
|
||||||
|
|
||||||
go 1.21.1
|
go 1.20
|
||||||
|
|
||||||
require github.com/mgechev/revive v1.3.3
|
require github.com/mgechev/revive v1.3.3
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package htpasswd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrExists indicates the specified user already exists
|
||||||
|
ErrExists = errors.New("user already exists")
|
||||||
|
// ErrNotExists indicates the specified user does not exist
|
||||||
|
ErrNotExists = errors.New("user does not exist")
|
||||||
|
// ErrInvalidAlgorithm indicates the htpasswd entry doesn't contain a
|
||||||
|
// valid algorithm signature
|
||||||
|
ErrInvalidAlgorithm = errors.New("invalid algorithm")
|
||||||
|
// ErrInvalidPassword indicates the offered password doesn't match
|
||||||
|
ErrInvalidPassword = errors.New("invalid password")
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserError indicates an error associated to the specified user
|
||||||
|
type UserError struct {
|
||||||
|
Name string
|
||||||
|
Hint string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserError) Error() string {
|
||||||
|
var buf strings.Builder
|
||||||
|
var reason string
|
||||||
|
|
||||||
|
if e.Hint != "" {
|
||||||
|
reason = e.Hint
|
||||||
|
} else {
|
||||||
|
reason = e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Name == "" {
|
||||||
|
return reason
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = buf.WriteString(e.Name)
|
||||||
|
_, _ = buf.WriteString(": ")
|
||||||
|
_, _ = buf.WriteString(reason)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
+53
-32
@@ -19,18 +19,18 @@ type Hasher interface {
|
|||||||
Prefix() string
|
Prefix() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseHtpasswdFile parses a .htpasswd file
|
// ParseFile parses a .htpasswd file
|
||||||
// and returns a Passwd type
|
// and returns a Passwd type
|
||||||
func ParseHtpasswdFile(file string) (Passwds, error) {
|
func ParseFile(file string) (Passwds, error) {
|
||||||
htpasswdBytes, err := os.ReadFile(file)
|
htpasswdBytes, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ParseHtpasswd(htpasswdBytes)
|
return Parse(htpasswdBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseHtpasswd parses a slice of bytes in htpasswd style
|
// Parse parses a slice of bytes in htpasswd style
|
||||||
func ParseHtpasswd(htpasswdBytes []byte) (Passwds, error) {
|
func Parse(htpasswdBytes []byte) (Passwds, error) {
|
||||||
lines := strings.Split(string(htpasswdBytes), "\n")
|
lines := strings.Split(string(htpasswdBytes), "\n")
|
||||||
passwords := make(map[string]string)
|
passwords := make(map[string]string)
|
||||||
var err error
|
var err error
|
||||||
@@ -51,8 +51,10 @@ func ParseHtpasswd(htpasswdBytes []byte) (Passwds, error) {
|
|||||||
_, exists := passwords[parts[0]]
|
_, exists := passwords[parts[0]]
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
err = errors.New("invalid htpasswords file - user " +
|
err = &UserError{
|
||||||
parts[0] + " defined more than once")
|
Name: parts[0],
|
||||||
|
Err: ErrExists,
|
||||||
|
}
|
||||||
return passwords, err
|
return passwords, err
|
||||||
}
|
}
|
||||||
passwords[parts[0]] = parts[1]
|
passwords[parts[0]] = parts[1]
|
||||||
@@ -63,7 +65,7 @@ func ParseHtpasswd(htpasswdBytes []byte) (Passwds, error) {
|
|||||||
// CreateUser creates a record in the named file with
|
// CreateUser creates a record in the named file with
|
||||||
// the named password and hash algorithm
|
// the named password and hash algorithm
|
||||||
func CreateUser(file, user, passwd string, algo Hasher) error {
|
func CreateUser(file, user, passwd string, algo Hasher) error {
|
||||||
pp, err := ParseHtpasswdFile(file)
|
pp, err := ParseFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -71,15 +73,19 @@ func CreateUser(file, user, passwd string, algo Hasher) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return pp.Write(file)
|
return pp.WriteFile(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser will create a new user in the given Passwd object
|
// CreateUser will create a new user in the given Passwd object
|
||||||
// using the given name, password and hashing algorithm
|
// using the given name, password and hashing algorithm
|
||||||
func (pp Passwds) CreateUser(user, passwd string, algo Hasher) error {
|
func (pp Passwds) CreateUser(user, passwd string, algo Hasher) error {
|
||||||
if _, exists := pp[user]; exists {
|
if _, exists := pp[user]; exists {
|
||||||
return fmt.Errorf("user %s already exists", user)
|
return &UserError{
|
||||||
|
Name: user,
|
||||||
|
Err: ErrExists,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h, err := algo.Hash(passwd)
|
h, err := algo.Hash(passwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -91,7 +97,7 @@ func (pp Passwds) CreateUser(user, passwd string, algo Hasher) error {
|
|||||||
// UpdateUser will update the password for the named user
|
// UpdateUser will update the password for the named user
|
||||||
// in the named file
|
// in the named file
|
||||||
func UpdateUser(file, user, passwd string, algo Hasher) error {
|
func UpdateUser(file, user, passwd string, algo Hasher) error {
|
||||||
pp, err := ParseHtpasswdFile(file)
|
pp, err := ParseFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -99,15 +105,19 @@ func UpdateUser(file, user, passwd string, algo Hasher) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return pp.Write(file)
|
return pp.WriteFile(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser will update the password for the named user
|
// UpdateUser will update the password for the named user
|
||||||
// using the given name, password and hashing algorithm
|
// using the given name, password and hashing algorithm
|
||||||
func (pp Passwds) UpdateUser(user, passwd string, algo Hasher) error {
|
func (pp Passwds) UpdateUser(user, passwd string, algo Hasher) error {
|
||||||
if _, exists := pp[user]; !exists {
|
if _, exists := pp[user]; !exists {
|
||||||
return fmt.Errorf("user %s does not exist", user)
|
return &UserError{
|
||||||
|
Name: user,
|
||||||
|
Err: ErrNotExists,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h, err := algo.Hash(passwd)
|
h, err := algo.Hash(passwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -118,7 +128,7 @@ func (pp Passwds) UpdateUser(user, passwd string, algo Hasher) error {
|
|||||||
|
|
||||||
// DeleteUser deletes the named user from the named file
|
// DeleteUser deletes the named user from the named file
|
||||||
func DeleteUser(file, user string) error {
|
func DeleteUser(file, user string) error {
|
||||||
pp, err := ParseHtpasswdFile(file)
|
pp, err := ParseFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -127,13 +137,16 @@ func DeleteUser(file, user string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return pp.Write(file)
|
return pp.WriteFile(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser deletes the named user from the named file
|
// DeleteUser deletes the named user from the named file
|
||||||
func (pp Passwds) DeleteUser(user string) error {
|
func (pp Passwds) DeleteUser(user string) error {
|
||||||
if _, exists := pp[user]; !exists {
|
if _, exists := pp[user]; !exists {
|
||||||
return fmt.Errorf("user %s does not exist", user)
|
return &UserError{
|
||||||
|
Name: user,
|
||||||
|
Err: ErrNotExists,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(pp, user)
|
delete(pp, user)
|
||||||
@@ -143,7 +156,7 @@ func (pp Passwds) DeleteUser(user string) error {
|
|||||||
// VerifyUser will check if the given user and password are matching
|
// VerifyUser will check if the given user and password are matching
|
||||||
// with the content of the given file
|
// with the content of the given file
|
||||||
func VerifyUser(file, user, passwd string) error {
|
func VerifyUser(file, user, passwd string) error {
|
||||||
pp, err := ParseHtpasswdFile(file)
|
pp, err := ParseFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -154,17 +167,25 @@ func VerifyUser(file, user, passwd string) error {
|
|||||||
// with the given Passwd object
|
// with the given Passwd object
|
||||||
func (pp Passwds) VerifyUser(user, passwd string) error {
|
func (pp Passwds) VerifyUser(user, passwd string) error {
|
||||||
if _, ok := pp[user]; !ok {
|
if _, ok := pp[user]; !ok {
|
||||||
return fmt.Errorf("user %s does not exist", user)
|
return &UserError{
|
||||||
|
Name: user,
|
||||||
|
Err: ErrNotExists,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
alg, err := identifyHash(pp[user])
|
|
||||||
if err != nil {
|
alg := identifyHash(pp[user])
|
||||||
return fmt.Errorf("cannot identify algo %v", alg)
|
if alg == nil {
|
||||||
|
return &UserError{
|
||||||
|
Name: user,
|
||||||
|
Err: ErrInvalidAlgorithm,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return alg.Match(passwd, pp[user])
|
return alg.Match(passwd, pp[user])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write will cwrite the Passwd object to the given file
|
// WriteFile will write the Passwds object to the given file
|
||||||
func (pp Passwds) Write(file string) error {
|
func (pp Passwds) WriteFile(file string) error {
|
||||||
return os.WriteFile(file, pp.Bytes(), os.ModePerm)
|
return os.WriteFile(file, pp.Bytes(), os.ModePerm)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,24 +198,24 @@ func (pp Passwds) Bytes() []byte {
|
|||||||
return pass
|
return pass
|
||||||
}
|
}
|
||||||
|
|
||||||
func identifyHash(h string) (Hasher, error) {
|
func identifyHash(h string) Hasher {
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"),
|
case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"),
|
||||||
strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"):
|
strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"):
|
||||||
return new(Bcrypt), nil
|
return new(Bcrypt)
|
||||||
case strings.HasPrefix(h, "$apr1$"):
|
case strings.HasPrefix(h, "$apr1$"):
|
||||||
return new(Apr1), nil
|
return new(Apr1)
|
||||||
case strings.HasPrefix(h, "{SHA}"):
|
case strings.HasPrefix(h, "{SHA}"):
|
||||||
return new(Sha), nil
|
return new(Sha)
|
||||||
case strings.HasPrefix(h, "{SSHA}"):
|
case strings.HasPrefix(h, "{SSHA}"):
|
||||||
return new(Ssha), nil
|
return new(Ssha)
|
||||||
case strings.HasPrefix(h, "$5$"):
|
case strings.HasPrefix(h, "$5$"):
|
||||||
return new(Sha256), nil
|
return new(Sha256)
|
||||||
case strings.HasPrefix(h, "$6$"):
|
case strings.HasPrefix(h, "$6$"):
|
||||||
return new(Sha512), nil
|
return new(Sha512)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("unsupported hash algorithm")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validLine(parts []string, lineNumber int, line string) (bool, error) {
|
func validLine(parts []string, lineNumber int, line string) (bool, error) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestParseHtpasswd(t *testing.T) {
|
func TestParseHtpasswd(t *testing.T) {
|
||||||
passwords, err := ParseHtpasswd([]byte("sha:{SHA}IRRjboXT92QSYXm8lpGPCZUvU1E=\n"))
|
passwords, err := Parse([]byte("sha:{SHA}IRRjboXT92QSYXm8lpGPCZUvU1E=\n"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ func TestVerifyPassword(t *testing.T) {
|
|||||||
TestCreateUser(t)
|
TestCreateUser(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = ParseHtpasswdFile(f.Name())
|
_, err = ParseFile(f.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ func TestVerifyUser(t *testing.T) {
|
|||||||
TestCreateUser(t)
|
TestCreateUser(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
pp, err := ParseHtpasswdFile(f.Name())
|
pp, err := ParseFile(f.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user