From 008ffae3bb5f1d068f2ea4dddb88ea80a6697297 Mon Sep 17 00:00:00 2001 From: Barret Rennie Date: Fri, 16 Feb 2018 02:14:41 -0600 Subject: [PATCH] Support bcrypt passwords in htpasswd --- Gopkg.lock | 53 ++++++++++++++++++++++++++++++++++++++++-------- Gopkg.toml | 4 ++++ htpasswd.go | 24 ++++++++++++++-------- htpasswd_test.go | 25 +++++++++++++++++++++-- main.go | 2 +- 5 files changed, 89 insertions(+), 19 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index f447496..5a3758a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -58,7 +58,10 @@ [[projects]] branch = "master" name = "github.com/pquerna/cachecontrol" - packages = [".","cacheobject"] + packages = [ + ".", + "cacheobject" + ] revision = "0dec1b30a0215bb68605dfc568e8855066c9202d" [[projects]] @@ -70,30 +73,60 @@ [[projects]] branch = "master" name = "golang.org/x/crypto" - packages = ["ed25519","ed25519/internal/edwards25519"] + packages = [ + "bcrypt", + "blowfish", + "ed25519", + "ed25519/internal/edwards25519" + ] revision = "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94" [[projects]] branch = "master" name = "golang.org/x/net" - packages = ["context","context/ctxhttp"] + packages = [ + "context", + "context/ctxhttp" + ] revision = "9dfe39835686865bff950a07b394c12a98ddc811" [[projects]] branch = "master" name = "golang.org/x/oauth2" - packages = [".","google","internal","jws","jwt"] + packages = [ + ".", + "google", + "internal", + "jws", + "jwt" + ] revision = "9ff8ebcc8e241d46f52ecc5bff0e5a2f2dbef402" [[projects]] branch = "master" name = "google.golang.org/api" - packages = ["admin/directory/v1","gensupport","googleapi","googleapi/internal/uritemplates"] + packages = [ + "admin/directory/v1", + "gensupport", + "googleapi", + "googleapi/internal/uritemplates" + ] revision = "8791354e7ab150705ede13637a18c1fcc16b62e8" [[projects]] name = "google.golang.org/appengine" - packages = [".","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"] + packages = [ + ".", + "internal", + "internal/app_identity", + "internal/base", + "internal/datastore", + "internal/log", + "internal/modules", + "internal/remote_api", + "internal/urlfetch", + "urlfetch" + ] revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" version = "v1.0.0" @@ -105,13 +138,17 @@ [[projects]] name = "gopkg.in/square/go-jose.v2" - packages = [".","cipher","json"] + packages = [ + ".", + "cipher", + "json" + ] revision = "f8f38de21b4dcd69d0413faf231983f5fd6634b1" version = "v2.1.3" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "efab48a0e196c2a849bfbe9aa02d2ae28d281ce1bfe9f23720d648858eefc8e6" + inputs-digest = "b502c41a61115d14d6379be26b0300f65d173bdad852f0170d387ebf2d7ec173" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 97f83d0..c4005e1 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -38,3 +38,7 @@ [[constraint]] name = "gopkg.in/fsnotify.v1" version = "~1.2.0" + +[[constraint]] + branch = "master" + name = "golang.org/x/crypto" diff --git a/htpasswd.go b/htpasswd.go index 6fd888e..c68e558 100644 --- a/htpasswd.go +++ b/htpasswd.go @@ -7,10 +7,12 @@ import ( "io" "log" "os" + + "golang.org/x/crypto/bcrypt" ) -// lookup passwords in a htpasswd file -// The entries must have been created with -s for SHA encryption +// Lookup passwords in a htpasswd file +// Passwords must be generated with -B for bcrypt or -s for SHA1. type HtpasswdFile struct { Users map[string]string @@ -47,14 +49,20 @@ func (h *HtpasswdFile) Validate(user string, password string) bool { if !exists { return false } - if realPassword[:5] == "{SHA}" { + + shaPrefix := realPassword[:5] + if shaPrefix == "{SHA}" { + shaValue := realPassword[5:] d := sha1.New() d.Write([]byte(password)) - if realPassword[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) { - return true - } - } else { - log.Printf("Invalid htpasswd entry for %s. Must be a SHA entry.", user) + return shaValue == base64.StdEncoding.EncodeToString(d.Sum(nil)) } + + bcryptPrefix := realPassword[:4] + if bcryptPrefix == "$2a$" || bcryptPrefix == "$2b$" || bcryptPrefix == "$2x$" || bcryptPrefix == "$2y$" { + return bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password)) == nil + } + + log.Printf("Invalid htpasswd entry for %s. Must be a SHA or bcrypt entry.", user) return false } diff --git a/htpasswd_test.go b/htpasswd_test.go index 17ce37b..ebfd503 100644 --- a/htpasswd_test.go +++ b/htpasswd_test.go @@ -2,11 +2,14 @@ package main import ( "bytes" - "github.com/stretchr/testify/assert" + "fmt" "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/bcrypt" ) -func TestHtpasswd(t *testing.T) { +func TestSHA(t *testing.T) { file := bytes.NewBuffer([]byte("testuser:{SHA}PaVBVZkYqAjCQCu6UBL2xgsnZhw=\n")) h, err := NewHtpasswd(file) assert.Equal(t, err, nil) @@ -14,3 +17,21 @@ func TestHtpasswd(t *testing.T) { valid := h.Validate("testuser", "asdf") assert.Equal(t, valid, true) } + +func TestBcrypt(t *testing.T) { + hash1, err := bcrypt.GenerateFromPassword([]byte("password"), 1) + hash2, err := bcrypt.GenerateFromPassword([]byte("top-secret"), 2) + assert.Equal(t, err, nil) + + contents := fmt.Sprintf("testuser1:%s\ntestuser2:%s\n", hash1, hash2) + file := bytes.NewBuffer([]byte(contents)) + + h, err := NewHtpasswd(file) + assert.Equal(t, err, nil) + + valid := h.Validate("testuser1", "password") + assert.Equal(t, valid, true) + + valid = h.Validate("testuser2", "top-secret") + assert.Equal(t, valid, true) +} diff --git a/main.go b/main.go index 4e4c7ed..287dc48 100644 --- a/main.go +++ b/main.go @@ -52,7 +52,7 @@ func main() { flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"") flagSet.String("client-secret", "", "the OAuth Client Secret") flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)") - flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption") + flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption or \"htpasswd -B\" for bcrypt encryption") flagSet.Bool("display-htpasswd-form", true, "display username / password login form if an htpasswd file is provided") flagSet.String("custom-templates-dir", "", "path to custom html templates") flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.")