diff --git a/htpasswd/apr1.go b/htpasswd/apr1.go new file mode 100644 index 0000000..f3b1e6f --- /dev/null +++ b/htpasswd/apr1.go @@ -0,0 +1,22 @@ +package htpasswd + +import "github.com/GehirnInc/crypt/apr1_crypt" + +// Apr1 facilitates apr1 style hashing +type Apr1 struct{} + +// Hash returns the hashed variant of the password or an error +func (*Apr1) Hash(passwd string) (string, error) { + return apr1_crypt.New().Generate([]byte(passwd), nil) +} + +// Match verifier the hashed password using the original +func (*Apr1) Match(password, hashedPassword string) error { + return apr1_crypt.New().Verify(hashedPassword, []byte(password)) +} + +// Name returns the name of the hasher +func (*Apr1) Name() string { return "apr1" } + +// Prefix returns the hasher's prefix +func (*Apr1) Prefix() string { return "$apr1$" } diff --git a/htpasswd/bcrypt.go b/htpasswd/bcrypt.go new file mode 100644 index 0000000..7a98e23 --- /dev/null +++ b/htpasswd/bcrypt.go @@ -0,0 +1,26 @@ +package htpasswd + +import "golang.org/x/crypto/bcrypt" + +// Bcrypt facilitates bcrypt style hashing +type Bcrypt struct{} + +// Hash returns the hashed variant of the password or an error +func (*Bcrypt) Hash(password string) (string, error) { + passwordBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(passwordBytes), nil +} + +// Match verifier the hashed password using the original +func (*Bcrypt) Match(password, hashedPassword string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} + +// Name returns the name of the hasher +func (*Bcrypt) Name() string { return "bcrypt" } + +// Prefix returns the hasher's prefix +func (*Bcrypt) Prefix() string { return "$2a$" } diff --git a/htpasswd/htpasswd.go b/htpasswd/htpasswd.go index f0195f6..8d16752 100644 --- a/htpasswd/htpasswd.go +++ b/htpasswd/htpasswd.go @@ -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") - } - - 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 + return alg.Match(passwd, pp[user]) } -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 +// 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) } -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 } diff --git a/htpasswd/htpasswd_test.go b/htpasswd/htpasswd_test.go index 67897c7..874168a 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) @@ -26,15 +26,15 @@ func TestCreateUser(t *testing.T) { } testCases := []struct { user, password string - hash HashAlgorithm + hash Hasher expected error }{ - {"apr1", "123456@", HashAPR1, nil}, - {"bcrypt", "123456@", HashBCrypt, nil}, - {"ssha", "123456@", HashSSHA, nil}, - {"sha", "123456@", HashSHA, nil}, - {"sha256", "123456@", HashSHA256, nil}, - {"sha512", "123456@", HashSHA512, nil}, + {"apr1", "123456@", new(Apr1), nil}, + {"bcrypt", "123456@", new(Bcrypt), nil}, + {"ssha", "123456@", new(Ssha), nil}, + {"sha", "123456@", new(Sha), nil}, + {"sha256", "123456@", new(Sha256), nil}, + {"sha512", "123456@", new(Sha512), nil}, } for _, tc := range testCases { @@ -71,13 +71,13 @@ func TestVerifyPassword(t *testing.T) { for _, tc := range testCases { err := VerifyUser(f.Name(), tc.user, tc.password) if err != tc.expected { - t.Errorf("VerifyUser(%s %s, %s) = %v; want %v", + t.Errorf("VerifyUser(%v %v, %v) = %v; want %v", f, tc.user, tc.password, err, tc.expected) } } } -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..c35ff21 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== +bcrypt:$2a$10$YNEmQcVCrvcA8m1DNCwR9eXaTySDEa1sC/T5xUUTFnVnP.RIP3O2u +ssha:{SSHA}fSLbdty+JHr+q3p/lHAPvNkOU3H8NLmI sha:{SHA}YEn9/RmoXLdbyB9TEDJ0OqWoPy8= sha256:$5$ufJ2S16IOhB6XLTGrVLqGQBKv/odjE3rypxnUDLqaS0= +sha512:$6$78RjySv19bx/knbdL6q1cpoV8WblZwc3x+wmPGQUvrSycxc4liKbksvDr9HZj76hgRuZZCyEngP+WEJmePArCQ== +apr1:$apr1$mO.FA9Gg$1LbPaKe7HCVHezEYzMCnn. diff --git a/htpasswd/sha.go b/htpasswd/sha.go new file mode 100644 index 0000000..61c3425 --- /dev/null +++ b/htpasswd/sha.go @@ -0,0 +1,52 @@ +package htpasswd + +import ( + "bytes" + "crypto/sha1" + "encoding/base64" + "fmt" +) + +// Sha facilitates sha1 style hashing +type Sha struct{} + +// Hash returns the hashed variant of the password or an error +func (ss *Sha) Hash(passwd string) (string, error) { + s := sha1.New() + _, err := s.Write([]byte(passwd)) + if err != nil { + return "", err + } + passwordSum := []byte(s.Sum(nil)) + return ss.Prefix() + base64.StdEncoding.EncodeToString(passwordSum), nil +} + +// Match verifier the hashed password using the original +func (*Sha) Match(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 +} + +// Name returns the name of the hasher +func (*Sha) Name() string { return "sha" } + +// Prefix returns the hasher's prefix +func (*Sha) Prefix() string { return "{SHA}" } diff --git a/htpasswd/sha256.go b/htpasswd/sha256.go new file mode 100644 index 0000000..6c3f3c7 --- /dev/null +++ b/htpasswd/sha256.go @@ -0,0 +1,52 @@ +package htpasswd + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "fmt" +) + +// Sha256 facilitates sha256 style hashing +type Sha256 struct{} + +// Hash returns the hashed variant of the password or an error +func (ss *Sha256) Hash(passwd string) (string, error) { + s := sha256.New() + _, err := s.Write([]byte(passwd)) + if err != nil { + return "", err + } + passwordSum := []byte(s.Sum(nil)) + return ss.Prefix() + base64.StdEncoding.EncodeToString(passwordSum), nil +} + +// Match verifier the hashed password using the original +func (*Sha256) Match(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 +} + +// Name returns the name of the hasher +func (*Sha256) Name() string { return "sha256" } + +// Prefix returns the hasher's prefix +func (*Sha256) Prefix() string { return "$5$" } diff --git a/htpasswd/sha512.go b/htpasswd/sha512.go new file mode 100644 index 0000000..b8acf5a --- /dev/null +++ b/htpasswd/sha512.go @@ -0,0 +1,52 @@ +package htpasswd + +import ( + "bytes" + "crypto/sha512" + "encoding/base64" + "fmt" +) + +// Sha512 facilitates sha512 style hashing +type Sha512 struct{} + +// Hash returns the hashed variant of the password or an error +func (ss *Sha512) Hash(passwd string) (string, error) { + s := sha512.New() + _, err := s.Write([]byte(passwd)) + if err != nil { + return "", err + } + passwordSum := []byte(s.Sum(nil)) + return ss.Prefix() + base64.StdEncoding.EncodeToString(passwordSum), nil +} + +// Match verifier the hashed password using the original +func (*Sha512) Match(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 +} + +// Name returns the name of the hasher +func (*Sha512) Name() string { return "sha512" } + +// Prefix returns the hasher's prefix +func (*Sha512) Prefix() string { return "$6$" } diff --git a/htpasswd/ssha.go b/htpasswd/ssha.go new file mode 100644 index 0000000..29ea797 --- /dev/null +++ b/htpasswd/ssha.go @@ -0,0 +1,71 @@ +package htpasswd + +import ( + "bytes" + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "fmt" +) + +// Ssha facilitates ssha style hashing +type Ssha struct{} + +// Hash returns the hashed variant of the password or an error +func (ss *Ssha) Hash(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 ss.Prefix() + base64.StdEncoding.EncodeToString(ret), nil +} + +// Match verifier the hashed password using the original +func (*Ssha) Match(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 +} + +// Name returns the name of the hasher +func (*Ssha) Name() string { return "ssha" } + +// Prefix returns the hasher's prefix +func (*Ssha) Prefix() string { return "{SSHA}" }