From 56008698b24d5606c77a19210dede8cba607e600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nagy=20K=C3=A1roly=20G=C3=A1briel?= Date: Wed, 27 Sep 2023 18:23:28 +0300 Subject: [PATCH] htpasswd: refactor and add Passwd methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nagy Károly Gábriel --- htpasswd/htpasswd.go | 323 +++++++++++++++++++++---------------- htpasswd/htpasswd_test.go | 25 ++- htpasswd/htpasswd_testdata | 10 +- 3 files changed, 206 insertions(+), 152 deletions(-) diff --git a/htpasswd/htpasswd.go b/htpasswd/htpasswd.go index f0195f6..7a80b49 100644 --- a/htpasswd/htpasswd.go +++ b/htpasswd/htpasswd.go @@ -79,24 +79,6 @@ 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 { @@ -104,15 +86,25 @@ func CreateUser(file, user, passwd string, algo HashAlgorithm) error { if err != nil { 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 { - return fmt.Errorf("user %s already exists", user) + return nil, fmt.Errorf("user %s already exists", user) } h, err := createHash(passwd, algo) if err != nil { - return err + return nil, err } pp[user] = h - return os.WriteFile(file, pp.toByte(), os.ModePerm) + return pp, nil } // 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 { 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 { - return fmt.Errorf("user %s does not exist", user) + return nil, fmt.Errorf("user %s does not exist", user) } h, err := createHash(passwd, algo) if err != nil { - return err + return nil, err } pp[user] = h - return os.WriteFile(file, pp.toByte(), os.ModePerm) + return pp, nil } // DeleteUser deletes the named user from the named file @@ -139,12 +141,22 @@ func DeleteUser(file, user string) error { if err != nil { 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 { - return fmt.Errorf("user %s does not exist", user) + return nil, fmt.Errorf("user %s does not exist", 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 @@ -154,12 +166,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) } @@ -170,6 +182,49 @@ func (pp Passwds) Verify(user, passwd string) error { 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 { switch alg { case HashAPR1: @@ -188,10 +243,55 @@ func verifyPass(pass, hash string, alg HashAlgorithm) error { 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 { 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 { eppS := hashedPassword[5:] hash, err := base64.StdEncoding.DecodeString(eppS) @@ -215,6 +315,26 @@ func verifySHA(password, hashedPassword string) error { 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 { eppS := hashedPassword[6:] hash, err := base64.StdEncoding.DecodeString(eppS) @@ -246,6 +366,16 @@ func verifySSHA(password, hashedPassword string) error { 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 { eppS := hashedPassword[3:] hash, err := base64.StdEncoding.DecodeString(eppS) @@ -269,6 +399,16 @@ func verifySHA256(password, hashedPassword string) error { 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 { eppS := hashedPassword[3:] hash, err := base64.StdEncoding.DecodeString(eppS) @@ -292,121 +432,20 @@ func verifySHA512(password, hashedPassword string) error { 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 := "" +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 } diff --git a/htpasswd/htpasswd_test.go b/htpasswd/htpasswd_test.go index 67897c7..cf16fd0 100644 --- a/htpasswd/htpasswd_test.go +++ b/htpasswd/htpasswd_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestParseHtpassd(t *testing.T) { +func TestParseHtpasswd(t *testing.T) { passwords, err := ParseHtpasswd([]byte("sha:{SHA}IRRjboXT92QSYXm8lpGPCZUvU1E=\n")) if err != nil { 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") if err != nil { TestCreateUser(t) @@ -87,8 +87,23 @@ func TestVerify(t *testing.T) { if err != nil { t.Fatal(err) } - err = pp.Verify("bcrypt", "123456@") - if err != nil { - t.Fatalf(err.Error()) + testCases := []struct { + user, password string + 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) + } } } diff --git a/htpasswd/htpasswd_testdata b/htpasswd/htpasswd_testdata index e9207a2..21e3874 100644 --- a/htpasswd/htpasswd_testdata +++ b/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= +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==