Skip to content

Commit

Permalink
Add support for generating htpasswd hash
Browse files Browse the repository at this point in the history
Fixes issue Masterminds#32
  • Loading branch information
Ravi Kumar committed Jan 9, 2020
1 parent 25dce23 commit acd062d
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 0 deletions.
14 changes: 14 additions & 0 deletions crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ import (
"net"
"time"

"strings"

"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/scrypt"
)

Expand All @@ -46,6 +49,17 @@ func adler32sum(input string) string {
return fmt.Sprintf("%d", hash)
}

func htpasswd(username string, password string) string {
if strings.Contains(username, ":") {
return fmt.Sprintf("invalid username: %s", username)
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Sprintf("failed to create htpasswd: %s", err)
}
return fmt.Sprintf("%s:%s", username, hash)
}

// uuidv4 provides a safe and secure UUID v4 implementation
func uuidv4() string {
return uuid.New().String()
Expand Down
29 changes: 29 additions & 0 deletions crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"golang.org/x/crypto/bcrypt"
)

const (
Expand Down Expand Up @@ -36,6 +37,34 @@ func TestAdler32Sum(t *testing.T) {
}
}

type HtpasswdCred struct {
Username string
Password string
Valid bool
}

func TestHtpasswd(t *testing.T) {
expectations := []HtpasswdCred{
{Username: "myUser", Password: "myPassword", Valid: true},
{Username: "special'o79Cv_*qFe,)<user", Password: "special<j7+3p#6-.Jx2U:m8G;kGypassword", Valid: true},
{Username: "wrongus:er", Password: "doesn'tmatter", Valid: false}, // ':' isn't allowed in the username - https://tools.ietf.org/html/rfc2617#page-6
}

for _, credential := range expectations {
out, err := runRaw(`{{htpasswd .Username .Password}}`, credential)
if err != nil {
t.Error(err)
}
result := strings.Split(out, ":")
if 0 != strings.Compare(credential.Username, result[0]) && credential.Valid {
t.Error("Generated username did not match for:", credential.Username)
}
if bcrypt.CompareHashAndPassword([]byte(result[1]), []byte(credential.Password)) != nil && credential.Valid {
t.Error("Generated hash is not the equivalent for password:", credential.Password)
}
}
}

func TestDerivePassword(t *testing.T) {
expectations := map[string]string{
`{{derivePassword 1 "long" "password" "user" "example.com"}}`: "ZedaFaxcZaso9*",
Expand Down
10 changes: 10 additions & 0 deletions docs/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ The `adler32sum` function receives a string, and computes its Adler-32 checksum.
adler32sum "Hello world!"
```

## htpasswd

The `htpasswd` function takes a `username` and `password` and generates a `bcrypt` hash of the psasword. The result can be used for [basic authentication](https://httpd.apache.org/docs/2.4/misc/password_encryptions.html#basic) on an Apache HTTP Server.

```
htpasswd "myUser" "myPassword"
```

Note that it is insecure to store the password directly in the template.

## derivePassword

The `derivePassword` function can be used to derive a specific password based on
Expand Down
1 change: 1 addition & 0 deletions functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ var genericMap = map[string]interface{}{
"concat": concat,

// Crypto:
"htpasswd": htpasswd,
"genPrivateKey": generatePrivateKey,
"derivePassword": derivePassword,
"buildCustomCert": buildCustomCertificate,
Expand Down

0 comments on commit acd062d

Please sign in to comment.