Explorar el Código

Support authentication switch with mysql_native_password authentication (#494)

* Support authentication switch with mysql_native_password authentication. Besides, fix bug that cipher needs to refreshed from authentication switch request packet from server.

* revert connection.go which was modified by accident.

* Address comments.

* Keep DSN params sorted

* DSN: Add code to format allowNativePasswords
twocode hace 9 años
padre
commit
ce924a41ee
Se han modificado 7 ficheros con 77 adiciones y 16 borrados
  1. 1 0
      AUTHORS
  2. 9 0
      README.md
  3. 10 5
      driver.go
  4. 18 0
      dsn.go
  5. 2 1
      errors.go
  6. 2 1
      infile.go
  7. 35 9
      packets.go

+ 1 - 0
AUTHORS

@@ -43,6 +43,7 @@ Runrioter Wung <runrioter at gmail.com>
 Soroush Pour <me at soroushjp.com>
 Stan Putrya <root.vagner at gmail.com>
 Stanley Gunawan <gunawan.stanley at gmail.com>
+Xiangyu Hu <xiangyu.hu at outlook.com>
 Xiaobing Jiang <s7v7nislands at gmail.com>
 Xiuming Chen <cc at cxm.cc>
 Zhenye Xie <xiezhenye at gmail.com>

+ 9 - 0
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`
 
 ```

+ 10 - 5
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
 }

+ 18 - 0
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

+ 2 - 1
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")

+ 2 - 1
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()

+ 35 - 9
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