2015-04-21 19:57:12 +00:00
|
|
|
package basic
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha1"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/csv"
|
|
|
|
"errors"
|
|
|
|
"os"
|
2015-06-04 15:46:34 +00:00
|
|
|
"regexp"
|
|
|
|
"strings"
|
2015-06-04 16:02:13 +00:00
|
|
|
|
2015-06-04 15:46:34 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
2015-04-21 19:57:12 +00:00
|
|
|
)
|
|
|
|
|
2015-06-06 05:37:32 +00:00
|
|
|
// ErrAuthenticationFailure A generic error message for authentication failure to be presented to agent.
|
2015-06-04 16:02:13 +00:00
|
|
|
var ErrAuthenticationFailure = errors.New("Bad username or password")
|
2015-04-21 19:57:12 +00:00
|
|
|
|
2015-06-06 05:37:32 +00:00
|
|
|
// htpasswd Holds a path to a system .htpasswd file and the machinery to parse it.
|
|
|
|
type htpasswd struct {
|
2015-04-21 19:57:12 +00:00
|
|
|
path string
|
|
|
|
reader *csv.Reader
|
|
|
|
}
|
|
|
|
|
2015-06-06 05:37:32 +00:00
|
|
|
// AuthType Represents a particular hash function used in the htpasswd file.
|
2015-06-04 16:02:13 +00:00
|
|
|
type AuthType int
|
|
|
|
|
2015-06-04 15:46:34 +00:00
|
|
|
const (
|
2015-06-06 05:37:32 +00:00
|
|
|
// PlainText Plain-text password storage (htpasswd -p)
|
2015-06-04 16:02:13 +00:00
|
|
|
PlainText AuthType = iota
|
2015-06-06 05:37:32 +00:00
|
|
|
// SHA1 sha hashed password storage (htpasswd -s)
|
2015-06-04 15:46:34 +00:00
|
|
|
SHA1
|
2015-06-06 05:37:32 +00:00
|
|
|
// ApacheMD5 apr iterated md5 hashing (htpasswd -m)
|
2015-06-04 15:46:34 +00:00
|
|
|
ApacheMD5
|
2015-06-06 05:37:32 +00:00
|
|
|
// BCrypt BCrypt adapative password hashing (htpasswd -B)
|
2015-06-04 15:46:34 +00:00
|
|
|
BCrypt
|
2015-06-06 05:37:32 +00:00
|
|
|
// Crypt System crypt() hashes. (htpasswd -d)
|
2015-06-04 15:46:34 +00:00
|
|
|
Crypt
|
|
|
|
)
|
|
|
|
|
2015-06-06 05:37:32 +00:00
|
|
|
// String Returns a text representation of the AuthType
|
2015-06-04 15:46:34 +00:00
|
|
|
func (at AuthType) String() string {
|
2015-06-04 16:02:13 +00:00
|
|
|
switch at {
|
|
|
|
case PlainText:
|
|
|
|
return "plaintext"
|
|
|
|
case SHA1:
|
|
|
|
return "sha1"
|
|
|
|
case ApacheMD5:
|
|
|
|
return "md5"
|
|
|
|
case BCrypt:
|
|
|
|
return "bcrypt"
|
|
|
|
case Crypt:
|
|
|
|
return "system crypt"
|
2015-06-04 15:46:34 +00:00
|
|
|
}
|
|
|
|
return "unknown"
|
|
|
|
}
|
|
|
|
|
2015-06-06 05:37:32 +00:00
|
|
|
// NewHTPasswd Create a new HTPasswd with the given path to .htpasswd file.
|
2015-06-06 05:58:45 +00:00
|
|
|
func newHTPasswd(htpath string) *htpasswd {
|
2015-06-06 05:37:32 +00:00
|
|
|
return &htpasswd{path: htpath}
|
2015-04-21 19:57:12 +00:00
|
|
|
}
|
|
|
|
|
2015-06-04 16:02:13 +00:00
|
|
|
var bcryptPrefixRegexp = regexp.MustCompile(`^\$2[ab]?y\$`)
|
2015-06-04 15:46:34 +00:00
|
|
|
|
2015-06-06 05:37:32 +00:00
|
|
|
// GetAuthCredentialType Inspect an htpasswd file credential and guess the encryption algorithm used.
|
2015-06-04 15:46:34 +00:00
|
|
|
func GetAuthCredentialType(cred string) AuthType {
|
|
|
|
if strings.HasPrefix(cred, "{SHA}") {
|
|
|
|
return SHA1
|
|
|
|
}
|
|
|
|
if strings.HasPrefix(cred, "$apr1$") {
|
|
|
|
return ApacheMD5
|
|
|
|
}
|
|
|
|
if bcryptPrefixRegexp.MatchString(cred) {
|
|
|
|
return BCrypt
|
|
|
|
}
|
|
|
|
// There's just not a great way to distinguish between these next two...
|
|
|
|
if len(cred) == 13 {
|
|
|
|
return Crypt
|
|
|
|
}
|
|
|
|
return PlainText
|
|
|
|
}
|
|
|
|
|
2015-06-06 05:37:32 +00:00
|
|
|
// AuthenticateUser Check a given user:password credential against the receiving HTPasswd's file.
|
|
|
|
func (htpasswd *htpasswd) AuthenticateUser(user string, pwd string) (bool, error) {
|
2015-04-21 19:57:12 +00:00
|
|
|
|
|
|
|
// Open the file.
|
|
|
|
in, err := os.Open(htpasswd.path)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the contents of the standard .htpasswd until we hit the end or find a match.
|
|
|
|
reader := csv.NewReader(in)
|
|
|
|
reader.Comma = ':'
|
|
|
|
reader.Comment = '#'
|
|
|
|
reader.TrimLeadingSpace = true
|
|
|
|
for entry, readerr := reader.Read(); entry != nil || readerr != nil; entry, readerr = reader.Read() {
|
2015-06-04 15:46:34 +00:00
|
|
|
if readerr != nil {
|
|
|
|
return false, readerr
|
|
|
|
}
|
|
|
|
if len(entry) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2015-04-21 19:57:12 +00:00
|
|
|
if entry[0] == user {
|
2015-06-04 15:46:34 +00:00
|
|
|
credential := entry[1]
|
|
|
|
credType := GetAuthCredentialType(credential)
|
2015-06-04 16:02:13 +00:00
|
|
|
switch credType {
|
|
|
|
case SHA1:
|
|
|
|
{
|
2015-06-04 15:46:34 +00:00
|
|
|
sha := sha1.New()
|
|
|
|
sha.Write([]byte(pwd))
|
|
|
|
hash := base64.StdEncoding.EncodeToString(sha.Sum(nil))
|
|
|
|
return entry[1][5:] == hash, nil
|
|
|
|
}
|
2015-06-04 16:02:13 +00:00
|
|
|
case ApacheMD5:
|
|
|
|
{
|
|
|
|
return false, errors.New(ApacheMD5.String() + " htpasswd hash function not yet supported")
|
2015-06-04 15:46:34 +00:00
|
|
|
}
|
2015-06-04 16:02:13 +00:00
|
|
|
case BCrypt:
|
|
|
|
{
|
|
|
|
err := bcrypt.CompareHashAndPassword([]byte(credential), []byte(pwd))
|
2015-06-04 15:46:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
2015-06-04 16:02:13 +00:00
|
|
|
case Crypt:
|
|
|
|
{
|
|
|
|
return false, errors.New(Crypt.String() + " htpasswd hash function not yet supported")
|
2015-06-04 15:46:34 +00:00
|
|
|
}
|
2015-06-04 16:02:13 +00:00
|
|
|
case PlainText:
|
|
|
|
{
|
2015-06-04 15:46:34 +00:00
|
|
|
if pwd == credential {
|
|
|
|
return true, nil
|
2015-06-04 16:02:13 +00:00
|
|
|
}
|
|
|
|
return false, ErrAuthenticationFailure
|
2015-06-04 15:46:34 +00:00
|
|
|
}
|
2015-04-21 19:57:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-06-04 16:02:13 +00:00
|
|
|
return false, ErrAuthenticationFailure
|
2015-04-21 19:57:12 +00:00
|
|
|
}
|