Skip to content

Commit 002b47e

Browse files
Use scram-sha-256 hash if postgresql parameter password_encryption set to do so. (zalando#995)
* Use scram-sha-256 hash if postgresql parameter password_encryption set to do so. * test fixed * Refactoring * code style
1 parent ec932f8 commit 002b47e

File tree

5 files changed

+90
-17
lines changed

5 files changed

+90
-17
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/sirupsen/logrus v1.6.0
1111
github.com/stretchr/testify v1.5.1
1212
golang.org/x/mod v0.3.0 // indirect
13+
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975
1314
golang.org/x/tools v0.0.0-20200615222825-6aa8f57aacd9 // indirect
1415
gopkg.in/yaml.v2 v2.2.8
1516
k8s.io/api v0.18.3

pkg/cluster/cluster.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres
124124

125125
return fmt.Sprintf("%s-%s", e.PodName, e.ResourceVersion), nil
126126
})
127+
password_encryption, ok := pgSpec.Spec.PostgresqlParam.Parameters["password_encryption"]
128+
if !ok {
129+
password_encryption = "md5"
130+
}
127131

128132
cluster := &Cluster{
129133
Config: cfg,
@@ -135,7 +139,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres
135139
Secrets: make(map[types.UID]*v1.Secret),
136140
Services: make(map[PostgresRole]*v1.Service),
137141
Endpoints: make(map[PostgresRole]*v1.Endpoints)},
138-
userSyncStrategy: users.DefaultUserSyncStrategy{},
142+
userSyncStrategy: users.DefaultUserSyncStrategy{password_encryption},
139143
deleteOptions: metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy},
140144
podEventsQueue: podEventsQueue,
141145
KubeClient: kubeClient,

pkg/util/users/users.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const (
2828
// an existing roles of another role membership, nor it removes the already assigned flag
2929
// (except for the NOLOGIN). TODO: process other NOflags, i.e. NOSUPERUSER correctly.
3030
type DefaultUserSyncStrategy struct {
31+
PasswordEncryption string
3132
}
3233

3334
// 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
4546
}
4647
} else {
4748
r := spec.PgSyncUserRequest{}
48-
newMD5Password := util.PGUserPassword(newUser)
49+
newMD5Password := util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(newUser)
4950

5051
if dbUser.Password != newMD5Password {
5152
r.User.Password = newMD5Password
@@ -140,7 +141,7 @@ func (strategy DefaultUserSyncStrategy) createPgUser(user spec.PgUser, db *sql.D
140141
if user.Password == "" {
141142
userPassword = "PASSWORD NULL"
142143
} else {
143-
userPassword = fmt.Sprintf(passwordTemplate, util.PGUserPassword(user))
144+
userPassword = fmt.Sprintf(passwordTemplate, util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(user))
144145
}
145146
query := fmt.Sprintf(createUserSQL, user.Name, strings.Join(userFlags, " "), userPassword)
146147

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

157158
if user.Password != "" || len(user.Flags) > 0 {
158-
alterStmt := produceAlterStmt(user)
159+
alterStmt := produceAlterStmt(user, strategy.PasswordEncryption)
159160
resultStmt = append(resultStmt, alterStmt)
160161
}
161162
if len(user.MemberOf) > 0 {
@@ -174,14 +175,14 @@ func (strategy DefaultUserSyncStrategy) alterPgUser(user spec.PgUser, db *sql.DB
174175
return nil
175176
}
176177

177-
func produceAlterStmt(user spec.PgUser) string {
178+
func produceAlterStmt(user spec.PgUser, encryption string) string {
178179
// ALTER ROLE ... LOGIN ENCRYPTED PASSWORD ..
179180
result := make([]string, 0)
180181
password := user.Password
181182
flags := user.Flags
182183

183184
if password != "" {
184-
result = append(result, fmt.Sprintf(passwordTemplate, util.PGUserPassword(user)))
185+
result = append(result, fmt.Sprintf(passwordTemplate, util.NewEncryptor(encryption).PGUserPassword(user)))
185186
}
186187
if len(flags) != 0 {
187188
result = append(result, strings.Join(flags, " "))

pkg/util/util.go

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package util
22

33
import (
4+
"crypto/hmac"
45
"crypto/md5" // #nosec we need it to for PostgreSQL md5 passwords
56
cryptoRand "crypto/rand"
7+
"crypto/sha256"
8+
"encoding/base64"
69
"encoding/hex"
710
"fmt"
811
"math/big"
@@ -16,10 +19,14 @@ import (
1619
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1720

1821
"github.com/zalando/postgres-operator/pkg/spec"
22+
"golang.org/x/crypto/pbkdf2"
1923
)
2024

2125
const (
22-
md5prefix = "md5"
26+
md5prefix = "md5"
27+
scramsha256prefix = "SCRAM-SHA-256"
28+
saltlength = 16
29+
iterations = 4096
2330
)
2431

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

64-
// PGUserPassword is used to generate md5 password hash for a given user. It does nothing for already hashed passwords.
65-
func PGUserPassword(user spec.PgUser) string {
66-
if (len(user.Password) == md5.Size*2+len(md5prefix) && user.Password[:3] == md5prefix) || user.Password == "" {
71+
type Hasher func(user spec.PgUser) string
72+
type Random func(n int) string
73+
74+
type Encryptor struct {
75+
encrypt Hasher
76+
random Random
77+
}
78+
79+
func NewEncryptor(encryption string) *Encryptor {
80+
e := Encryptor{random: RandomPassword}
81+
m := map[string]Hasher{
82+
"md5": e.PGUserPasswordMD5,
83+
"scram-sha-256": e.PGUserPasswordScramSHA256,
84+
}
85+
hasher, ok := m[encryption]
86+
if !ok {
87+
hasher = e.PGUserPasswordMD5
88+
}
89+
e.encrypt = hasher
90+
return &e
91+
}
92+
93+
func (e *Encryptor) PGUserPassword(user spec.PgUser) string {
94+
if (len(user.Password) == md5.Size*2+len(md5prefix) && user.Password[:3] == md5prefix) ||
95+
(len(user.Password) > len(scramsha256prefix) && user.Password[:len(scramsha256prefix)] == scramsha256prefix) || user.Password == "" {
6796
// Avoid processing already encrypted or empty passwords
6897
return user.Password
6998
}
99+
return e.encrypt(user)
100+
}
101+
102+
func (e *Encryptor) PGUserPasswordMD5(user spec.PgUser) string {
70103
s := md5.Sum([]byte(user.Password + user.Name)) // #nosec, using md5 since PostgreSQL uses it for hashing passwords.
71104
return md5prefix + hex.EncodeToString(s[:])
72105
}
73106

107+
func (e *Encryptor) PGUserPasswordScramSHA256(user spec.PgUser) string {
108+
salt := []byte(e.random(saltlength))
109+
key := pbkdf2.Key([]byte(user.Password), salt, iterations, 32, sha256.New)
110+
mac := hmac.New(sha256.New, key)
111+
mac.Write([]byte("Server Key"))
112+
serverKey := mac.Sum(nil)
113+
mac = hmac.New(sha256.New, key)
114+
mac.Write([]byte("Client Key"))
115+
clientKey := mac.Sum(nil)
116+
storedKey := sha256.Sum256(clientKey)
117+
pass := fmt.Sprintf("%s$%v:%s$%s:%s",
118+
scramsha256prefix,
119+
iterations,
120+
base64.StdEncoding.EncodeToString(salt),
121+
base64.StdEncoding.EncodeToString(storedKey[:]),
122+
base64.StdEncoding.EncodeToString(serverKey),
123+
)
124+
return pass
125+
}
126+
74127
// Diff returns diffs between 2 objects
75128
func Diff(a, b interface{}) []string {
76129
return pretty.Diff(a, b)

pkg/util/util_test.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,27 @@ import (
1212
)
1313

1414
var pgUsers = []struct {
15-
in spec.PgUser
16-
out string
15+
in spec.PgUser
16+
outmd5 string
17+
outscramsha256 string
1718
}{{spec.PgUser{
1819
Name: "test",
1920
Password: "password",
2021
Flags: []string{},
2122
MemberOf: []string{}},
22-
"md587f77988ccb5aa917c93201ba314fcd4"},
23+
"md587f77988ccb5aa917c93201ba314fcd4", "SCRAM-SHA-256$4096:c2FsdA==$lF4cRm/Jky763CN4HtxdHnjV4Q8AWTNlKvGmEFFU8IQ=:ub8OgRsftnk2ccDMOt7ffHXNcikRkQkq1lh4xaAqrSw="},
2324
{spec.PgUser{
2425
Name: "test",
2526
Password: "md592f413f3974bdf3799bb6fecb5f9f2c6",
2627
Flags: []string{},
2728
MemberOf: []string{}},
28-
"md592f413f3974bdf3799bb6fecb5f9f2c6"}}
29+
"md592f413f3974bdf3799bb6fecb5f9f2c6", "md592f413f3974bdf3799bb6fecb5f9f2c6"},
30+
{spec.PgUser{
31+
Name: "test",
32+
Password: "SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs=",
33+
Flags: []string{},
34+
MemberOf: []string{}},
35+
"SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs=", "SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs="}}
2936

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

108115
func TestPGUserPassword(t *testing.T) {
109116
for _, tt := range pgUsers {
110-
pwd := PGUserPassword(tt.in)
111-
if pwd != tt.out {
112-
t.Errorf("PgUserPassword expected: %q, got: %q", tt.out, pwd)
117+
e := NewEncryptor("md5")
118+
pwd := e.PGUserPassword(tt.in)
119+
if pwd != tt.outmd5 {
120+
t.Errorf("PgUserPassword expected: %q, got: %q", tt.outmd5, pwd)
121+
}
122+
e = NewEncryptor("scram-sha-256")
123+
e.random = func(n int) string { return "salt" }
124+
pwd = e.PGUserPassword(tt.in)
125+
if pwd != tt.outscramsha256 {
126+
t.Errorf("PgUserPassword expected: %q, got: %q", tt.outscramsha256, pwd)
113127
}
114128
}
115129
}

0 commit comments

Comments
 (0)