Browse Source

htpasswd: refactor and add Passwd methods

Signed-off-by: Nagy Károly Gábriel <k@jpi.io>
parent
commit
56008698b2
Signed by: karasz
GPG Key ID: FA002E83BC206F83
  1. 323
      htpasswd/htpasswd.go
  2. 25
      htpasswd/htpasswd_test.go
  3. 10
      htpasswd/htpasswd_testdata

323
htpasswd/htpasswd.go

@ -79,24 +79,6 @@ func ParseHtpasswd(htpasswdBytes []byte) (Passwds, error) {
return passwords, err 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 // 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 HashAlgorithm) error { func CreateUser(file, user, passwd string, algo HashAlgorithm) error {
@ -104,15 +86,25 @@ func CreateUser(file, user, passwd string, algo HashAlgorithm) error {
if err != nil { if err != nil {
return err return err
} }
newpp, err := pp.CreateUser(user, passwd, algo)
if err != nil {
return err
}
return newpp.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 HashAlgorithm) (Passwds, error) {
if _, exists := pp[user]; exists { if _, exists := pp[user]; exists {
return fmt.Errorf("user %s already exists", user) return nil, fmt.Errorf("user %s already exists", user)
} }
h, err := createHash(passwd, algo) h, err := createHash(passwd, algo)
if err != nil { if err != nil {
return err return nil, err
} }
pp[user] = h pp[user] = h
return os.WriteFile(file, pp.toByte(), os.ModePerm) return pp, nil
} }
// UpdateUser will update the password for the named user // UpdateUser will update the password for the named user
@ -122,15 +114,25 @@ func UpdateUser(file, user, passwd string, algo HashAlgorithm) error {
if err != nil { if err != nil {
return err return err
} }
newpp, err := pp.UpdateUser(user, passwd, algo)
if err != nil {
return err
}
return newpp.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 HashAlgorithm) (Passwds, error) {
if _, exists := pp[user]; !exists { if _, exists := pp[user]; !exists {
return fmt.Errorf("user %s does not exist", user) return nil, fmt.Errorf("user %s does not exist", user)
} }
h, err := createHash(passwd, algo) h, err := createHash(passwd, algo)
if err != nil { if err != nil {
return err return nil, err
} }
pp[user] = h pp[user] = h
return os.WriteFile(file, pp.toByte(), os.ModePerm) return pp, nil
} }
// DeleteUser deletes the named user from the named file // DeleteUser deletes the named user from the named file
@ -139,12 +141,22 @@ func DeleteUser(file, user string) error {
if err != nil { if err != nil {
return err return err
} }
newpp, err := pp.DeleteUser(user)
if err != nil {
return err
}
return newpp.Write(file)
}
// DeleteUser deletes the named user from the named file
func (pp Passwds) DeleteUser(user string) (Passwds, error) {
if _, exists := pp[user]; !exists { if _, exists := pp[user]; !exists {
return fmt.Errorf("user %s does not exist", user) return nil, fmt.Errorf("user %s does not exist", user)
} }
delete(pp, user) delete(pp, user)
return os.WriteFile(file, pp.toByte(), os.ModePerm) return pp, nil
} }
// VerifyUser will check if the given user and password are matching // VerifyUser will check if the given user and password are matching
@ -154,12 +166,12 @@ func VerifyUser(file, user, passwd string) error {
if err != nil { if err != nil {
return err return err
} }
return pp.Verify(user, passwd) return pp.VerifyUser(user, passwd)
} }
// Verify will check if the given user and password are matching // VerifyUser will check if the given user and password are matching
// with the given Passwd object content // with the given Passwd object
func (pp Passwds) Verify(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 fmt.Errorf("user %s does not exist", user)
} }
@ -170,6 +182,49 @@ func (pp Passwds) Verify(user, passwd string) error {
return verifyPass(passwd, pp[user], alg) return verifyPass(passwd, pp[user], alg)
} }
// Write will cwrite the Passwd object to the given file
func (pp Passwds) Write(file string) error {
return os.WriteFile(file, pp.Byte(), os.ModePerm)
}
// Byte will return the Passwd as a byte slice
func (pp Passwds) Byte() []byte {
pass := []byte{}
for name, hash := range pp {
pass = append(pass, []byte(name+":"+hash+"\n")...)
}
return pass
}
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 verifyPass(pass, hash string, alg HashAlgorithm) error { func verifyPass(pass, hash string, alg HashAlgorithm) error {
switch alg { switch alg {
case HashAPR1: case HashAPR1:
@ -188,10 +243,55 @@ func verifyPass(pass, hash string, alg HashAlgorithm) error {
return fmt.Errorf("unsupported hash algorithm %v", alg) return fmt.Errorf("unsupported hash algorithm %v", alg)
} }
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 hashApr1(passwd string) (string, error) {
return apr1_crypt.New().Generate([]byte(passwd), nil)
}
func verifyAPR1(password, hashedPassword string) error { func verifyAPR1(password, hashedPassword string) error {
return apr1_crypt.New().Verify(hashedPassword, []byte(password)) return apr1_crypt.New().Verify(hashedPassword, []byte(password))
} }
func hashBcrypt(passwd string) (string, error) {
passwordBytes, err := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(passwordBytes), nil
}
// func verifyBcrypt does not exist as its place
// is taken by `bcrypt.CompareHashAndPassword`
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 verifySHA(password, hashedPassword string) error { func verifySHA(password, hashedPassword string) error {
eppS := hashedPassword[5:] eppS := hashedPassword[5:]
hash, err := base64.StdEncoding.DecodeString(eppS) hash, err := base64.StdEncoding.DecodeString(eppS)
@ -215,6 +315,26 @@ func verifySHA(password, hashedPassword string) error {
return nil return 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
}
func verifySSHA(password, hashedPassword string) error { func verifySSHA(password, hashedPassword string) error {
eppS := hashedPassword[6:] eppS := hashedPassword[6:]
hash, err := base64.StdEncoding.DecodeString(eppS) hash, err := base64.StdEncoding.DecodeString(eppS)
@ -246,6 +366,16 @@ func verifySSHA(password, hashedPassword string) error {
return nil return 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 verifySHA256(password, hashedPassword string) error { func verifySHA256(password, hashedPassword string) error {
eppS := hashedPassword[3:] eppS := hashedPassword[3:]
hash, err := base64.StdEncoding.DecodeString(eppS) hash, err := base64.StdEncoding.DecodeString(eppS)
@ -269,6 +399,16 @@ func verifySHA256(password, hashedPassword string) error {
return nil return 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 verifySHA512(password, hashedPassword string) error { func verifySHA512(password, hashedPassword string) error {
eppS := hashedPassword[3:] eppS := hashedPassword[3:]
hash, err := base64.StdEncoding.DecodeString(eppS) hash, err := base64.StdEncoding.DecodeString(eppS)
@ -292,121 +432,20 @@ func verifySHA512(password, hashedPassword string) error {
return nil return nil
} }
func (pp Passwds) toByte() []byte { func validLine(parts []string, lineNumber int, line string) (bool, error) {
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 var err error
switch algo { if len(parts) != 2 {
case HashAPR1: err = errors.New(fmt.Sprintln("invalid line", lineNumber+1,
hash, err = hashApr1(passwd) "unexpected number of parts split by", ":", len(parts),
case HashBCrypt: "instead of 2 in\"", line, "\""))
hash, err = hashBcrypt(passwd) return false, err
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 true, nil
return base64.StdEncoding.EncodeToString(passwordSum), nil
} }
func hashSSha(passwd string) (string, error) { func trimParts(parts []string) []string {
s := sha1.New() for i, part := range parts {
_, err := s.Write([]byte(passwd)) parts[i] = strings.Trim(part, " ")
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)) return parts
ret := append(passwordSum, salt...)
return base64.StdEncoding.EncodeToString(ret), nil
} }

25
htpasswd/htpasswd_test.go

@ -5,7 +5,7 @@ import (
"testing" "testing"
) )
func TestParseHtpassd(t *testing.T) { func TestParseHtpasswd(t *testing.T) {
passwords, err := ParseHtpasswd([]byte("sha:{SHA}IRRjboXT92QSYXm8lpGPCZUvU1E=\n")) passwords, err := ParseHtpasswd([]byte("sha:{SHA}IRRjboXT92QSYXm8lpGPCZUvU1E=\n"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -77,7 +77,7 @@ func TestVerifyPassword(t *testing.T) {
} }
} }
func TestVerify(t *testing.T) { func TestVerifyUser(t *testing.T) {
f, err := os.Stat("htpasswd_testdata") f, err := os.Stat("htpasswd_testdata")
if err != nil { if err != nil {
TestCreateUser(t) TestCreateUser(t)
@ -87,8 +87,23 @@ func TestVerify(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
err = pp.Verify("bcrypt", "123456@") testCases := []struct {
if err != nil { user, password string
t.Fatalf(err.Error()) expected error
}{
{"apr1", "123456@", nil},
{"bcrypt", "123456@", nil},
{"ssha", "123456@", nil},
{"sha", "123456@", nil},
{"sha256", "123456@", nil},
{"sha512", "123456@", nil},
}
for _, tc := range testCases {
err := pp.VerifyUser(tc.user, tc.password)
if err != tc.expected {
t.Errorf("VerifyUser(%s, %s) = %v; want %v",
tc.user, tc.password, err, tc.expected)
}
} }
} }

10
htpasswd/htpasswd_testdata

@ -1,6 +1,6 @@
apr1:$apr1$v0FnbGJM$b2P3y1ltZYHakDaHrWx3N1
bcrypt:$2a$10$pBSUext6NDsFYrm8GviW.OFe6SczH91INRC3YmsfE3HJp/fPmRaee
ssha:{SSHA}LoTRQgCdeGIeJ3nxDQMCmQSWdnMEsLqj
sha512:$6$78RjySv19bx/knbdL6q1cpoV8WblZwc3x+wmPGQUvrSycxc4liKbksvDr9HZj76hgRuZZCyEngP+WEJmePArCQ==
sha:{SHA}YEn9/RmoXLdbyB9TEDJ0OqWoPy8=
sha256:$5$ufJ2S16IOhB6XLTGrVLqGQBKv/odjE3rypxnUDLqaS0= sha256:$5$ufJ2S16IOhB6XLTGrVLqGQBKv/odjE3rypxnUDLqaS0=
apr1:$apr1$paiD.yqE$4lKqGF77KuaaEDY.bBpnv0
bcrypt:$2a$10$imLJRfdHQkOg4/cleE31Re5Xp73DQctfXtlqeZc2Mcs2I9YdjXUl2
ssha:{SSHA}m2iNELVYJhxKppu7FZHXXkGcKSlUkA/t
sha:{SHA}YEn9/RmoXLdbyB9TEDJ0OqWoPy8=
sha512:$6$78RjySv19bx/knbdL6q1cpoV8WblZwc3x+wmPGQUvrSycxc4liKbksvDr9HZj76hgRuZZCyEngP+WEJmePArCQ==

Loading…
Cancel
Save