Skip to content

Commit

Permalink
Use scram-sha-256 hash if postgresql parameter password_encryption se…
Browse files Browse the repository at this point in the history
…t to do so. (zalando#995)

* Use scram-sha-256 hash if postgresql parameter password_encryption set to do so.

* test fixed

* Refactoring

* code style
  • Loading branch information
yanchenko-igor authored Jul 16, 2020
1 parent ec932f8 commit 002b47e
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 17 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/sirupsen/logrus v1.6.0
github.com/stretchr/testify v1.5.1
golang.org/x/mod v0.3.0 // indirect
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975
golang.org/x/tools v0.0.0-20200615222825-6aa8f57aacd9 // indirect
gopkg.in/yaml.v2 v2.2.8
k8s.io/api v0.18.3
Expand Down
6 changes: 5 additions & 1 deletion pkg/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres

return fmt.Sprintf("%s-%s", e.PodName, e.ResourceVersion), nil
})
password_encryption, ok := pgSpec.Spec.PostgresqlParam.Parameters["password_encryption"]
if !ok {
password_encryption = "md5"
}

cluster := &Cluster{
Config: cfg,
Expand All @@ -135,7 +139,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres
Secrets: make(map[types.UID]*v1.Secret),
Services: make(map[PostgresRole]*v1.Service),
Endpoints: make(map[PostgresRole]*v1.Endpoints)},
userSyncStrategy: users.DefaultUserSyncStrategy{},
userSyncStrategy: users.DefaultUserSyncStrategy{password_encryption},
deleteOptions: metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy},
podEventsQueue: podEventsQueue,
KubeClient: kubeClient,
Expand Down
11 changes: 6 additions & 5 deletions pkg/util/users/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
// an existing roles of another role membership, nor it removes the already assigned flag
// (except for the NOLOGIN). TODO: process other NOflags, i.e. NOSUPERUSER correctly.
type DefaultUserSyncStrategy struct {
PasswordEncryption string
}

// ProduceSyncRequests figures out the types of changes that need to happen with the given users.
Expand All @@ -45,7 +46,7 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM
}
} else {
r := spec.PgSyncUserRequest{}
newMD5Password := util.PGUserPassword(newUser)
newMD5Password := util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(newUser)

if dbUser.Password != newMD5Password {
r.User.Password = newMD5Password
Expand Down Expand Up @@ -140,7 +141,7 @@ func (strategy DefaultUserSyncStrategy) createPgUser(user spec.PgUser, db *sql.D
if user.Password == "" {
userPassword = "PASSWORD NULL"
} else {
userPassword = fmt.Sprintf(passwordTemplate, util.PGUserPassword(user))
userPassword = fmt.Sprintf(passwordTemplate, util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(user))
}
query := fmt.Sprintf(createUserSQL, user.Name, strings.Join(userFlags, " "), userPassword)

Expand All @@ -155,7 +156,7 @@ func (strategy DefaultUserSyncStrategy) alterPgUser(user spec.PgUser, db *sql.DB
var resultStmt []string

if user.Password != "" || len(user.Flags) > 0 {
alterStmt := produceAlterStmt(user)
alterStmt := produceAlterStmt(user, strategy.PasswordEncryption)
resultStmt = append(resultStmt, alterStmt)
}
if len(user.MemberOf) > 0 {
Expand All @@ -174,14 +175,14 @@ func (strategy DefaultUserSyncStrategy) alterPgUser(user spec.PgUser, db *sql.DB
return nil
}

func produceAlterStmt(user spec.PgUser) string {
func produceAlterStmt(user spec.PgUser, encryption string) string {
// ALTER ROLE ... LOGIN ENCRYPTED PASSWORD ..
result := make([]string, 0)
password := user.Password
flags := user.Flags

if password != "" {
result = append(result, fmt.Sprintf(passwordTemplate, util.PGUserPassword(user)))
result = append(result, fmt.Sprintf(passwordTemplate, util.NewEncryptor(encryption).PGUserPassword(user)))
}
if len(flags) != 0 {
result = append(result, strings.Join(flags, " "))
Expand Down
61 changes: 57 additions & 4 deletions pkg/util/util.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package util

import (
"crypto/hmac"
"crypto/md5" // #nosec we need it to for PostgreSQL md5 passwords
cryptoRand "crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"math/big"
Expand All @@ -16,10 +19,14 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/zalando/postgres-operator/pkg/spec"
"golang.org/x/crypto/pbkdf2"
)

const (
md5prefix = "md5"
md5prefix = "md5"
scramsha256prefix = "SCRAM-SHA-256"
saltlength = 16
iterations = 4096
)

var passwordChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
Expand Down Expand Up @@ -61,16 +68,62 @@ func NameFromMeta(meta metav1.ObjectMeta) spec.NamespacedName {
}
}

// PGUserPassword is used to generate md5 password hash for a given user. It does nothing for already hashed passwords.
func PGUserPassword(user spec.PgUser) string {
if (len(user.Password) == md5.Size*2+len(md5prefix) && user.Password[:3] == md5prefix) || user.Password == "" {
type Hasher func(user spec.PgUser) string
type Random func(n int) string

type Encryptor struct {
encrypt Hasher
random Random
}

func NewEncryptor(encryption string) *Encryptor {
e := Encryptor{random: RandomPassword}
m := map[string]Hasher{
"md5": e.PGUserPasswordMD5,
"scram-sha-256": e.PGUserPasswordScramSHA256,
}
hasher, ok := m[encryption]
if !ok {
hasher = e.PGUserPasswordMD5
}
e.encrypt = hasher
return &e
}

func (e *Encryptor) PGUserPassword(user spec.PgUser) string {
if (len(user.Password) == md5.Size*2+len(md5prefix) && user.Password[:3] == md5prefix) ||
(len(user.Password) > len(scramsha256prefix) && user.Password[:len(scramsha256prefix)] == scramsha256prefix) || user.Password == "" {
// Avoid processing already encrypted or empty passwords
return user.Password
}
return e.encrypt(user)
}

func (e *Encryptor) PGUserPasswordMD5(user spec.PgUser) string {
s := md5.Sum([]byte(user.Password + user.Name)) // #nosec, using md5 since PostgreSQL uses it for hashing passwords.
return md5prefix + hex.EncodeToString(s[:])
}

func (e *Encryptor) PGUserPasswordScramSHA256(user spec.PgUser) string {
salt := []byte(e.random(saltlength))
key := pbkdf2.Key([]byte(user.Password), salt, iterations, 32, sha256.New)
mac := hmac.New(sha256.New, key)
mac.Write([]byte("Server Key"))
serverKey := mac.Sum(nil)
mac = hmac.New(sha256.New, key)
mac.Write([]byte("Client Key"))
clientKey := mac.Sum(nil)
storedKey := sha256.Sum256(clientKey)
pass := fmt.Sprintf("%s$%v:%s$%s:%s",
scramsha256prefix,
iterations,
base64.StdEncoding.EncodeToString(salt),
base64.StdEncoding.EncodeToString(storedKey[:]),
base64.StdEncoding.EncodeToString(serverKey),
)
return pass
}

// Diff returns diffs between 2 objects
func Diff(a, b interface{}) []string {
return pretty.Diff(a, b)
Expand Down
28 changes: 21 additions & 7 deletions pkg/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,27 @@ import (
)

var pgUsers = []struct {
in spec.PgUser
out string
in spec.PgUser
outmd5 string
outscramsha256 string
}{{spec.PgUser{
Name: "test",
Password: "password",
Flags: []string{},
MemberOf: []string{}},
"md587f77988ccb5aa917c93201ba314fcd4"},
"md587f77988ccb5aa917c93201ba314fcd4", "SCRAM-SHA-256$4096:c2FsdA==$lF4cRm/Jky763CN4HtxdHnjV4Q8AWTNlKvGmEFFU8IQ=:ub8OgRsftnk2ccDMOt7ffHXNcikRkQkq1lh4xaAqrSw="},
{spec.PgUser{
Name: "test",
Password: "md592f413f3974bdf3799bb6fecb5f9f2c6",
Flags: []string{},
MemberOf: []string{}},
"md592f413f3974bdf3799bb6fecb5f9f2c6"}}
"md592f413f3974bdf3799bb6fecb5f9f2c6", "md592f413f3974bdf3799bb6fecb5f9f2c6"},
{spec.PgUser{
Name: "test",
Password: "SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs=",
Flags: []string{},
MemberOf: []string{}},
"SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs=", "SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs="}}

var prettyDiffTest = []struct {
inA interface{}
Expand Down Expand Up @@ -107,9 +114,16 @@ func TestNameFromMeta(t *testing.T) {

func TestPGUserPassword(t *testing.T) {
for _, tt := range pgUsers {
pwd := PGUserPassword(tt.in)
if pwd != tt.out {
t.Errorf("PgUserPassword expected: %q, got: %q", tt.out, pwd)
e := NewEncryptor("md5")
pwd := e.PGUserPassword(tt.in)
if pwd != tt.outmd5 {
t.Errorf("PgUserPassword expected: %q, got: %q", tt.outmd5, pwd)
}
e = NewEncryptor("scram-sha-256")
e.random = func(n int) string { return "salt" }
pwd = e.PGUserPassword(tt.in)
if pwd != tt.outscramsha256 {
t.Errorf("PgUserPassword expected: %q, got: %q", tt.outscramsha256, pwd)
}
}
}
Expand Down

0 comments on commit 002b47e

Please sign in to comment.