Improve password hash handling

This commit is contained in:
Simon Beck 2022-02-08 17:59:59 +01:00
parent daf9474703
commit b43af83926
6 changed files with 97 additions and 60 deletions

2
go.mod
View file

@ -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
)

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/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=

View file

@ -1,10 +1,11 @@
# LDIF Export for uid=john.lemon,ou=People,dc=earthnet,dc=local
# Server: ldap (ldap)
# Search Scope: base
# Search Filter: (objectClass=*)
# Total Entries: 1
#
# Generated by phpLDAPadmin (http://phpldapadmin.sourceforge.net) on February 8, 2022 10:36 am
# Generated by phpLDAPadmin (http://phpldapadmin.sourceforge.net) on February 8, 2022 4:23 pm
# Version: 1.2.5
version: 1
@ -20,5 +21,5 @@ objectclass: organizationalPerson
objectclass: person
sn: Lemon
uid: john.lemon
userpassword: {SSHA512}j18l2W+Sa9wlka6u5dyITbO4CiCO99bTH0piiQuqM2iTo14l6DTpU
y+jaR2LbO29yNshFN5ejZh/gGIL2eBXH94/efrvGEKf
userpassword: {SSHA512}1vkCNmm7u8yqGXauYdl83ycT5BLViD1RANG8H1cXozHFqsJk8O5p/
S39diDnW4KFV7Y1L9iMM6jDRDRIevLkulUCLxg6hyXb

39
main.go
View file

@ -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},
})
}
}

76
ssha.go
View file

@ -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")
}

View file

@ -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 {