Improve password hash handling
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing

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
This commit is contained in:
Simon Beck 2022-02-08 17:59:59 +01:00
parent dbd9003714
commit f05e41c9aa
5 changed files with 93 additions and 57 deletions

2
go.mod
View file

@ -3,9 +3,9 @@ module bottin
go 1.13 go 1.13
require ( require (
github.com/go-ldap/ldap/v3 v3.3.0
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/hashicorp/consul/api v1.3.0 github.com/hashicorp/consul/api v1.3.0
github.com/jsimonetti/pwscheme v0.0.0-20220125093853-4d9895f5db73
github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus v1.4.2
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect
) )

10
go.sum
View file

@ -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/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 h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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 h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 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= 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/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 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 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 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 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= 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/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-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-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/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 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

39
main.go
View file

@ -12,8 +12,8 @@ import (
"os/signal" "os/signal"
"syscall" "syscall"
ldap "bottin/ldapserver"
message "bottin/goldap" message "bottin/goldap"
ldap "bottin/ldapserver"
consul "github.com/hashicorp/consul/api" consul "github.com/hashicorp/consul/api"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -320,7 +320,6 @@ func (server *Server) init() error {
return err return err
} }
admin_pass_str, environnement_variable_exist := os.LookupEnv("BOTTIN_DEFAULT_ADMIN_PW") admin_pass_str, environnement_variable_exist := os.LookupEnv("BOTTIN_DEFAULT_ADMIN_PW")
if !environnement_variable_exist { if !environnement_variable_exist {
admin_pass := make([]byte, 8) admin_pass := make([]byte, 8)
@ -329,11 +328,15 @@ func (server *Server) init() error {
return err return err
} }
admin_pass_str = base64.RawURLEncoding.EncodeToString(admin_pass) 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") 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_dn := "cn=admin," + server.config.Suffix
admin_attributes := Entry{ admin_attributes := Entry{
@ -434,8 +437,8 @@ func (server *Server) handleBindInternal(state *State, r *message.BindRequest) (
} }
for _, hash := range passwd { for _, hash := range passwd {
valid := SSHAMatches(hash, []byte(r.AuthenticationSimple())) valid, err := SSHAMatches(hash, string(r.AuthenticationSimple()))
if valid { if valid && err == nil {
groups, err := server.getAttribute(string(r.Name()), ATTR_MEMBEROF) groups, err := server.getAttribute(string(r.Name()), ATTR_MEMBEROF)
if err != nil { if err != nil {
return ldap.LDAPResultOperationsError, err return ldap.LDAPResultOperationsError, err
@ -444,8 +447,32 @@ func (server *Server) handleBindInternal(state *State, r *message.BindRequest) (
user: string(r.Name()), user: string(r.Name()),
groups: groups, groups: groups,
} }
updatePasswordHash(string(r.AuthenticationSimple()), hash, server, string(r.Name()))
return ldap.LDAPResultSuccess, nil return ldap.LDAPResultSuccess, nil
} else {
return ldap.LDAPResultInvalidCredentials, fmt.Errorf("can't authenticate: %w", err)
} }
} }
return ldap.LDAPResultInvalidCredentials, fmt.Errorf("No password match") 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},
})
}
}

76
ssha.go
View file

@ -1,59 +1,53 @@
package main package main
import ( import (
"bytes" "errors"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"fmt"
"strings"
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 const (
func SSHAEncode(rawPassPhrase []byte) string { SSHA = "{SSHA}"
hash := makeSSHAHash(rawPassPhrase, makeSalt()) SSHA256 = "{SSHA256}"
b64 := base64.StdEncoding.EncodeToString(hash) SSHA512 = "{SSHA512}"
return fmt.Sprintf("{ssha}%s", b64) )
// 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 // Matches matches the encoded password and the raw password
func SSHAMatches(encodedPassPhrase string, rawPassPhrase []byte) bool { func SSHAMatches(encodedPassPhrase string, rawPassPhrase string) (bool, error) {
if !strings.EqualFold(encodedPassPhrase[:6], "{ssha}") { hashType, err := determineHashType(encodedPassPhrase)
return false
}
bhash, err := base64.StdEncoding.DecodeString(encodedPassPhrase[6:])
if err != nil { if err != nil {
return false return false, errors.New("invalid password hash stored")
} }
salt := bhash[20:]
newssha := makeSSHAHash(rawPassPhrase, salt) switch hashType {
case SSHA:
if bytes.Compare(newssha, bhash) != 0 { return ssha.Validate(rawPassPhrase, encodedPassPhrase)
return false 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 determineHashType(hash string) (string, error) {
func makeSalt() []byte { if len(hash) >= 7 && string(hash[0:6]) == SSHA {
sbytes := make([]byte, 32) return SSHA, nil
_, err := rand.Read(sbytes) }
if err != nil { if len(hash) >= 10 && string(hash[0:9]) == SSHA256 {
log.Panicf("Could not read random bytes: %s", err) 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. return "", errors.New("no valid hash found")
func makeSSHAHash(passphrase, salt []byte) []byte {
sha := sha1.New()
sha.Write(passphrase)
sha.Write(salt)
h := sha.Sum(nil)
return append(h, salt...)
} }

View file

@ -7,8 +7,9 @@ import (
ldap "bottin/ldapserver" ldap "bottin/ldapserver"
consul "github.com/hashicorp/consul/api"
message "bottin/goldap" message "bottin/goldap"
consul "github.com/hashicorp/consul/api"
) )
// Generic item modification function -------- // 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 // Retreieve previously existing attributes, which we will use to delete
// entries with the wrong case // 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 { if err != nil {
return err 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 // If we have zero values, delete associated k/v pair
// Otherwise, write new values // Otherwise, write new values
if len(values) == 0 { if len(values) == 0 {