From f05e41c9aad83f3d45aff620a739a116c32b4c47 Mon Sep 17 00:00:00 2001 From: Simon Beck Date: Tue, 8 Feb 2022 17:59:59 +0100 Subject: [PATCH] Improve password hash handling This adds support for more hash algorithms. Also a stored password will be updated to SSHA512 upon a successful bind. It will also automatically hash a cleartext password if the `userpassword` field is modified with a cleartext one. Hashes supported: * SSHA * SSHA256 * SSHA512 --- go.mod | 2 +- go.sum | 10 +++----- main.go | 39 ++++++++++++++++++++++++----- ssha.go | 76 ++++++++++++++++++++++++++------------------------------ write.go | 23 +++++++++++++++-- 5 files changed, 93 insertions(+), 57 deletions(-) diff --git a/go.mod b/go.mod index a7ce11d..410153e 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module bottin go 1.13 require ( - github.com/go-ldap/ldap/v3 v3.3.0 github.com/google/uuid v1.1.1 github.com/hashicorp/consul/api v1.3.0 + github.com/jsimonetti/pwscheme v0.0.0-20220125093853-4d9895f5db73 github.com/sirupsen/logrus v1.4.2 golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect ) diff --git a/go.sum b/go.sum index 8954334..c1ba571 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28= -github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -9,11 +7,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= -github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-ldap/ldap v2.5.1+incompatible h1:Opaoft5zMW8IU/VRULB0eGMBQ9P5buRvCW6sFTRmMn8= -github.com/go-ldap/ldap/v3 v3.3.0 h1:lwx+SJpgOHd8tG6SumBQZXCmNX51zM8B1cfxJ5gv4tQ= -github.com/go-ldap/ldap/v3 v3.3.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= @@ -49,6 +42,8 @@ github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG67 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/jsimonetti/pwscheme v0.0.0-20220125093853-4d9895f5db73 h1:ZhC4QngptYaGx53+ph1RjxcH8fkCozBaY+935TNX4i8= +github.com/jsimonetti/pwscheme v0.0.0-20220125093853-4d9895f5db73/go.mod h1:t0Q9JvoMTfTYdAWIk2MF69iz+Qpdk9D+PgVu6fVmaDI= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -88,6 +83,7 @@ golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/main.go b/main.go index 4e5abce..2b37803 100644 --- a/main.go +++ b/main.go @@ -12,8 +12,8 @@ import ( "os/signal" "syscall" - ldap "bottin/ldapserver" message "bottin/goldap" + ldap "bottin/ldapserver" consul "github.com/hashicorp/consul/api" log "github.com/sirupsen/logrus" @@ -320,7 +320,6 @@ func (server *Server) init() error { return err } - admin_pass_str, environnement_variable_exist := os.LookupEnv("BOTTIN_DEFAULT_ADMIN_PW") if !environnement_variable_exist { admin_pass := make([]byte, 8) @@ -329,11 +328,15 @@ func (server *Server) init() error { return err } admin_pass_str = base64.RawURLEncoding.EncodeToString(admin_pass) - } else { + } else { server.logger.Debug("BOTTIN_DEFAULT_ADMIN_PW environment variable is set, using it for admin's password") } - admin_pass_hash := SSHAEncode([]byte(admin_pass_str)) + admin_pass_hash, err := SSHAEncode(admin_pass_str) + if err != nil { + server.logger.Error("can't create admin password") + panic(err) + } admin_dn := "cn=admin," + server.config.Suffix admin_attributes := Entry{ @@ -434,8 +437,8 @@ func (server *Server) handleBindInternal(state *State, r *message.BindRequest) ( } for _, hash := range passwd { - valid := SSHAMatches(hash, []byte(r.AuthenticationSimple())) - if valid { + valid, err := SSHAMatches(hash, string(r.AuthenticationSimple())) + if valid && err == nil { groups, err := server.getAttribute(string(r.Name()), ATTR_MEMBEROF) if err != nil { return ldap.LDAPResultOperationsError, err @@ -444,8 +447,32 @@ func (server *Server) handleBindInternal(state *State, r *message.BindRequest) ( user: string(r.Name()), groups: groups, } + + updatePasswordHash(string(r.AuthenticationSimple()), hash, server, string(r.Name())) + return ldap.LDAPResultSuccess, nil + } else { + return ldap.LDAPResultInvalidCredentials, fmt.Errorf("can't authenticate: %w", err) } } return ldap.LDAPResultInvalidCredentials, fmt.Errorf("No password match") } + +// Update the hash if it's not already SSHA512 +func updatePasswordHash(password string, currentHash string, server *Server, dn string) { + hashType, err := determineHashType(currentHash) + if err != nil { + server.logger.Errorf("can't determine hash type of password") + return + } + if hashType != SSHA512 { + reencodedPassword, err := SSHAEncode(password) + if err != nil { + server.logger.Errorf("can't encode password") + return + } + server.putAttributes(dn, Entry{ + ATTR_USERPASSWORD: []string{reencodedPassword}, + }) + } +} diff --git a/ssha.go b/ssha.go index 15b8e1d..202ce12 100644 --- a/ssha.go +++ b/ssha.go @@ -1,59 +1,53 @@ package main import ( - "bytes" - "crypto/rand" - "crypto/sha1" - "encoding/base64" - "fmt" - "strings" + "errors" - log "github.com/sirupsen/logrus" + "github.com/jsimonetti/pwscheme/ssha" + "github.com/jsimonetti/pwscheme/ssha256" + "github.com/jsimonetti/pwscheme/ssha512" ) -// Encode encodes the []byte of raw password -func SSHAEncode(rawPassPhrase []byte) string { - hash := makeSSHAHash(rawPassPhrase, makeSalt()) - b64 := base64.StdEncoding.EncodeToString(hash) - return fmt.Sprintf("{ssha}%s", b64) +const ( + SSHA = "{SSHA}" + SSHA256 = "{SSHA256}" + SSHA512 = "{SSHA512}" +) + +// Encode encodes the string to ssha512 +func SSHAEncode(rawPassPhrase string) (string, error) { + return ssha512.Generate(rawPassPhrase, 16) } // Matches matches the encoded password and the raw password -func SSHAMatches(encodedPassPhrase string, rawPassPhrase []byte) bool { - if !strings.EqualFold(encodedPassPhrase[:6], "{ssha}") { - return false - } - - bhash, err := base64.StdEncoding.DecodeString(encodedPassPhrase[6:]) +func SSHAMatches(encodedPassPhrase string, rawPassPhrase string) (bool, error) { + hashType, err := determineHashType(encodedPassPhrase) if err != nil { - return false + return false, errors.New("invalid password hash stored") } - salt := bhash[20:] - newssha := makeSSHAHash(rawPassPhrase, salt) - - if bytes.Compare(newssha, bhash) != 0 { - return false + switch hashType { + case SSHA: + return ssha.Validate(rawPassPhrase, encodedPassPhrase) + case SSHA256: + return ssha256.Validate(rawPassPhrase, encodedPassPhrase) + case SSHA512: + return ssha512.Validate(rawPassPhrase, encodedPassPhrase) } - return true + + return false, errors.New("no matching hash type found") } -// makeSalt make a 32 byte array containing random bytes. -func makeSalt() []byte { - sbytes := make([]byte, 32) - _, err := rand.Read(sbytes) - if err != nil { - log.Panicf("Could not read random bytes: %s", err) +func determineHashType(hash string) (string, error) { + if len(hash) >= 7 && string(hash[0:6]) == SSHA { + return SSHA, nil + } + if len(hash) >= 10 && string(hash[0:9]) == SSHA256 { + return SSHA256, nil + } + if len(hash) >= 10 && string(hash[0:9]) == SSHA512 { + return SSHA512, nil } - return sbytes -} -// makeSSHAHash make hasing using SHA-1 with salt. This is not the final output though. You need to append {SSHA} string with base64 of this hash. -func makeSSHAHash(passphrase, salt []byte) []byte { - sha := sha1.New() - sha.Write(passphrase) - sha.Write(salt) - - h := sha.Sum(nil) - return append(h, salt...) + return "", errors.New("no valid hash found") } diff --git a/write.go b/write.go index 2dd42c6..55ab5e0 100644 --- a/write.go +++ b/write.go @@ -7,8 +7,9 @@ import ( ldap "bottin/ldapserver" - consul "github.com/hashicorp/consul/api" message "bottin/goldap" + + consul "github.com/hashicorp/consul/api" ) // Generic item modification function -------- @@ -38,7 +39,7 @@ func (server *Server) putAttributes(dn string, attrs Entry) error { // Retreieve previously existing attributes, which we will use to delete // entries with the wrong case - previous_pairs, _, err := server.kv.List(prefix + "/attribute=", &server.readOpts) + previous_pairs, _, err := server.kv.List(prefix+"/attribute=", &server.readOpts) if err != nil { return err } @@ -65,6 +66,24 @@ func (server *Server) putAttributes(dn string, attrs Entry) error { } } + // if the password is not yet hashed we hash it + if k == ATTR_USERPASSWORD { + tmpValues := []string{} + for _, pw := range values { + _, err := determineHashType(pw) + if err != nil { + encodedPassword, err := SSHAEncode(pw) + if err != nil { + return err + } + tmpValues = append(tmpValues, encodedPassword) + } else { + tmpValues = append(tmpValues, pw) + } + } + values = tmpValues + } + // If we have zero values, delete associated k/v pair // Otherwise, write new values if len(values) == 0 {