Skip to content
Merged
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
197 changes: 197 additions & 0 deletions pkg/auth/authenticator/password/htpasswd/htpasswd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package htpasswd

import (
"bufio"
"crypto/sha1"
"encoding/base64"
"errors"
"fmt"
"os"
"strings"

"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
"github.com/golang/glog"
authapi "github.com/openshift/origin/pkg/auth/api"
"github.com/openshift/origin/pkg/auth/authenticator"
)

// Authenticator watches a file generated by htpasswd to validate usernames and passwords
type Authenticator struct {
file string
fileInfo os.FileInfo
mapper authapi.UserIdentityMapper
usernames map[string]string
}

// New returns an authenticator which will validate usernames and passwords against the given htpasswd file
func New(file string, mapper authapi.UserIdentityMapper) (authenticator.Password, error) {
auth := &Authenticator{
file: file,
mapper: mapper,
}
if err := auth.loadIfNeeded(); err != nil {
return nil, err
}
return auth, nil
}

func (a *Authenticator) AuthenticatePassword(username, password string) (user.Info, bool, error) {
a.loadIfNeeded()

if len(username) > 255 {
username = username[:255]
}
if strings.Contains(username, ":") {
return nil, false, errors.New("Usernames may not contain : characters")
}
hash, ok := a.usernames[username]
if !ok {
return nil, false, nil
}
if ok, err := testPassword(password, hash); !ok || err != nil {
return nil, false, err
}

identity := &authapi.DefaultUserIdentityInfo{
UserName: username,
}
user, err := a.mapper.UserFor(identity)
glog.V(4).Infof("Got userIdentityMapping: %#v", user)
if err != nil {
return nil, false, fmt.Errorf("Error creating or updating mapping for: %#v due to %v", identity, err)
}

return user, true, nil

}

func (a *Authenticator) load() error {
file, err := os.Open(a.file)
if err != nil {
return err
}
defer file.Close()

newusernames := map[string]string{}
warnedusernames := map[string]bool{}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()

if len(line) == 0 {
continue
}

parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
glog.Warningf("Ignoring malformed htpasswd line: %s", line)
continue
}

username := parts[0]
password := parts[1]
if _, duplicate := newusernames[username]; duplicate {
if _, warned := warnedusernames[username]; !warned {
warnedusernames[username] = true
glog.Warningf("%s contains multiple passwords for user '%s'. The last one specified will be used.", a.file, username)
}
}
newusernames[username] = password
}

a.usernames = newusernames

return nil
}

func (a *Authenticator) loadIfNeeded() error {
info, err := os.Stat(a.file)
if err != nil {
return err
}
if a.fileInfo == nil || a.fileInfo.ModTime() != info.ModTime() {
glog.V(4).Infof("Loading htpasswd file %s...", a.file)
a.fileInfo = info
return a.load()
}
return nil
}

func testPassword(password, hash string) (bool, error) {
switch {
case strings.HasPrefix(hash, "$apr1$"):
// MD5, default
return testMD5Password(password, hash)
case strings.HasPrefix(hash, "$2y$") || strings.HasPrefix(hash, "$2a$"):
// Bcrypt, secure
return testBCryptPassword(password, hash)
case strings.HasPrefix(hash, "{SHA}"):
// SHA-1, insecure
return testSHAPassword(password, hash[5:])
case len(hash) == 13:
// looks like crypt
return testCryptPassword(password, hash)
default:
return false, errors.New("Unrecognized hash type")
}
}

func testSHAPassword(password, hash string) (bool, error) {
if len(hash) == 0 {
return false, errors.New("Invalid SHA hash")
}
// Compute hash of password
shasum := sha1.Sum([]byte(password))
// Base-64 encode
base64shasum := base64.StdEncoding.EncodeToString(shasum[:])
// Compare
match := hash == base64shasum
return match, nil
}

func testBCryptPassword(password, hash string) (bool, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return errors from unimplemented test functions, so that we don't end up with weird, silent behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to return errors and re-enabled test cases for those types to make sure all cases result in errors

// TODO: import bcrypt
// err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
// if err == bcrypt.ErrMismatchedHashAndPassword {
// return false, nil
// }
// if err != nil {
// return false, err
// }
// return true, nil
return false, errors.New("bcrypt password hashes are not supported")
}

func testMD5Password(password, hash string) (bool, error) {
parts := strings.Split(hash, "$")
if len(parts) != 4 {
return false, errors.New("Malformed MD5 hash")
}

salt := parts[2]
if len(salt) == 0 {
return false, errors.New("Malformed MD5 hash: missing salt")
}
if len(salt) > 8 {
salt = salt[:8]
}

md5hash := parts[3]
if len(md5hash) == 0 {
return false, errors.New("Malformed MD5 hash: missing hash")
}

testhash := string(apr_md5([]byte(password), []byte(salt)))
match := testhash == hash

return match, nil
}

func testCryptPassword(password, hash string) (bool, error) {
// if len(password) > 8 {
// password = password[:8]
// }
// salt := hash[0:2]
// hash = hash[2:]
return false, errors.New("crypt password hashes are not supported")
}
184 changes: 184 additions & 0 deletions pkg/auth/authenticator/password/htpasswd/htpasswd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package htpasswd

import "testing"

func TestPasswordHashes(t *testing.T) {
testCases := []struct {
Name string
Password string
Hash string
Match bool
Error bool
}{
// htpasswd -n -b "" ""
// ":$apr1$AQEmuiLe$2lPjK2hL6mnTakRWskgaQ1"
{
Name: "md5 empty",
Password: "",
Hash: "$apr1$AQEmuiLe$2lPjK2hL6mnTakRWskgaQ1",
Match: true,
},
// htpasswd -n -b "username" "password"
// username:$apr1$6TMtuxUJ$0M76TkGjp0qVg/e7rfk22.
{
Name: "md5 password",
Password: "password",
Hash: "$apr1$6TMtuxUJ$0M76TkGjp0qVg/e7rfk22.",
Match: true,
},
{
Name: "md5 mismatch",
Password: "mypassword",
Hash: "$apr1$6TMtuxUJ$0M76TkGjp0qVg/e7rfk22.",
Match: false,
},
{
Name: "md5 missing salt",
Password: "password",
Hash: "$apr1$$0M76TkGjp0qVg/e7rfk22.",
Match: false,
Error: true,
},
// htpasswd -n -b "username" "passwordthatisreallyreallyreallyreallyreallyreallyreallylong"
// username:$apr1$6VmuPCYl$OvuHDqaS59nsRov9HnsGc1
{
Name: "md5 with long password",
Password: "passwordthatisreallyreallyreallyreallyreallyreallyreallylong",
Hash: "$apr1$6VmuPCYl$OvuHDqaS59nsRov9HnsGc1",
Match: true,
},

// htpasswd -d -n -b "" ""
// :lNO4S8u4F4oNo
{
Name: "crypt empty",
Password: "",
Hash: "lNO4S8u4F4oNo",
Match: false, // TODO: change to true if/when we add crypt support
Error: true, // TODO: change to false if/when we add crypt support
},
// htpasswd -d -n -b "username" "password"
// username:.zs/E.NK2vwFs
{
Name: "crypt match",
Password: "password",
Hash: ".zs/E.NK2vwFs",
Match: false, // TODO: change to true if/when we add crypt support
Error: true, // TODO: change to false if/when we add crypt support
},
{
Name: "crypt mismatch",
Password: "mypassword",
Hash: ".zs/E.NK2vwFs",
Match: false, // TODO: change to true if/when we add crypt support
Error: true, // TODO: change to false if/when we add crypt support
},
{
Name: "crypt missing salt",
Password: "password",
Hash: "s",
Match: false,
Error: true,
},

// htpasswd -s -n -b "" ""
// :{SHA}2jmj7l5rSw0yVb/vlWAYkK/YBwk=
{
Name: "sha empty",
Password: "",
Hash: "{SHA}2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
Match: true,
},
// htpasswd -s -n -b "username" "password"
// username:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
{
Name: "sha match",
Password: "password",
Hash: "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
Match: true,
},
{
Name: "sha mismatch",
Password: "mypassword",
Hash: "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
Match: false,
},
{
Name: "sha invalid",
Password: "mypassword",
Hash: "{SHA}",
Match: false,
Error: true,
},

// htpasswd -B -n -b "" ""
// :$2y$05$Edf.Eeznh19sIYYcTc7YOeltcWjzFuvrcYp57lq78diiJr512GILm
{
Name: "bcrypt strength 5 empty",
Password: "",
Hash: "$2y$05$Edf.Eeznh19sIYYcTc7YOeltcWjzFuvrcYp57lq78diiJr512GILm",
Match: false, // TODO: change to true if/when we add bcrypt support
Error: true, // TODO: change to false if/when we add bcrypt support
},
// htpasswd -B -n -b "username" "password"
// username:$2y$05$Vfd6hjeQXB6nTFTVMkoFE.CAItk2W8akuomafFBakd0n/mHqIzoUO
{
Name: "bcrypt strength 5 match",
Password: "password",
Hash: "$2y$05$Vfd6hjeQXB6nTFTVMkoFE.CAItk2W8akuomafFBakd0n/mHqIzoUO",
Match: false, // TODO: change to true if/when we add bcrypt support
Error: true, // TODO: change to false if/when we add bcrypt support
},
{
Name: "bcrypt strength 5 mismatch",
Password: "mypassword",
Hash: "$2y$05$Vfd6hjeQXB6nTFTVMkoFE.CAItk2W8akuomafFBakd0n/mHqIzoUO",
Match: false,
Error: true, // TODO: change to false if/when we add bcrypt support
},

// htpasswd -C 10 -B -n -b "" ""
// :$2y$10$v0c.7wrYEv2AZnLsPXO57.48Qc5widamyKkmwrUolKwYW0Zw8zhJ.
{
Name: "bcrypt strength 10 empty",
Password: "",
Hash: "$2y$10$v0c.7wrYEv2AZnLsPXO57.48Qc5widamyKkmwrUolKwYW0Zw8zhJ.",
Match: false, // TODO: change to true if/when we add bcrypt support
Error: true, // TODO: change to false if/when we add bcrypt support
},
// htpasswd -C 10 -B -n -b "username" "password"
// username:$2y$10$Fk32bQky/.91nbecGjFfPO1m97V12d.ickjAzpNF22NgMKs4qWDOK
{
Name: "bcrypt strength 10 match",
Password: "password",
Hash: "$2y$10$Fk32bQky/.91nbecGjFfPO1m97V12d.ickjAzpNF22NgMKs4qWDOK",
Match: false, // TODO: change to true if/when we add bcrypt support
Error: true, // TODO: change to false if/when we add bcrypt support
},
{
Name: "bcrypt strength 10 mismatch",
Password: "mypassword",
Hash: "$2y$10$Fk32bQky/.91nbecGjFfPO1m97V12d.ickjAzpNF22NgMKs4qWDOK",
Match: false,
Error: true, // TODO: change to false if/when we add bcrypt support
},

{
Name: "bcrypt missing strength",
Password: "password",
Hash: "$2y$$Fk32bQky/.91nbecGjFfPO1m97V12d.ickjAzpNF22NgMKs4qWDOK",
Match: false,
Error: true,
},
}

for _, testCase := range testCases {
match, err := testPassword(testCase.Password, testCase.Hash)
if testCase.Error != (err != nil) {
t.Errorf("%s: Expected error=%v, got %v", testCase.Name, testCase.Error, err)
}
if match != testCase.Match {
t.Errorf("%s: Expected match=%v, got %v", testCase.Name, testCase.Match, match)
}
}
}
Loading