From fcd35eb25658ab526a256571a860dbadc5fff061 Mon Sep 17 00:00:00 2001 From: Igor Yanchenko Date: Thu, 28 May 2020 12:21:01 +0200 Subject: [PATCH 1/4] Use scram-sha-256 hash if postgresql parameter password_encryption set to do so. --- go.mod | 1 + pkg/cluster/cluster.go | 6 +++++- pkg/util/users/users.go | 11 ++++++----- pkg/util/util.go | 33 ++++++++++++++++++++++++++++++--- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 041f90706..6e8cd8ef4 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 44c3e9b62..37f4be763 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -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, @@ -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, diff --git a/pkg/util/users/users.go b/pkg/util/users/users.go index 345caa001..3fa2cf495 100644 --- a/pkg/util/users/users.go +++ b/pkg/util/users/users.go @@ -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. @@ -45,7 +46,7 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM } } else { r := spec.PgSyncUserRequest{} - newMD5Password := util.PGUserPassword(newUser) + newMD5Password := util.PGUserPassword(newUser, strategy.PasswordEncryption) if dbUser.Password != newMD5Password { r.User.Password = newMD5Password @@ -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.PGUserPassword(user, strategy.PasswordEncryption)) } query := fmt.Sprintf(createUserSQL, user.Name, strings.Join(userFlags, " "), userPassword) @@ -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 { @@ -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.PGUserPassword(user, encryption))) } if len(flags) != 0 { result = append(result, strings.Join(flags, " ")) diff --git a/pkg/util/util.go b/pkg/util/util.go index ff1be4e68..b17405a59 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -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" @@ -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" + saltlenght = 16 + iterations = 4096 ) var passwordChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") @@ -62,11 +69,31 @@ 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 == "" { +func PGUserPassword(user spec.PgUser, encryption string) 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 } + if encryption == "scram-sha-256" { + salt := []byte(RandomPassword(saltlenght)) + 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 + } s := md5.Sum([]byte(user.Password + user.Name)) // #nosec, using md5 since PostgreSQL uses it for hashing passwords. return md5prefix + hex.EncodeToString(s[:]) } From 45eac113163ce92753b20086be2a44e81c92a7d2 Mon Sep 17 00:00:00 2001 From: Igor Yanchenko Date: Thu, 28 May 2020 14:30:56 +0200 Subject: [PATCH 2/4] test fixed --- pkg/util/util_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index 1f86ea1b4..62688ad61 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -107,7 +107,7 @@ func TestNameFromMeta(t *testing.T) { func TestPGUserPassword(t *testing.T) { for _, tt := range pgUsers { - pwd := PGUserPassword(tt.in) + pwd := PGUserPassword(tt.in, "md5") if pwd != tt.out { t.Errorf("PgUserPassword expected: %q, got: %q", tt.out, pwd) } From 7d74b06734daec286a737b2c14b3ed71e8f9b0f0 Mon Sep 17 00:00:00 2001 From: Igor Yanchenko Date: Thu, 28 May 2020 12:21:01 +0200 Subject: [PATCH 3/4] Refactoring --- pkg/util/users/users.go | 6 ++-- pkg/util/util.go | 69 ++++++++++++++++++++++++++++------------- pkg/util/util_test.go | 26 ++++++++++++---- 3 files changed, 70 insertions(+), 31 deletions(-) diff --git a/pkg/util/users/users.go b/pkg/util/users/users.go index 3fa2cf495..166e90264 100644 --- a/pkg/util/users/users.go +++ b/pkg/util/users/users.go @@ -46,7 +46,7 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM } } else { r := spec.PgSyncUserRequest{} - newMD5Password := util.PGUserPassword(newUser, strategy.PasswordEncryption) + newMD5Password := util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(newUser) if dbUser.Password != newMD5Password { r.User.Password = newMD5Password @@ -141,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, strategy.PasswordEncryption)) + userPassword = fmt.Sprintf(passwordTemplate, util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(user)) } query := fmt.Sprintf(createUserSQL, user.Name, strings.Join(userFlags, " "), userPassword) @@ -182,7 +182,7 @@ func produceAlterStmt(user spec.PgUser, encryption string) string { flags := user.Flags if password != "" { - result = append(result, fmt.Sprintf(passwordTemplate, util.PGUserPassword(user, encryption))) + result = append(result, fmt.Sprintf(passwordTemplate, util.NewEncryptor(encryption).PGUserPassword(user))) } if len(flags) != 0 { result = append(result, strings.Join(flags, " ")) diff --git a/pkg/util/util.go b/pkg/util/util.go index b17405a59..be729b63e 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -25,7 +25,7 @@ import ( const ( md5prefix = "md5" scramsha256prefix = "SCRAM-SHA-256" - saltlenght = 16 + saltlength = 16 iterations = 4096 ) @@ -67,37 +67,62 @@ func NameFromMeta(meta metav1.ObjectMeta) spec.NamespacedName { Name: meta.Name, } } +type Hasher func(user spec.PgUser) string +type Random func(n int) string -// PGUserPassword is used to generate md5 password hash for a given user. It does nothing for already hashed passwords. -func PGUserPassword(user spec.PgUser, encryption string) 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 } - if encryption == "scram-sha-256" { - salt := []byte(RandomPassword(saltlenght)) - 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 - } + 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) diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index 62688ad61..fa6ffa357 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -13,19 +13,26 @@ import ( var pgUsers = []struct { in spec.PgUser - out string + 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{} @@ -107,9 +114,16 @@ func TestNameFromMeta(t *testing.T) { func TestPGUserPassword(t *testing.T) { for _, tt := range pgUsers { - pwd := PGUserPassword(tt.in, "md5") - 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) } } } From 99a2c4359bef1a8b7057b3f69535b546bab23851 Mon Sep 17 00:00:00 2001 From: Igor Yanchenko Date: Fri, 29 May 2020 13:13:47 +0200 Subject: [PATCH 4/4] code style --- pkg/util/util.go | 5 +++-- pkg/util/util_test.go | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/util/util.go b/pkg/util/util.go index be729b63e..abb9be01f 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -67,16 +67,17 @@ func NameFromMeta(meta metav1.ObjectMeta) spec.NamespacedName { Name: meta.Name, } } + type Hasher func(user spec.PgUser) string type Random func(n int) string type Encryptor struct { encrypt Hasher - random Random + random Random } func NewEncryptor(encryption string) *Encryptor { - e := Encryptor{random:RandomPassword} + e := Encryptor{random: RandomPassword} m := map[string]Hasher{ "md5": e.PGUserPasswordMD5, "scram-sha-256": e.PGUserPasswordScramSHA256, diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index fa6ffa357..a9d25112b 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -12,8 +12,8 @@ import ( ) var pgUsers = []struct { - in spec.PgUser - outmd5 string + in spec.PgUser + outmd5 string outscramsha256 string }{{spec.PgUser{ Name: "test", @@ -120,7 +120,7 @@ func TestPGUserPassword(t *testing.T) { t.Errorf("PgUserPassword expected: %q, got: %q", tt.outmd5, pwd) } e = NewEncryptor("scram-sha-256") - e.random = func(n int) string {return "salt"} + 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)