Skip to content

Commit 5f26fd6

Browse files
committed
Don't store plaintext passwords
Rather store hashed passwords in the way mysql server does. here we: - update CredentialProvider to return new Credential struct - Credential includes the plugin that the user was created with - update InMemoryProvider to handle hashing of passwords and add default auth method to make usage backwards compatible - update server authentication to use mysql server methods of comparing hashes rather than relying on having the plaintext password available - rework the password negotiation to switch plugin type to match the stored credentials - add hashing and comparison functions for the above where missing from existing libraries
1 parent 31aad20 commit 5f26fd6

10 files changed

+325
-149
lines changed

client/auth.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ func (c *Conn) genAuthResponse(authData []byte) ([]byte, bool, error) {
154154
// password hashing
155155
switch c.authPluginName {
156156
case mysql.AUTH_NATIVE_PASSWORD:
157-
return mysql.CalcPassword(authData[:20], []byte(c.password)), false, nil
157+
return mysql.CalcNativePassword(authData[:20], []byte(c.password)), false, nil
158158
case mysql.AUTH_CACHING_SHA2_PASSWORD:
159-
return mysql.CalcCachingSha2Password(authData, c.password), false, nil
159+
return mysql.CalcCachingSha2Password(authData, []byte(c.password)), false, nil
160160
case mysql.AUTH_CLEAR_PASSWORD:
161161
return []byte(c.password), true, nil
162162
case mysql.AUTH_SHA256_PASSWORD:

mysql/util.go

Lines changed: 164 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"crypto/sha1"
1010
"crypto/sha256"
1111
"crypto/sha512"
12+
"crypto/subtle"
1213
"encoding/binary"
14+
"encoding/hex"
1315
"fmt"
1416
"io"
1517
mrand "math/rand"
@@ -29,7 +31,7 @@ func Pstack() string {
2931
return string(buf[0:n])
3032
}
3133

32-
func CalcPassword(scramble, password []byte) []byte {
34+
func CalcNativePassword(scramble, password []byte) []byte {
3335
if len(password) == 0 {
3436
return nil
3537
}
@@ -39,35 +41,100 @@ func CalcPassword(scramble, password []byte) []byte {
3941
crypt.Write(password)
4042
stage1 := crypt.Sum(nil)
4143

42-
// scrambleHash = SHA1(scramble + SHA1(stage1Hash))
43-
// inner Hash
44+
// stage2Hash = SHA1(stage1Hash)
4445
crypt.Reset()
4546
crypt.Write(stage1)
46-
hash := crypt.Sum(nil)
47+
stage2 := crypt.Sum(nil)
4748

48-
// outer Hash
49+
// scrambleHash = SHA1(scramble + stage2Hash)
4950
crypt.Reset()
5051
crypt.Write(scramble)
51-
crypt.Write(hash)
52-
scramble = crypt.Sum(nil)
52+
crypt.Write(stage2)
53+
scrambleHash := crypt.Sum(nil)
5354

5455
// token = scrambleHash XOR stage1Hash
55-
for i := range scramble {
56-
scramble[i] ^= stage1[i]
56+
return Xor(scrambleHash, stage1)
57+
}
58+
59+
func Xor(hash1 []byte, hash2 []byte) []byte {
60+
for i := range hash1 {
61+
hash1[i] ^= hash2[i]
5762
}
58-
return scramble
63+
return hash1
64+
}
65+
66+
// hash_stage1 = xor(reply, sha1(public_seed, hash_stage2))
67+
func Stage1FromReply(scramble []byte, seed []byte, stage2 []byte) []byte {
68+
crypt := sha1.New()
69+
crypt.Write(seed)
70+
crypt.Write(stage2)
71+
seededHash := crypt.Sum(nil)
72+
73+
return Xor(scramble, seededHash)
74+
}
75+
76+
// FROM vitess.io/vitess/go/mysql/auth_server.go
77+
// DecodePasswordHex decodes the standard format used by MySQL
78+
// for 4.1 style password hashes. It drops the optionally leading * before
79+
// decoding the rest as a hex encoded string.
80+
func DecodePasswordHex(hexEncodedPassword string) ([]byte, error) {
81+
if hexEncodedPassword[0] == '*' {
82+
hexEncodedPassword = hexEncodedPassword[1:]
83+
}
84+
return hex.DecodeString(hexEncodedPassword)
85+
}
86+
87+
// EncodePasswordHex encodes to the standard format used by MySQL
88+
// adds the optionally leading * to the hashed password
89+
func EncodePasswordHex(passwordHash []byte) string {
90+
hexstr := strings.ToUpper(hex.EncodeToString(passwordHash))
91+
return "*" + hexstr
92+
}
93+
94+
// NativePasswordHash = sha1(sha1(password))
95+
func NativePasswordHash(password []byte) []byte {
96+
if len(password) == 0 {
97+
return nil
98+
}
99+
100+
// stage1Hash = SHA1(password)
101+
crypt := sha1.New()
102+
crypt.Write(password)
103+
stage1 := crypt.Sum(nil)
104+
105+
// stage2Hash = SHA1(stage1Hash)
106+
crypt.Reset()
107+
crypt.Write(stage1)
108+
return crypt.Sum(nil)
109+
}
110+
111+
func CompareNativePassword(reply []byte, stored []byte, seed []byte) bool {
112+
if len(stored) == 0 {
113+
return false
114+
}
115+
116+
// hash_stage1 = xor(reply, sha1(public_seed, hash_stage2))
117+
stage1 := Stage1FromReply(reply, seed, stored)
118+
// andidate_hash2 = sha1(hash_stage1)
119+
crypt := sha1.New()
120+
crypt.Write(stage1)
121+
stage2 := crypt.Sum(nil)
122+
123+
// check(candidate_hash2 == hash_stage2)
124+
// use ConstantTimeCompare to mitigate timing based attacks
125+
return subtle.ConstantTimeCompare(stage2, stored) == 1
59126
}
60127

61128
// CalcCachingSha2Password: Hash password using MySQL 8+ method (SHA256)
62-
func CalcCachingSha2Password(scramble []byte, password string) []byte {
129+
func CalcCachingSha2Password(scramble []byte, password []byte) []byte {
63130
if len(password) == 0 {
64131
return nil
65132
}
66133

67134
// XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble))
68135

69136
crypt := sha256.New()
70-
crypt.Write([]byte(password))
137+
crypt.Write(password)
71138
message1 := crypt.Sum(nil)
72139

73140
crypt.Reset()
@@ -135,6 +202,91 @@ func EncryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte,
135202
return rsa.EncryptOAEP(sha1v, rand.Reader, pub, plain, nil)
136203
}
137204

205+
const (
206+
SALT_LENGTH = 16
207+
ITERATION_MULTIPLIER = 1000
208+
SHA256_PASSWORD_ITERATIONS = 5
209+
)
210+
211+
// generateUserSalt generate salt of given length for sha256_password hash
212+
func generateUserSalt(length int) ([]byte, error) {
213+
// Generate a random salt of the given length
214+
// Implement this function for your project
215+
salt := make([]byte, length)
216+
_, err := rand.Read(salt)
217+
if err != nil {
218+
return []byte(""), err
219+
}
220+
221+
// Restrict to 7-bit to avoid multi-byte UTF-8
222+
for i := range salt {
223+
salt[i] = salt[i] &^ 128
224+
for salt[i] == 36 || salt[i] == 0 { // '$' or NUL
225+
newval := make([]byte, 1)
226+
_, err := rand.Read(newval)
227+
if err != nil {
228+
return []byte(""), err
229+
}
230+
salt[i] = newval[0] &^ 128
231+
}
232+
}
233+
return salt, nil
234+
}
235+
236+
// hashCrypt256 salt and hash a password the given number of iterations
237+
func hashCrypt256(source, salt string, iterations uint64) (string, error) {
238+
actualIterations := iterations * ITERATION_MULTIPLIER
239+
hashInput := []byte(source + salt)
240+
var hash [32]byte
241+
for i := uint64(0); i < actualIterations; i++ {
242+
h := sha256.New()
243+
h.Write(hashInput)
244+
hash = sha256.Sum256(h.Sum(nil))
245+
hashInput = hash[:]
246+
}
247+
248+
hashHex := hex.EncodeToString(hash[:])
249+
digest := fmt.Sprintf("$%d$%s$%s", iterations, salt, hashHex)
250+
return digest, nil
251+
}
252+
253+
// Check256HashingPassword compares a password to a hash for sha256_password
254+
// rather than trying to recreate just the hash we recreate the full hash
255+
// and use that for comparison
256+
func Check256HashingPassword(pwhash []byte, password string) (bool, error) {
257+
pwHashParts := bytes.Split(pwhash, []byte("$"))
258+
if len(pwHashParts) != 4 {
259+
return false, errors.New("failed to decode hash parts")
260+
}
261+
262+
iterationsPart := pwHashParts[1]
263+
if len(iterationsPart) == 0 {
264+
return false, errors.New("iterations part is empty")
265+
}
266+
267+
iterations, err := strconv.ParseUint(string(iterationsPart), 10, 64)
268+
if err != nil {
269+
return false, errors.New("failed to decode iterations")
270+
}
271+
salt := pwHashParts[2][:SALT_LENGTH]
272+
273+
newHash, err := hashCrypt256(password, string(salt), iterations)
274+
if err != nil {
275+
return false, err
276+
}
277+
278+
return bytes.Equal(pwhash, []byte(newHash)), nil
279+
}
280+
281+
// NewSha256PasswordHash creates a new password hash for sha256_password
282+
func NewSha256PasswordHash(pwd string) (string, error) {
283+
salt, err := generateUserSalt(SALT_LENGTH)
284+
if err != nil {
285+
return "", err
286+
}
287+
return hashCrypt256(pwd, string(salt), SHA256_PASSWORD_ITERATIONS)
288+
}
289+
138290
func DecompressMariadbData(data []byte) ([]byte, error) {
139291
// algorithm always 0=zlib
140292
// algorithm := (data[pos] & 0x07) >> 4

0 commit comments

Comments
 (0)