Test Failed
Pull Request — master (#36)
by Frank
03:43 queued 02:03
created

mysql.*mysqlConn.handleAuthResult   F

Complexity

Conditions 31

Size

Total Lines 127
Code Lines 76

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 31
eloc 76
nop 2
dl 0
loc 127
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
	"fmt"
19
	"sync"
20
)
21
22
// server pub keys registry
23
var (
24
	serverPubKeyLock     sync.RWMutex
25
	serverPubKeyRegistry map[string]*rsa.PublicKey
26
)
27
28
// RegisterServerPubKey registers a server RSA public key which can be used to
29
// send data in a secure manner to the server without receiving the public key
30
// in a potentially insecure way from the server first.
31
// Registered keys can afterwards be used adding serverPubKey=<name> to the DSN.
32
//
33
// Note: The provided rsa.PublicKey instance is exclusively owned by the driver
34
// after registering it and may not be modified.
35
//
36
//  data, err := ioutil.ReadFile("mykey.pem")
37
//  if err != nil {
38
//  	log.Fatal(err)
39
//  }
40
//
41
//  block, _ := pem.Decode(data)
42
//  if block == nil || block.Type != "PUBLIC KEY" {
43
//  	log.Fatal("failed to decode PEM block containing public key")
44
//  }
45
//
46
//  pub, err := x509.ParsePKIXPublicKey(block.Bytes)
47
//  if err != nil {
48
//  	log.Fatal(err)
49
//  }
50
//
51
//  if rsaPubKey, ok := pub.(*rsa.PublicKey); ok {
52
//  	mysql.RegisterServerPubKey("mykey", rsaPubKey)
53
//  } else {
54
//  	log.Fatal("not a RSA public key")
55
//  }
56
//
57
func RegisterServerPubKey(name string, pubKey *rsa.PublicKey) {
58
	serverPubKeyLock.Lock()
59
	if serverPubKeyRegistry == nil {
60
		serverPubKeyRegistry = make(map[string]*rsa.PublicKey)
61
	}
62
63
	serverPubKeyRegistry[name] = pubKey
64
	serverPubKeyLock.Unlock()
65
}
66
67
// DeregisterServerPubKey removes the public key registered with the given name.
68
func DeregisterServerPubKey(name string) {
69
	serverPubKeyLock.Lock()
70
	if serverPubKeyRegistry != nil {
71
		delete(serverPubKeyRegistry, name)
72
	}
73
	serverPubKeyLock.Unlock()
74
}
75
76
func getServerPubKey(name string) (pubKey *rsa.PublicKey) {
77
	serverPubKeyLock.RLock()
78
	if v, ok := serverPubKeyRegistry[name]; ok {
79
		pubKey = v
80
	}
81
	serverPubKeyLock.RUnlock()
82
	return
83
}
84
85
// Hash password using pre 4.1 (old password) method
86
// https://github.com/atcurtis/mariadb/blob/master/mysys/my_rnd.c
87
type myRnd struct {
88
	seed1, seed2 uint32
89
}
90
91
const myRndMaxVal = 0x3FFFFFFF
92
93
// Pseudo random number generator
94
func newMyRnd(seed1, seed2 uint32) *myRnd {
95
	return &myRnd{
96
		seed1: seed1 % myRndMaxVal,
97
		seed2: seed2 % myRndMaxVal,
98
	}
99
}
100
101
// Tested to be equivalent to MariaDB's floating point variant
102
// http://play.golang.org/p/QHvhd4qved
103
// http://play.golang.org/p/RG0q4ElWDx
104
func (r *myRnd) NextByte() byte {
105
	r.seed1 = (r.seed1*3 + r.seed2) % myRndMaxVal
106
	r.seed2 = (r.seed1 + r.seed2 + 33) % myRndMaxVal
107
108
	return byte(uint64(r.seed1) * 31 / myRndMaxVal)
109
}
110
111
// Generate binary hash from byte string using insecure pre 4.1 method
112
func pwHash(password []byte) (result [2]uint32) {
113
	var add uint32 = 7
114
	var tmp uint32
115
116
	result[0] = 1345345333
117
	result[1] = 0x12345671
118
119
	for _, c := range password {
120
		// skip spaces and tabs in password
121
		if c == ' ' || c == '\t' {
122
			continue
123
		}
124
125
		tmp = uint32(c)
126
		result[0] ^= (((result[0] & 63) + add) * tmp) + (result[0] << 8)
127
		result[1] += (result[1] << 8) ^ result[0]
128
		add += tmp
129
	}
130
131
	// Remove sign bit (1<<31)-1)
132
	result[0] &= 0x7FFFFFFF
133
	result[1] &= 0x7FFFFFFF
134
135
	return
136
}
137
138
// Hash password using insecure pre 4.1 method
139
func scrambleOldPassword(scramble []byte, password string) []byte {
140
	scramble = scramble[:8]
141
142
	hashPw := pwHash([]byte(password))
143
	hashSc := pwHash(scramble)
144
145
	r := newMyRnd(hashPw[0]^hashSc[0], hashPw[1]^hashSc[1])
146
147
	var out [8]byte
148
	for i := range out {
149
		out[i] = r.NextByte() + 64
150
	}
151
152
	mask := r.NextByte()
153
	for i := range out {
154
		out[i] ^= mask
155
	}
156
157
	return out[:]
158
}
159
160
// Hash password using 4.1+ method (SHA1)
161
func scramblePassword(scramble []byte, password string) []byte {
162
	if len(password) == 0 {
163
		return nil
164
	}
165
166
	// stage1Hash = SHA1(password)
167
	crypt := sha1.New()
168
	crypt.Write([]byte(password))
169
	stage1 := crypt.Sum(nil)
170
171
	// scrambleHash = SHA1(scramble + SHA1(stage1Hash))
172
	// inner Hash
173
	crypt.Reset()
174
	crypt.Write(stage1)
175
	hash := crypt.Sum(nil)
176
177
	// outer Hash
178
	crypt.Reset()
179
	crypt.Write(scramble)
180
	crypt.Write(hash)
181
	scramble = crypt.Sum(nil)
182
183
	// token = scrambleHash XOR stage1Hash
184
	for i := range scramble {
185
		scramble[i] ^= stage1[i]
186
	}
187
	return scramble
188
}
189
190
// Hash password using MySQL 8+ method (SHA256)
191
func scrambleSHA256Password(scramble []byte, password string) []byte {
192
	if len(password) == 0 {
193
		return nil
194
	}
195
196
	// XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble))
197
198
	crypt := sha256.New()
199
	crypt.Write([]byte(password))
200
	message1 := crypt.Sum(nil)
201
202
	crypt.Reset()
203
	crypt.Write(message1)
204
	message1Hash := crypt.Sum(nil)
205
206
	crypt.Reset()
207
	crypt.Write(message1Hash)
208
	crypt.Write(scramble)
209
	message2 := crypt.Sum(nil)
210
211
	for i := range message1 {
212
		message1[i] ^= message2[i]
213
	}
214
215
	return message1
216
}
217
218
func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, error) {
219
	plain := make([]byte, len(password)+1)
220
	copy(plain, password)
221
	for i := range plain {
222
		j := i % len(seed)
223
		plain[i] ^= seed[j]
224
	}
225
	sha1 := sha1.New()
226
	return rsa.EncryptOAEP(sha1, rand.Reader, pub, plain, nil)
227
}
228
229
func (mc *mysqlConn) sendEncryptedPassword(seed []byte, pub *rsa.PublicKey) error {
230
	enc, err := encryptPassword(mc.cfg.Passwd, seed, pub)
231
	if err != nil {
232
		return err
233
	}
234
	return mc.writeAuthSwitchPacket(enc)
235
}
236
237
func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) {
238
	switch plugin {
239
	case "caching_sha2_password":
240
		authResp := scrambleSHA256Password(authData, mc.cfg.Passwd)
241
		return authResp, nil
242
243
	case "mysql_old_password":
244
		if !mc.cfg.AllowOldPasswords {
245
			return nil, ErrOldPassword
246
		}
247
		if len(mc.cfg.Passwd) == 0 {
248
			return nil, nil
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, err := mc.buf.takeSmallBuffer(4 + 1)
364
						if err != nil {
365
							return err
366
						}
367
						data[4] = cachingSha2PasswordRequestPublicKey
368
						mc.writePacket(data)
369
370
						// parse public key
371
						if data, err = mc.readPacket(); err != nil {
372
							return err
373
						}
374
375
						block, rest := pem.Decode(data[1:])
376
						if block == nil {
377
							return fmt.Errorf("No Pem data found, data: %s", rest)
378
						}
379
						pkix, err := x509.ParsePKIXPublicKey(block.Bytes)
380
						if err != nil {
381
							return err
382
						}
383
						pubKey = pkix.(*rsa.PublicKey)
384
					}
385
386
					// send encrypted password
387
					err = mc.sendEncryptedPassword(oldAuthData, pubKey)
388
					if err != nil {
389
						return err
390
					}
391
				}
392
				return mc.readResultOK()
393
394
			default:
395
				return ErrMalformPkt
396
			}
397
		default:
398
			return ErrMalformPkt
399
		}
400
401
	case "sha256_password":
402
		switch len(authData) {
403
		case 0:
404
			return nil // auth successful
405
		default:
406
			block, _ := pem.Decode(authData)
407
			pub, err := x509.ParsePKIXPublicKey(block.Bytes)
408
			if err != nil {
409
				return err
410
			}
411
412
			// send encrypted password
413
			err = mc.sendEncryptedPassword(oldAuthData, pub.(*rsa.PublicKey))
414
			if err != nil {
415
				return err
416
			}
417
			return mc.readResultOK()
418
		}
419
420
	default:
421
		return nil // auth successful
422
	}
423
424
	return err
425
}
426