Passed
Pull Request — master (#23)
by Frank
04:26 queued 02:18
created

mysql.*mysqlConn.handleAuthResult   F

Complexity

Conditions 29

Size

Total Lines 122
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 29
eloc 73
nop 2
dl 0
loc 122
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like mysql.*mysqlConn.handleAuthResult often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
//
3
// Copyright 2018 The Go-MySQL-Driver Authors. All rights reserved.
4
//
5
// This Source Code Form is subject to the terms of the Mozilla Public
6
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
// You can obtain one at http://mozilla.org/MPL/2.0/.
8
9
package mysql
10
11
import (
12
	"crypto/rand"
13
	"crypto/rsa"
14
	"crypto/sha1"
15
	"crypto/sha256"
16
	"crypto/x509"
17
	"encoding/pem"
18
	"sync"
19
)
20
21
// server pub keys registry
22
var (
23
	serverPubKeyLock     sync.RWMutex
24
	serverPubKeyRegistry map[string]*rsa.PublicKey
25
)
26
27
// RegisterServerPubKey registers a server RSA public key which can be used to
28
// send data in a secure manner to the server without receiving the public key
29
// in a potentially insecure way from the server first.
30
// Registered keys can afterwards be used adding serverPubKey=<name> to the DSN.
31
//
32
// Note: The provided rsa.PublicKey instance is exclusively owned by the driver
33
// after registering it and may not be modified.
34
//
35
//  data, err := ioutil.ReadFile("mykey.pem")
36
//  if err != nil {
37
//  	log.Fatal(err)
38
//  }
39
//
40
//  block, _ := pem.Decode(data)
41
//  if block == nil || block.Type != "PUBLIC KEY" {
42
//  	log.Fatal("failed to decode PEM block containing public key")
43
//  }
44
//
45
//  pub, err := x509.ParsePKIXPublicKey(block.Bytes)
46
//  if err != nil {
47
//  	log.Fatal(err)
48
//  }
49
//
50
//  if rsaPubKey, ok := pub.(*rsa.PublicKey); ok {
51
//  	mysql.RegisterServerPubKey("mykey", rsaPubKey)
52
//  } else {
53
//  	log.Fatal("not a RSA public key")
54
//  }
55
//
56
func RegisterServerPubKey(name string, pubKey *rsa.PublicKey) {
57
	serverPubKeyLock.Lock()
58
	if serverPubKeyRegistry == nil {
59
		serverPubKeyRegistry = make(map[string]*rsa.PublicKey)
60
	}
61
62
	serverPubKeyRegistry[name] = pubKey
63
	serverPubKeyLock.Unlock()
64
}
65
66
// DeregisterServerPubKey removes the public key registered with the given name.
67
func DeregisterServerPubKey(name string) {
68
	serverPubKeyLock.Lock()
69
	if serverPubKeyRegistry != nil {
70
		delete(serverPubKeyRegistry, name)
71
	}
72
	serverPubKeyLock.Unlock()
73
}
74
75
func getServerPubKey(name string) (pubKey *rsa.PublicKey) {
76
	serverPubKeyLock.RLock()
77
	if v, ok := serverPubKeyRegistry[name]; ok {
78
		pubKey = v
79
	}
80
	serverPubKeyLock.RUnlock()
81
	return
82
}
83
84
// Hash password using pre 4.1 (old password) method
85
// https://github.com/atcurtis/mariadb/blob/master/mysys/my_rnd.c
86
type myRnd struct {
87
	seed1, seed2 uint32
88
}
89
90
const myRndMaxVal = 0x3FFFFFFF
91
92
// Pseudo random number generator
93
func newMyRnd(seed1, seed2 uint32) *myRnd {
94
	return &myRnd{
95
		seed1: seed1 % myRndMaxVal,
96
		seed2: seed2 % myRndMaxVal,
97
	}
98
}
99
100
// Tested to be equivalent to MariaDB's floating point variant
101
// http://play.golang.org/p/QHvhd4qved
102
// http://play.golang.org/p/RG0q4ElWDx
103
func (r *myRnd) NextByte() byte {
104
	r.seed1 = (r.seed1*3 + r.seed2) % myRndMaxVal
105
	r.seed2 = (r.seed1 + r.seed2 + 33) % myRndMaxVal
106
107
	return byte(uint64(r.seed1) * 31 / myRndMaxVal)
108
}
109
110
// Generate binary hash from byte string using insecure pre 4.1 method
111
func pwHash(password []byte) (result [2]uint32) {
112
	var add uint32 = 7
113
	var tmp uint32
114
115
	result[0] = 1345345333
116
	result[1] = 0x12345671
117
118
	for _, c := range password {
119
		// skip spaces and tabs in password
120
		if c == ' ' || c == '\t' {
121
			continue
122
		}
123
124
		tmp = uint32(c)
125
		result[0] ^= (((result[0] & 63) + add) * tmp) + (result[0] << 8)
126
		result[1] += (result[1] << 8) ^ result[0]
127
		add += tmp
128
	}
129
130
	// Remove sign bit (1<<31)-1)
131
	result[0] &= 0x7FFFFFFF
132
	result[1] &= 0x7FFFFFFF
133
134
	return
135
}
136
137
// Hash password using insecure pre 4.1 method
138
func scrambleOldPassword(scramble []byte, password string) []byte {
139
	if len(password) == 0 {
140
		return nil
141
	}
142
143
	scramble = scramble[:8]
144
145
	hashPw := pwHash([]byte(password))
146
	hashSc := pwHash(scramble)
147
148
	r := newMyRnd(hashPw[0]^hashSc[0], hashPw[1]^hashSc[1])
149
150
	var out [8]byte
151
	for i := range out {
152
		out[i] = r.NextByte() + 64
153
	}
154
155
	mask := r.NextByte()
156
	for i := range out {
157
		out[i] ^= mask
158
	}
159
160
	return out[:]
161
}
162
163
// Hash password using 4.1+ method (SHA1)
164
func scramblePassword(scramble []byte, password string) []byte {
165
	if len(password) == 0 {
166
		return nil
167
	}
168
169
	// stage1Hash = SHA1(password)
170
	crypt := sha1.New()
171
	crypt.Write([]byte(password))
172
	stage1 := crypt.Sum(nil)
173
174
	// scrambleHash = SHA1(scramble + SHA1(stage1Hash))
175
	// inner Hash
176
	crypt.Reset()
177
	crypt.Write(stage1)
178
	hash := crypt.Sum(nil)
179
180
	// outer Hash
181
	crypt.Reset()
182
	crypt.Write(scramble)
183
	crypt.Write(hash)
184
	scramble = crypt.Sum(nil)
185
186
	// token = scrambleHash XOR stage1Hash
187
	for i := range scramble {
188
		scramble[i] ^= stage1[i]
189
	}
190
	return scramble
191
}
192
193
// Hash password using MySQL 8+ method (SHA256)
194
func scrambleSHA256Password(scramble []byte, password string) []byte {
195
	if len(password) == 0 {
196
		return nil
197
	}
198
199
	// XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble))
200
201
	crypt := sha256.New()
202
	crypt.Write([]byte(password))
203
	message1 := crypt.Sum(nil)
204
205
	crypt.Reset()
206
	crypt.Write(message1)
207
	message1Hash := crypt.Sum(nil)
208
209
	crypt.Reset()
210
	crypt.Write(message1Hash)
211
	crypt.Write(scramble)
212
	message2 := crypt.Sum(nil)
213
214
	for i := range message1 {
215
		message1[i] ^= message2[i]
216
	}
217
218
	return message1
219
}
220
221
func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, error) {
222
	plain := make([]byte, len(password)+1)
223
	copy(plain, password)
224
	for i := range plain {
225
		j := i % len(seed)
226
		plain[i] ^= seed[j]
227
	}
228
	sha1 := sha1.New()
229
	return rsa.EncryptOAEP(sha1, rand.Reader, pub, plain, nil)
230
}
231
232
func (mc *mysqlConn) sendEncryptedPassword(seed []byte, pub *rsa.PublicKey) error {
233
	enc, err := encryptPassword(mc.cfg.Passwd, seed, pub)
234
	if err != nil {
235
		return err
236
	}
237
	return mc.writeAuthSwitchPacket(enc)
238
}
239
240
func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) {
241
	switch plugin {
242
	case "caching_sha2_password":
243
		authResp := scrambleSHA256Password(authData, mc.cfg.Passwd)
244
		return authResp, nil
245
246
	case "mysql_old_password":
247
		if !mc.cfg.AllowOldPasswords {
248
			return nil, ErrOldPassword
249
		}
250
		// Note: there are edge cases where this should work but doesn't;
251
		// this is currently "wontfix":
252
		// https://github.com/go-sql-driver/mysql/issues/184
253
		authResp := append(scrambleOldPassword(authData[:8], mc.cfg.Passwd), 0)
254
		return authResp, nil
255
256
	case "mysql_clear_password":
257
		if !mc.cfg.AllowCleartextPasswords {
258
			return nil, ErrCleartextPassword
259
		}
260
		// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
261
		// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
262
		return append([]byte(mc.cfg.Passwd), 0), nil
263
264
	case "mysql_native_password":
265
		if !mc.cfg.AllowNativePasswords {
266
			return nil, ErrNativePassword
267
		}
268
		// https://dev.mysql.com/doc/internals/en/secure-password-authentication.html
269
		// Native password authentication only need and will need 20-byte challenge.
270
		authResp := scramblePassword(authData[:20], mc.cfg.Passwd)
271
		return authResp, nil
272
273
	case "sha256_password":
274
		if len(mc.cfg.Passwd) == 0 {
275
			return []byte{0}, nil
276
		}
277
		if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
278
			// write cleartext auth packet
279
			return append([]byte(mc.cfg.Passwd), 0), nil
280
		}
281
282
		pubKey := mc.cfg.pubKey
283
		if pubKey == nil {
284
			// request public key from server
285
			return []byte{1}, nil
286
		}
287
288
		// encrypted password
289
		enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey)
290
		return enc, err
291
292
	default:
293
		errLog.Print("unknown auth plugin:", plugin)
294
		return nil, ErrUnknownPlugin
295
	}
296
}
297
298
func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
299
	// Read Result Packet
300
	authData, newPlugin, err := mc.readAuthResult()
301
	if err != nil {
302
		return err
303
	}
304
305
	// handle auth plugin switch, if requested
306
	if newPlugin != "" {
307
		// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
308
		// sent and we have to keep using the cipher sent in the init packet.
309
		if authData == nil {
310
			authData = oldAuthData
311
		} else {
312
			// copy data from read buffer to owned slice
313
			copy(oldAuthData, authData)
314
		}
315
316
		plugin = newPlugin
317
318
		authResp, err := mc.auth(authData, plugin)
319
		if err != nil {
320
			return err
321
		}
322
		if err = mc.writeAuthSwitchPacket(authResp); err != nil {
323
			return err
324
		}
325
326
		// Read Result Packet
327
		authData, newPlugin, err = mc.readAuthResult()
328
		if err != nil {
329
			return err
330
		}
331
332
		// Do not allow to change the auth plugin more than once
333
		if newPlugin != "" {
334
			return ErrMalformPkt
335
		}
336
	}
337
338
	switch plugin {
339
340
	// https://insidemysql.com/preparing-your-community-connector-for-mysql-8-part-2-sha256/
341
	case "caching_sha2_password":
342
		switch len(authData) {
343
		case 0:
344
			return nil // auth successful
345
		case 1:
346
			switch authData[0] {
347
			case cachingSha2PasswordFastAuthSuccess:
348
				if err = mc.readResultOK(); err == nil {
349
					return nil // auth successful
350
				}
351
352
			case cachingSha2PasswordPerformFullAuthentication:
353
				if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
354
					// write cleartext auth packet
355
					err = mc.writeAuthSwitchPacket(append([]byte(mc.cfg.Passwd), 0))
356
					if err != nil {
357
						return err
358
					}
359
				} else {
360
					pubKey := mc.cfg.pubKey
361
					if pubKey == nil {
362
						// request public key from server
363
						data := mc.buf.takeSmallBuffer(4 + 1)
364
						data[4] = cachingSha2PasswordRequestPublicKey
365
						mc.writePacket(data)
366
367
						// parse public key
368
						data, err := mc.readPacket()
369
						if err != nil {
370
							return err
371
						}
372
373
						block, _ := pem.Decode(data[1:])
374
						pkix, err := x509.ParsePKIXPublicKey(block.Bytes)
375
						if err != nil {
376
							return err
377
						}
378
						pubKey = pkix.(*rsa.PublicKey)
379
					}
380
381
					// send encrypted password
382
					err = mc.sendEncryptedPassword(oldAuthData, pubKey)
383
					if err != nil {
384
						return err
385
					}
386
				}
387
				return mc.readResultOK()
388
389
			default:
390
				return ErrMalformPkt
391
			}
392
		default:
393
			return ErrMalformPkt
394
		}
395
396
	case "sha256_password":
397
		switch len(authData) {
398
		case 0:
399
			return nil // auth successful
400
		default:
401
			block, _ := pem.Decode(authData)
402
			pub, err := x509.ParsePKIXPublicKey(block.Bytes)
403
			if err != nil {
404
				return err
405
			}
406
407
			// send encrypted password
408
			err = mc.sendEncryptedPassword(oldAuthData, pub.(*rsa.PublicKey))
409
			if err != nil {
410
				return err
411
			}
412
			return mc.readResultOK()
413
		}
414
415
	default:
416
		return nil // auth successful
417
	}
418
419
	return err
420
}
421