|
|
|
// Package htpasswd contains utilities for manipulating .htpasswd files
|
|
|
|
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"
|
|
|
|
)
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
|
|
|
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 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 {
|
|
|
|
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
|
|
|
|
case strings.HasPrefix(h, "$5$"):
|
|
|
|
return HashSHA256, nil
|
|
|
|
case strings.HasPrefix(h, "$6$"):
|
|
|
|
return HashSHA512, 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)
|
|
|
|
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
|
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|