Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
42 changes: 42 additions & 0 deletions htpasswd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"io/ioutil"
"os"
"strings"

"github.com/GehirnInc/crypt/apr1_crypt"
"golang.org/x/crypto/bcrypt"
)

// HashedPasswords name => hash
Expand All @@ -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 = ":"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
22 changes: 22 additions & 0 deletions htpasswd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"))
Expand All @@ -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))
Expand Down Expand Up @@ -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)])
}
}
}