diff --git a/AUTHORS b/AUTHORS index 466ac86ab..148ea93aa 100644 --- a/AUTHORS +++ b/AUTHORS @@ -43,6 +43,7 @@ Runrioter Wung Soroush Pour Stan Putrya Stanley Gunawan +Xiangyu Hu Xiaobing Jiang Xiuming Chen Zhenye Xie diff --git a/README.md b/README.md index a110cf1d3..3dcc23544 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,15 @@ Default: false `allowCleartextPasswords=true` allows using the [cleartext client side plugin](http://dev.mysql.com/doc/en/cleartext-authentication-plugin.html) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network. +##### `allowNativePasswords` + +``` +Type: bool +Valid Values: true, false +Default: false +``` +`allowNativePasswords=true` allows the usage of the mysql native password method. + ##### `allowOldPasswords` ``` diff --git a/driver.go b/driver.go index 562ddeffb..f5ee4728e 100644 --- a/driver.go +++ b/driver.go @@ -101,7 +101,7 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { } // Handle response to auth packet, switch methods if possible - if err = handleAuthResult(mc, cipher); err != nil { + if err = handleAuthResult(mc); err != nil { // Authentication failed and MySQL has already closed the connection // (https://dev.mysql.com/doc/internals/en/authentication-fails.html). // Do not send COM_QUIT, just cleanup and return the error. @@ -134,9 +134,9 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { return mc, nil } -func handleAuthResult(mc *mysqlConn, cipher []byte) error { +func handleAuthResult(mc *mysqlConn) error { // Read Result Packet - err := mc.readResultOK() + cipher, err := mc.readResultOK() if err == nil { return nil // auth successful } @@ -153,7 +153,7 @@ func handleAuthResult(mc *mysqlConn, cipher []byte) error { if err = mc.writeOldAuthPacket(cipher); err != nil { return err } - err = mc.readResultOK() + _, err = mc.readResultOK() } else if mc.cfg.AllowCleartextPasswords && err == ErrCleartextPassword { // Retry with clear text password for // http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html @@ -161,7 +161,12 @@ func handleAuthResult(mc *mysqlConn, cipher []byte) error { if err = mc.writeClearAuthPacket(); err != nil { return err } - err = mc.readResultOK() + _, err = mc.readResultOK() + } else if mc.cfg.AllowNativePasswords && err == ErrNativePassword { + if err = mc.writeNativeAuthPacket(cipher); err != nil { + return err + } + _, err = mc.readResultOK() } return err } diff --git a/dsn.go b/dsn.go index 896be9ef5..ac00dcedd 100644 --- a/dsn.go +++ b/dsn.go @@ -46,6 +46,7 @@ type Config struct { AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE AllowCleartextPasswords bool // Allows the cleartext client side plugin + AllowNativePasswords bool // Allows the native password authentication method AllowOldPasswords bool // Allows the old insecure password method ClientFoundRows bool // Return number of matching rows instead of rows changed ColumnsWithAlias bool // Prepend table alias to column names @@ -101,6 +102,15 @@ func (cfg *Config) FormatDSN() string { } } + if cfg.AllowNativePasswords { + if hasParam { + buf.WriteString("&allowNativePasswords=true") + } else { + hasParam = true + buf.WriteString("?allowNativePasswords=true") + } + } + if cfg.AllowOldPasswords { if hasParam { buf.WriteString("&allowOldPasswords=true") @@ -381,6 +391,14 @@ func parseDSNParams(cfg *Config, params string) (err error) { return errors.New("invalid bool value: " + value) } + // Use native password authentication + case "allowNativePasswords": + var isBool bool + cfg.AllowNativePasswords, isBool = readBool(value) + if !isBool { + return errors.New("invalid bool value: " + value) + } + // Use old authentication mode (pre MySQL 4.1) case "allowOldPasswords": var isBool bool diff --git a/errors.go b/errors.go index 1543a8054..857854e14 100644 --- a/errors.go +++ b/errors.go @@ -22,8 +22,9 @@ var ( ErrInvalidConn = errors.New("invalid connection") ErrMalformPkt = errors.New("malformed packet") ErrNoTLS = errors.New("TLS requested but server does not support TLS") - ErrOldPassword = errors.New("this user requires 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") ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN") + ErrNativePassword = errors.New("this user requires mysql native password authentication.") + ErrOldPassword = errors.New("this user requires 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") ErrUnknownPlugin = errors.New("this authentication plugin is not supported") ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+") ErrPktSync = errors.New("commands out of sync. You can't run this command now") diff --git a/infile.go b/infile.go index 0f975bbc2..547357cfa 100644 --- a/infile.go +++ b/infile.go @@ -173,7 +173,8 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) { // read OK packet if err == nil { - return mc.readResultOK() + _, err = mc.readResultOK() + return err } mc.readPacket() diff --git a/packets.go b/packets.go index f06752b02..9160beb09 100644 --- a/packets.go +++ b/packets.go @@ -372,6 +372,26 @@ func (mc *mysqlConn) writeClearAuthPacket() error { return mc.writePacket(data) } +// Native password authentication method +// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchResponse +func (mc *mysqlConn) writeNativeAuthPacket(cipher []byte) error { + scrambleBuff := scramblePassword(cipher, []byte(mc.cfg.Passwd)) + + // Calculate the packet length and add a tailing 0 + pktLen := len(scrambleBuff) + data := mc.buf.takeSmallBuffer(4 + pktLen) + if data == nil { + // can not take the buffer. Something must be wrong with the connection + errLog.Print(ErrBusyBuffer) + return driver.ErrBadConn + } + + // Add the scramble + copy(data[4:], scrambleBuff) + + return mc.writePacket(data) +} + /****************************************************************************** * Command Packets * ******************************************************************************/ @@ -445,36 +465,42 @@ func (mc *mysqlConn) writeCommandPacketUint32(command byte, arg uint32) error { ******************************************************************************/ // Returns error if Packet is not an 'Result OK'-Packet -func (mc *mysqlConn) readResultOK() error { +func (mc *mysqlConn) readResultOK() ([]byte, error) { data, err := mc.readPacket() if err == nil { // packet indicator switch data[0] { case iOK: - return mc.handleOkPacket(data) + return nil, mc.handleOkPacket(data) case iEOF: if len(data) > 1 { - plugin := string(data[1:bytes.IndexByte(data, 0x00)]) + pluginEndIndex := bytes.IndexByte(data, 0x00) + plugin := string(data[1:pluginEndIndex]) + cipher := data[pluginEndIndex+1 : len(data)-1] + if plugin == "mysql_old_password" { // using old_passwords - return ErrOldPassword + return cipher, ErrOldPassword } else if plugin == "mysql_clear_password" { // using clear text password - return ErrCleartextPassword + return cipher, ErrCleartextPassword + } else if plugin == "mysql_native_password" { + // using mysql default authentication method + return cipher, ErrNativePassword } else { - return ErrUnknownPlugin + return cipher, ErrUnknownPlugin } } else { - return ErrOldPassword + return nil, ErrOldPassword } default: // Error otherwise - return mc.handleErrorPacket(data) + return nil, mc.handleErrorPacket(data) } } - return err + return nil, err } // Result Set Header Packet