|
|
|
@ -2,41 +2,22 @@
|
|
|
|
|
package htpasswd |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"bytes" |
|
|
|
|
"crypto/rand" |
|
|
|
|
"crypto/sha1" |
|
|
|
|
"crypto/sha256" |
|
|
|
|
"crypto/sha512" |
|
|
|
|
"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" |
|
|
|
|
// HashSHA256 256 variant of SHA
|
|
|
|
|
HashSHA256 HashAlgorithm = "sha256" |
|
|
|
|
// HashSHA512 512 variant of SHA
|
|
|
|
|
HashSHA512 HashAlgorithm = "sha512" |
|
|
|
|
) |
|
|
|
|
// Hasher interface implemented by hash algos
|
|
|
|
|
type Hasher interface { |
|
|
|
|
Hash(password string) (string, error) |
|
|
|
|
Match(password, hashedPassword string) error |
|
|
|
|
Name() string |
|
|
|
|
Prefix() string |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ParseHtpasswdFile parses a .htpasswd file
|
|
|
|
|
// and returns a Passwd type
|
|
|
|
@ -79,58 +60,60 @@ func ParseHtpasswd(htpasswdBytes []byte) (Passwds, error) {
|
|
|
|
|
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 { |
|
|
|
|
func CreateUser(file, user, passwd string, algo Hasher) error { |
|
|
|
|
pp, err := ParseHtpasswdFile(file) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
err = pp.CreateUser(user, passwd, algo) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
return pp.Write(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 := createHash(passwd, algo) |
|
|
|
|
h, err := algo.Hash(passwd) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
pp[user] = h |
|
|
|
|
return os.WriteFile(file, pp.toByte(), os.ModePerm) |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// UpdateUser will update the password for the named user
|
|
|
|
|
// in the named file
|
|
|
|
|
func UpdateUser(file, user, passwd string, algo HashAlgorithm) error { |
|
|
|
|
func UpdateUser(file, user, passwd string, algo Hasher) error { |
|
|
|
|
pp, err := ParseHtpasswdFile(file) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
err = pp.UpdateUser(user, passwd, algo) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
return pp.Write(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 := createHash(passwd, algo) |
|
|
|
|
h, err := algo.Hash(passwd) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
pp[user] = h |
|
|
|
|
return os.WriteFile(file, pp.toByte(), os.ModePerm) |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// DeleteUser deletes the named user from the named file
|
|
|
|
@ -139,12 +122,22 @@ func DeleteUser(file, user string) error {
|
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
err = pp.DeleteUser(user) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return pp.Write(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 os.WriteFile(file, pp.toByte(), os.ModePerm) |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// VerifyUser will check if the given user and password are matching
|
|
|
|
@ -154,12 +147,12 @@ func VerifyUser(file, user, passwd string) error {
|
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
return pp.Verify(user, passwd) |
|
|
|
|
return pp.VerifyUser(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 { |
|
|
|
|
// 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) |
|
|
|
|
} |
|
|
|
@ -167,132 +160,16 @@ func (pp Passwds) Verify(user, passwd string) error {
|
|
|
|
|
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) |
|
|
|
|
case HashSHA256: |
|
|
|
|
return verifySHA256(pass, hash) |
|
|
|
|
case HashSHA512: |
|
|
|
|
return verifySHA512(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") |
|
|
|
|
return alg.Match(passwd, pp[user]) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
salt := hash[len(hash)-4:] |
|
|
|
|
sha := sha1.New() |
|
|
|
|
_, err = sha.Write([]byte(password)) |
|
|
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
// Write will cwrite the Passwd object to the given file
|
|
|
|
|
func (pp Passwds) Write(file string) error { |
|
|
|
|
return os.WriteFile(file, pp.Bytes(), os.ModePerm) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
_, 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 verifySHA256(password, hashedPassword string) error { |
|
|
|
|
eppS := hashedPassword[3:] |
|
|
|
|
hash, err := base64.StdEncoding.DecodeString(eppS) |
|
|
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
return fmt.Errorf("cannot base64 decode") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
sha := sha256.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 verifySHA512(password, hashedPassword string) error { |
|
|
|
|
eppS := hashedPassword[3:] |
|
|
|
|
hash, err := base64.StdEncoding.DecodeString(eppS) |
|
|
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
return fmt.Errorf("cannot base64 decode") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
sha := sha512.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 (pp Passwds) toByte() []byte { |
|
|
|
|
// 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")...) |
|
|
|
@ -300,113 +177,40 @@ func (pp Passwds) toByte() []byte {
|
|
|
|
|
return pass |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func identifyHash(h string) (HashAlgorithm, error) { |
|
|
|
|
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 HashBCrypt, nil |
|
|
|
|
return new(Bcrypt), nil |
|
|
|
|
case strings.HasPrefix(h, "$apr1$"): |
|
|
|
|
return HashAPR1, nil |
|
|
|
|
return new(Apr1), nil |
|
|
|
|
case strings.HasPrefix(h, "{SHA}"): |
|
|
|
|
return HashSHA, nil |
|
|
|
|
return new(Sha), nil |
|
|
|
|
case strings.HasPrefix(h, "{SSHA}"): |
|
|
|
|
return HashSSHA, nil |
|
|
|
|
return new(Ssha), nil |
|
|
|
|
case strings.HasPrefix(h, "$5$"): |
|
|
|
|
return HashSHA256, nil |
|
|
|
|
return new(Sha256), nil |
|
|
|
|
case strings.HasPrefix(h, "$6$"): |
|
|
|
|
return HashSHA512, nil |
|
|
|
|
return new(Sha512), nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return "", fmt.Errorf("unsupported hash algorithm") |
|
|
|
|
return nil, fmt.Errorf("unsupported hash algorithm") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func createHash(passwd string, algo HashAlgorithm) (string, error) { |
|
|
|
|
hash := "" |
|
|
|
|
prefix := "" |
|
|
|
|
func validLine(parts []string, lineNumber int, line string) (bool, error) { |
|
|
|
|
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) |
|
|
|
|
case HashSHA256: |
|
|
|
|
prefix = "$5$" |
|
|
|
|
hash, err = hashSha256(passwd) |
|
|
|
|
case HashSHA512: |
|
|
|
|
prefix = "$6$" |
|
|
|
|
hash, err = hashSha512(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 hashSha256(passwd string) (string, error) { |
|
|
|
|
s := sha256.New() |
|
|
|
|
_, err := s.Write([]byte(passwd)) |
|
|
|
|
if err != nil { |
|
|
|
|
return "", err |
|
|
|
|
} |
|
|
|
|
passwordSum := []byte(s.Sum(nil)) |
|
|
|
|
return base64.StdEncoding.EncodeToString(passwordSum), nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func hashSha512(passwd string) (string, error) { |
|
|
|
|
s := sha512.New() |
|
|
|
|
_, err := s.Write([]byte(passwd)) |
|
|
|
|
if err != nil { |
|
|
|
|
return "", err |
|
|
|
|
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 |
|
|
|
|
} |
|
|
|
|
passwordSum := []byte(s.Sum(nil)) |
|
|
|
|
return base64.StdEncoding.EncodeToString(passwordSum), nil |
|
|
|
|
return true, 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 |
|
|
|
|
func trimParts(parts []string) []string { |
|
|
|
|
for i, part := range parts { |
|
|
|
|
parts[i] = strings.Trim(part, " ") |
|
|
|
|
} |
|
|
|
|
passwordSum := []byte(s.Sum(nil)) |
|
|
|
|
ret := append(passwordSum, salt...) |
|
|
|
|
return base64.StdEncoding.EncodeToString(ret), nil |
|
|
|
|
return parts |
|
|
|
|
} |
|
|
|
|