diff --git a/README.md b/README.md index 4149dc1..117ebf5 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,12 @@ password := "secret" err := htpasswd.SetPassword(file, name, password, htpasswd.HashBCrypt) ``` +Verify password in file: + +```Go +ok, err := htpasswd.VerifyPassword(file, name, password, htpasswd.HashBCrypt) +``` + Remove a user: ```Go @@ -33,4 +39,10 @@ Read user hash table: passwords, err := htpasswd.ParseHtpasswdFile(file) ``` +Verify password: + +```Go +ok := passwords.VerifyPassword(name, password, htpasswd.HashBCrypt) +``` + Have fun. diff --git a/go.sum b/go.sum index 3409258..274a6c6 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,7 @@ github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46/go.mod h1:kC29dT1v golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/htpasswd.go b/htpasswd.go index 0ad0d3d..7a5c814 100644 --- a/htpasswd.go +++ b/htpasswd.go @@ -8,6 +8,9 @@ import ( "io/ioutil" "os" "strings" + + "github.com/GehirnInc/crypt/apr1_crypt" + "golang.org/x/crypto/bcrypt" ) // HashedPasswords name => hash @@ -25,6 +28,13 @@ const ( HashSHA = "sha" ) +// HashAlgorithms is a list of supported hashes +var HashAlgorithms = []HashAlgorithm{ + HashAPR1, + HashBCrypt, + HashSHA, +} + const ( // PasswordSeparator separates passwords from hashes PasswordSeparator = ":" @@ -75,6 +85,25 @@ func (hp HashedPasswords) SetPassword(name, password string, hashAlgorithm HashA return nil } +// VerifyPassword verify a password for a user with a hashing algo +func (hp HashedPasswords) VerifyPassword(name, password string, hashAlgorithm HashAlgorithm) bool { + if len(password) == 0 { + return false + } + switch hashAlgorithm { + case HashAPR1: + err := apr1_crypt.New().Verify(hp[name], []byte(password)) + return err == nil + case HashSHA: + return "{SHA}"+hashSha(password) == hp[name] + case HashBCrypt: + err := bcrypt.CompareHashAndPassword([]byte(hp[name]), []byte(password)) + return err == nil + default: + return false + } +} + // ParseHtpasswdFile load a htpasswd file func ParseHtpasswdFile(file string) (passwords HashedPasswords, err error) { htpasswdBytes, err := ioutil.ReadFile(file) @@ -171,3 +200,16 @@ func SetPassword(file, name, password string, hashAlgorithm HashAlgorithm) error } return passwords.WriteToFile(file) } + +// VerifyPassword verify password in file for a user with a given hashing algorithm +func VerifyPassword(file, name, password string, hashAlgorithm HashAlgorithm) (bool, error) { + _, err := os.Stat(file) + passwords := HashedPasswords(map[string]string{}) + if err == nil { + passwords, err = ParseHtpasswdFile(file) + if err != nil { + return false, err + } + } + return passwords.VerifyPassword(name, password, hashAlgorithm), nil +} diff --git a/htpasswd_test.go b/htpasswd_test.go index d9e62a8..020a370 100644 --- a/htpasswd_test.go +++ b/htpasswd_test.go @@ -51,12 +51,14 @@ func TestParseHtpassd(t *testing.T) { func TestEmptyHtpasswdFile(t *testing.T) { f := tFile("empty") + defer os.Remove(f) SetPassword(f, "sha", "sha", HashSHA) fileContentsAre(t, f, "sha:{SHA}2PRZAyDhNDqRW2OUFwZQqPNdaSY=\n") } func TestRemoveUser(t *testing.T) { f := tFile("removeUser") + defer os.Remove(f) const firstUser = "sha" SetPassword(f, firstUser, "sha", HashSHA) const user = "foo" @@ -92,6 +94,7 @@ func TestCorruption(t *testing.T) { func TestSetPasswordHash(t *testing.T) { f := tFile("set-hashes") + defer os.Remove(f) poe(SetPasswordHash(f, "a", "a")) poe(SetPasswordHash(f, "b", "b")) poe(SetPasswordHash(f, "c", "c")) @@ -108,6 +111,16 @@ func TestSetPasswordHash(t *testing.T) { t.Fatal("c failed") } } +func TestVerifyPasswordInFile(t *testing.T) { + f := tFile("verify-hash") + defer os.Remove(f) + SetPassword(f, "sha", "sha", HashSHA) + ok, err := VerifyPassword(f, "sha", "sha", HashSHA) + poe(err) + if !ok { + t.Fatal("Hash in file verify failed") + } +} func TestHashing(t *testing.T) { testHashes := HashedPasswords(make(map[string]string)) @@ -135,3 +148,12 @@ func TestHashing(t *testing.T) { } } } + +func TestVerify(t *testing.T) { + testHashes := getHashedPasswords() + for _, algo := range HashAlgorithms { + if !testHashes.VerifyPassword(string(algo), string(algo), algo) { + t.Error(algo, testHashes[string(algo)]) + } + } +}