Skip to content

Commit f4bf8e8

Browse files
committedSep 15, 2013
Merge pull request go-sql-driver#124 from go-sql-driver/old_passwords_support
old_passwords support
2 parents 3327fa9 + ff8fee6 commit f4bf8e8

7 files changed

+179
-47
lines changed
 

‎README.md

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ For Unix domain sockets the address is the absolute path to the MySQL-Server-soc
107107

108108
Possible Parameters are:
109109
* `allowAllFiles`: `allowAllFiles=true` disables the file Whitelist for `LOAD DATA LOCAL INFILE` and allows *all* files. [*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)
110+
* `allowOldPasswords`: `allowAllFiles=true` allows the usage of the insecure old password method. This should be avoided, but is necessary in some cases. See also [the old_passwords wiki page](https://github.com/go-sql-driver/mysql/wiki/old_passwords).
110111
* `charset`: Sets the charset used for client-server interaction ("SET NAMES `value`"). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset failes. This enables support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`).
111112
* `clientFoundRows`: `clientFoundRows=true` causes an UPDATE to return the number of matching rows instead of the number of rows changed.
112113
* `loc`: Sets the location for time.Time values (when using `parseTime=true`). The default is `UTC`. *"Local"* sets the system's location. See [time.LoadLocation](http://golang.org/pkg/time/#LoadLocation) for details.

‎connection.go

+12-12
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
type mysqlConn struct {
2121
cfg *config
2222
flags clientFlag
23-
cipher []byte
2423
netConn net.Conn
2524
buf *buffer
2625
protocol uint8
@@ -34,17 +33,18 @@ type mysqlConn struct {
3433
}
3534

3635
type config struct {
37-
user string
38-
passwd string
39-
net string
40-
addr string
41-
dbname string
42-
params map[string]string
43-
loc *time.Location
44-
timeout time.Duration
45-
tls *tls.Config
46-
allowAllFiles bool
47-
clientFoundRows bool
36+
user string
37+
passwd string
38+
net string
39+
addr string
40+
dbname string
41+
params map[string]string
42+
loc *time.Location
43+
timeout time.Duration
44+
tls *tls.Config
45+
allowAllFiles bool
46+
allowOldPasswords bool
47+
clientFoundRows bool
4848
}
4949

5050
// Handles parameters set in DSN

‎driver.go

+22-5
Original file line numberDiff line numberDiff line change
@@ -51,26 +51,42 @@ func (d *MySQLDriver) Open(dsn string) (driver.Conn, error) {
5151
mc.buf = newBuffer(mc.netConn)
5252

5353
// Reading Handshake Initialization Packet
54-
err = mc.readInitPacket()
54+
cipher, err := mc.readInitPacket()
5555
if err != nil {
56+
mc.Close()
5657
return nil, err
5758
}
5859

5960
// Send Client Authentication Packet
60-
err = mc.writeAuthPacket()
61-
if err != nil {
61+
if err = mc.writeAuthPacket(cipher); err != nil {
62+
mc.Close()
6263
return nil, err
6364
}
6465

6566
// Read Result Packet
6667
err = mc.readResultOK()
6768
if err != nil {
68-
return nil, err
69+
// Retry with old authentication method, if allowed
70+
if mc.cfg.allowOldPasswords && err == errOldPassword {
71+
if err = mc.writeOldAuthPacket(cipher); err != nil {
72+
mc.Close()
73+
return nil, err
74+
}
75+
if err = mc.readResultOK(); err != nil {
76+
mc.Close()
77+
return nil, err
78+
}
79+
} else {
80+
mc.Close()
81+
return nil, err
82+
}
83+
6984
}
7085

7186
// Get max allowed packet size
7287
maxap, err := mc.getSystemVar("max_allowed_packet")
7388
if err != nil {
89+
mc.Close()
7490
return nil, err
7591
}
7692
mc.maxPacketAllowed = stringToInt(maxap) - 1
@@ -81,10 +97,11 @@ func (d *MySQLDriver) Open(dsn string) (driver.Conn, error) {
8197
// Handle DSN Params
8298
err = mc.handleParams()
8399
if err != nil {
100+
mc.Close()
84101
return nil, err
85102
}
86103

87-
return mc, err
104+
return mc, nil
88105
}
89106

90107
func init() {

‎errors.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var (
1919
errInvalidConn = errors.New("Invalid Connection")
2020
errMalformPkt = errors.New("Malformed Packet")
2121
errNoTLS = errors.New("TLS encryption requested but server does not support TLS")
22-
errOldPassword = errors.New("It seems like you are using old_passwords, which is unsupported. See https://github.com/go-sql-driver/mysql/wiki/old_passwords")
22+
errOldPassword = errors.New("This server only supports the insecure old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords")
2323
errOldProtocol = errors.New("MySQL-Server does not support required Protocol 41+")
2424
errPktSync = errors.New("Commands out of sync. You can't run this command now")
2525
errPktSyncMul = errors.New("Commands out of sync. Did you run multiple statements at once?")

‎packets.go

+33-10
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,14 @@ func (mc *mysqlConn) splitPacket(data []byte) (err error) {
137137

138138
// Handshake Initialization Packet
139139
// http://dev.mysql.com/doc/internals/en/connection-phase.html#packet-Protocol::Handshake
140-
func (mc *mysqlConn) readInitPacket() (err error) {
140+
func (mc *mysqlConn) readInitPacket() (cipher []byte, err error) {
141141
data, err := mc.readPacket()
142142
if err != nil {
143143
return
144144
}
145145

146146
if data[0] == iERR {
147-
return mc.handleErrorPacket(data)
147+
return nil, mc.handleErrorPacket(data)
148148
}
149149

150150
// protocol version [1 byte]
@@ -153,25 +153,26 @@ func (mc *mysqlConn) readInitPacket() (err error) {
153153
"Unsupported MySQL Protocol Version %d. Protocol Version %d or higher is required",
154154
data[0],
155155
minProtocolVersion)
156+
return
156157
}
157158

158159
// server version [null terminated string]
159160
// connection id [4 bytes]
160161
pos := 1 + bytes.IndexByte(data[1:], 0x00) + 1 + 4
161162

162163
// first part of the password cipher [8 bytes]
163-
mc.cipher = append(mc.cipher, data[pos:pos+8]...)
164+
cipher = data[pos : pos+8]
164165

165166
// (filler) always 0x00 [1 byte]
166167
pos += 8 + 1
167168

168169
// capability flags (lower 2 bytes) [2 bytes]
169170
mc.flags = clientFlag(binary.LittleEndian.Uint16(data[pos : pos+2]))
170171
if mc.flags&clientProtocol41 == 0 {
171-
err = errOldProtocol
172+
return nil, errOldProtocol
172173
}
173174
if mc.flags&clientSSL == 0 && mc.cfg.tls != nil {
174-
return errNoTLS
175+
return nil, errNoTLS
175176
}
176177
pos += 2
177178

@@ -187,7 +188,7 @@ func (mc *mysqlConn) readInitPacket() (err error) {
187188
// The documentation is ambiguous about the length.
188189
// The official Python library uses the fixed length 12
189190
// which is not documented but seems to work.
190-
mc.cipher = append(mc.cipher, data[pos:pos+12]...)
191+
cipher = append(cipher, data[pos:pos+12]...)
191192

192193
// TODO: Verify string termination
193194
// EOF if version (>= 5.5.7 and < 5.5.10) or (>= 5.6.0 and < 5.6.2)
@@ -205,7 +206,7 @@ func (mc *mysqlConn) readInitPacket() (err error) {
205206

206207
// Client Authentication Packet
207208
// http://dev.mysql.com/doc/internals/en/connection-phase.html#packet-Protocol::HandshakeResponse
208-
func (mc *mysqlConn) writeAuthPacket() error {
209+
func (mc *mysqlConn) writeAuthPacket(cipher []byte) error {
209210
// Adjust client flags based on server support
210211
clientFlags := clientProtocol41 |
211212
clientSecureConn |
@@ -224,8 +225,7 @@ func (mc *mysqlConn) writeAuthPacket() error {
224225
}
225226

226227
// User Password
227-
scrambleBuff := scramblePassword(mc.cipher, []byte(mc.cfg.passwd))
228-
mc.cipher = nil
228+
scrambleBuff := scramblePassword(cipher, []byte(mc.cfg.passwd))
229229

230230
pktLen := 4 + 4 + 1 + 23 + len(mc.cfg.user) + 1 + 1 + len(scrambleBuff)
231231

@@ -306,6 +306,28 @@ func (mc *mysqlConn) writeAuthPacket() error {
306306
return mc.writePacket(data)
307307
}
308308

309+
// Client old authentication packet
310+
// http://dev.mysql.com/doc/internals/en/connection-phase.html#packet-Protocol::AuthSwitchResponse
311+
func (mc *mysqlConn) writeOldAuthPacket(cipher []byte) error {
312+
// User password
313+
scrambleBuff := scrambleOldPassword(cipher, []byte(mc.cfg.passwd))
314+
315+
// Calculate the packet lenght and add a tailing 0
316+
pktLen := len(scrambleBuff) + 1
317+
data := make([]byte, pktLen+4)
318+
319+
// Add the packet header [24bit length + 1 byte sequence]
320+
data[0] = byte(pktLen)
321+
data[1] = byte(pktLen >> 8)
322+
data[2] = byte(pktLen >> 16)
323+
data[3] = mc.sequence
324+
325+
// Add the scrambled password [null terminated string]
326+
copy(data[4:], scrambleBuff)
327+
328+
return mc.writePacket(data)
329+
}
330+
309331
/******************************************************************************
310332
* Command Packets *
311333
******************************************************************************/
@@ -387,7 +409,8 @@ func (mc *mysqlConn) readResultOK() error {
387409
case iOK:
388410
return mc.handleOkPacket(data)
389411

390-
case iEOF: // someone is using old_passwords
412+
case iEOF:
413+
// someone is using old_passwords
391414
return errOldPassword
392415

393416
default: // Error otherwise

‎utils.go

+101-10
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ func parseDSN(dsn string) (cfg *config, err error) {
124124
return
125125
}
126126

127+
// Use old authentication mode (pre MySQL 4.1)
128+
case "allowOldPasswords":
129+
var isBool bool
130+
cfg.allowOldPasswords, isBool = readBool(value)
131+
if !isBool {
132+
err = fmt.Errorf("Invalid Bool value: %s", value)
133+
return
134+
}
135+
127136
// Time Location
128137
case "loc":
129138
cfg.loc, err = time.LoadLocation(value)
@@ -181,8 +190,25 @@ func parseDSN(dsn string) (cfg *config, err error) {
181190
return
182191
}
183192

193+
// Returns the bool value of the input.
194+
// The 2nd return value indicates if the input was a valid bool value
195+
func readBool(input string) (value bool, valid bool) {
196+
switch input {
197+
case "1", "true", "TRUE", "True":
198+
return true, true
199+
case "0", "false", "FALSE", "False":
200+
return false, true
201+
}
202+
203+
// Not a valid bool value
204+
return
205+
}
206+
207+
/******************************************************************************
208+
* Authentication *
209+
******************************************************************************/
210+
184211
// Encrypt password using 4.1+ method
185-
// http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#4.1_and_later
186212
func scramblePassword(scramble, password []byte) []byte {
187213
if len(password) == 0 {
188214
return nil
@@ -212,20 +238,85 @@ func scramblePassword(scramble, password []byte) []byte {
212238
return scramble
213239
}
214240

215-
// Returns the bool value of the input.
216-
// The 2nd return value indicates if the input was a valid bool value
217-
func readBool(input string) (value bool, valid bool) {
218-
switch input {
219-
case "1", "true", "TRUE", "True":
220-
return true, true
221-
case "0", "false", "FALSE", "False":
222-
return false, true
241+
// Encrypt password using pre 4.1 (old password) method
242+
// https://github.com/atcurtis/mariadb/blob/master/mysys/my_rnd.c
243+
type myRnd struct {
244+
seed1, seed2 uint32
245+
}
246+
247+
const myRndMaxVal = 0x3FFFFFFF
248+
249+
// Pseudo random number generator
250+
func newMyRnd(seed1, seed2 uint32) *myRnd {
251+
return &myRnd{
252+
seed1: seed1 % myRndMaxVal,
253+
seed2: seed2 % myRndMaxVal,
223254
}
255+
}
256+
257+
// Tested to be equivalent to MariaDB's floating point variant
258+
// http://play.golang.org/p/QHvhd4qved
259+
// http://play.golang.org/p/RG0q4ElWDx
260+
func (r *myRnd) NextByte() byte {
261+
r.seed1 = (r.seed1*3 + r.seed2) % myRndMaxVal
262+
r.seed2 = (r.seed1 + r.seed2 + 33) % myRndMaxVal
263+
264+
return byte(uint64(r.seed1) * 31 / myRndMaxVal)
265+
}
266+
267+
// Generate binary hash from byte string using insecure pre 4.1 method
268+
func pwHash(password []byte) (result [2]uint32) {
269+
var add uint32 = 7
270+
var tmp uint32
271+
272+
result[0] = 1345345333
273+
result[1] = 0x12345671
274+
275+
for _, c := range password {
276+
// skip spaces and tabs in password
277+
if c == ' ' || c == '\t' {
278+
continue
279+
}
280+
281+
tmp = uint32(c)
282+
result[0] ^= (((result[0] & 63) + add) * tmp) + (result[0] << 8)
283+
result[1] += (result[1] << 8) ^ result[0]
284+
add += tmp
285+
}
286+
287+
// Remove sign bit (1<<31)-1)
288+
result[0] &= 0x7FFFFFFF
289+
result[1] &= 0x7FFFFFFF
224290

225-
// Not a valid bool value
226291
return
227292
}
228293

294+
// Encrypt password using insecure pre 4.1 method
295+
func scrambleOldPassword(scramble, password []byte) []byte {
296+
if len(password) == 0 {
297+
return nil
298+
}
299+
300+
scramble = scramble[:8]
301+
302+
hashPw := pwHash(password)
303+
hashSc := pwHash(scramble)
304+
305+
r := newMyRnd(hashPw[0]^hashSc[0], hashPw[1]^hashSc[1])
306+
307+
var out [8]byte
308+
for i := range out {
309+
out[i] = r.NextByte() + 64
310+
}
311+
312+
mask := r.NextByte()
313+
for i := range out {
314+
out[i] ^= mask
315+
}
316+
317+
return out[:]
318+
}
319+
229320
/******************************************************************************
230321
* Time related utils *
231322
******************************************************************************/

‎utils_test.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ func TestDSNParser(t *testing.T) {
2020
out string
2121
loc *time.Location
2222
}{
23-
{"username:password@protocol(address)/dbname?param=value", "&{user:username passwd:password net:protocol addr:address dbname:dbname params:map[param:value] loc:%p timeout:0 tls:<nil> allowAllFiles:false clientFoundRows:false}", time.UTC},
24-
{"user@unix(/path/to/socket)/dbname?charset=utf8", "&{user:user passwd: net:unix addr:/path/to/socket dbname:dbname params:map[charset:utf8] loc:%p timeout:0 tls:<nil> allowAllFiles:false clientFoundRows:false}", time.UTC},
25-
{"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8] loc:%p timeout:0 tls:<nil> allowAllFiles:false clientFoundRows:false}", time.UTC},
26-
{"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8mb4,utf8] loc:%p timeout:0 tls:<nil> allowAllFiles:false clientFoundRows:false}", time.UTC},
27-
{"user:password@/dbname?loc=UTC&timeout=30s&allowAllFiles=1&clientFoundRows=true", "&{user:user passwd:password net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[] loc:%p timeout:30000000000 tls:<nil> allowAllFiles:true clientFoundRows:true}", time.UTC},
28-
{"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{user:user passwd:p@ss(word) net:tcp addr:[de:ad:be:ef::ca:fe]:80 dbname:dbname params:map[] loc:%p timeout:0 tls:<nil> allowAllFiles:false clientFoundRows:false}", time.Local},
29-
{"/dbname", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[] loc:%p timeout:0 tls:<nil> allowAllFiles:false clientFoundRows:false}", time.UTC},
30-
{"/", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p timeout:0 tls:<nil> allowAllFiles:false clientFoundRows:false}", time.UTC},
31-
{"user:p@/ssword@/", "&{user:user passwd:p@/ssword net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p timeout:0 tls:<nil> allowAllFiles:false clientFoundRows:false}", time.UTC},
23+
{"username:password@protocol(address)/dbname?param=value", "&{user:username passwd:password net:protocol addr:address dbname:dbname params:map[param:value] loc:%p timeout:0 tls:<nil> allowAllFiles:false allowOldPasswords:false clientFoundRows:false}", time.UTC},
24+
{"user@unix(/path/to/socket)/dbname?charset=utf8", "&{user:user passwd: net:unix addr:/path/to/socket dbname:dbname params:map[charset:utf8] loc:%p timeout:0 tls:<nil> allowAllFiles:false allowOldPasswords:false clientFoundRows:false}", time.UTC},
25+
{"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8] loc:%p timeout:0 tls:<nil> allowAllFiles:false allowOldPasswords:false clientFoundRows:false}", time.UTC},
26+
{"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8mb4,utf8] loc:%p timeout:0 tls:<nil> allowAllFiles:false allowOldPasswords:false clientFoundRows:false}", time.UTC},
27+
{"user:password@/dbname?loc=UTC&timeout=30s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE", "&{user:user passwd:password net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[] loc:%p timeout:30000000000 tls:<nil> allowAllFiles:true allowOldPasswords:true clientFoundRows:true}", time.UTC},
28+
{"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{user:user passwd:p@ss(word) net:tcp addr:[de:ad:be:ef::ca:fe]:80 dbname:dbname params:map[] loc:%p timeout:0 tls:<nil> allowAllFiles:false allowOldPasswords:false clientFoundRows:false}", time.Local},
29+
{"/dbname", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[] loc:%p timeout:0 tls:<nil> allowAllFiles:false allowOldPasswords:false clientFoundRows:false}", time.UTC},
30+
{"/", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p timeout:0 tls:<nil> allowAllFiles:false allowOldPasswords:false clientFoundRows:false}", time.UTC},
31+
{"user:p@/ssword@/", "&{user:user passwd:p@/ssword net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p timeout:0 tls:<nil> allowAllFiles:false allowOldPasswords:false clientFoundRows:false}", time.UTC},
3232
}
3333

3434
var cfg *config

0 commit comments

Comments
 (0)