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
This commit is contained in:
parent
dbd9003714
commit
f05e41c9aa
5 changed files with 93 additions and 57 deletions
2
go.mod
2
go.mod
|
@ -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
10
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/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=
|
||||||
|
|
37
main.go
37
main.go
|
@ -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)
|
||||||
|
@ -333,7 +332,11 @@ func (server *Server) init() error {
|
||||||
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
76
ssha.go
|
@ -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...)
|
|
||||||
}
|
}
|
||||||
|
|
23
write.go
23
write.go
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue