Browse Source

Fix empty SHA2 password handling (#800)

* Fix empty SHA2 password handling

Fixes #799

* add empty password test

* fix auth switch
Julien Schmidt 7 years ago
parent
commit
ceae19ce1c
4 changed files with 141 additions and 91 deletions
  1. 5 4
      const.go
  2. 70 45
      driver.go
  3. 27 0
      driver_test.go
  4. 39 42
      packets.go

+ 5 - 4
const.go

@@ -19,10 +19,11 @@ const (
 // http://dev.mysql.com/doc/internals/en/client-server-protocol.html
 
 const (
-	iOK          byte = 0x00
-	iLocalInFile byte = 0xfb
-	iEOF         byte = 0xfe
-	iERR         byte = 0xff
+	iOK           byte = 0x00
+	iAuthMoreData byte = 0x01
+	iLocalInFile  byte = 0xfb
+	iEOF          byte = 0xfe
+	iERR          byte = 0xff
 )
 
 // https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags

+ 70 - 45
driver.go

@@ -154,66 +154,91 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
 }
 
 func handleAuthResult(mc *mysqlConn, oldCipher []byte, pluginName string) error {
-
-	// handle caching_sha2_password
-	if pluginName == "caching_sha2_password" {
-		auth, err := mc.readCachingSha2PasswordAuthResult()
-		if err != nil {
-			return err
-		}
-		if auth == cachingSha2PasswordPerformFullAuthentication {
-			if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
-				if err = mc.writeClearAuthPacket(); err != nil {
-					return err
+	// Read Result Packet
+	cipher, err := mc.readResultOK()
+	if err == nil {
+		// handle caching_sha2_password
+		// https://insidemysql.com/preparing-your-community-connector-for-mysql-8-part-2-sha256/
+		if pluginName == "caching_sha2_password" {
+			if len(cipher) == 1 {
+				switch cipher[0] {
+				case cachingSha2PasswordFastAuthSuccess:
+					cipher, err = mc.readResultOK()
+					if err == nil {
+						return nil // auth successful
+					}
+
+				case cachingSha2PasswordPerformFullAuthentication:
+					if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
+						if err = mc.writeClearAuthPacket(); err != nil {
+							return err
+						}
+					} else {
+						if err = mc.writePublicKeyAuthPacket(oldCipher); err != nil {
+							return err
+						}
+					}
+					cipher, err = mc.readResultOK()
+					if err == nil {
+						return nil // auth successful
+					}
+
+				default:
+					return ErrMalformPkt
 				}
 			} else {
-				if err = mc.writePublicKeyAuthPacket(oldCipher); err != nil {
-					return err
-				}
+				return ErrMalformPkt
 			}
-		}
-	}
 
-	// Read Result Packet
-	cipher, err := mc.readResultOK()
-	if err == nil {
-		return nil // auth successful
+		} else {
+			return nil // auth successful
+		}
 	}
 
 	if mc.cfg == nil {
 		return err // auth failed and retry not possible
 	}
 
-	// Retry auth if configured to do so.
-	if mc.cfg.AllowOldPasswords && err == ErrOldPassword {
-		// Retry with old authentication method. Note: there are edge cases
-		// where this should work but doesn't; this is currently "wontfix":
-		// https://github.com/go-sql-driver/mysql/issues/184
-
-		// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
-		// sent and we have to keep using the cipher sent in the init packet.
-		if cipher == nil {
-			cipher = oldCipher
+	// Retry auth if configured to do so
+	switch err {
+	case ErrCleartextPassword:
+		if mc.cfg.AllowCleartextPasswords {
+			// Retry with clear text password for
+			// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
+			// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
+			if err = mc.writeClearAuthPacket(); err != nil {
+				return err
+			}
+			_, err = mc.readResultOK()
 		}
 
-		if err = mc.writeOldAuthPacket(cipher); err != nil {
-			return err
-		}
-		_, 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
-		// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
-		if err = mc.writeClearAuthPacket(); err != nil {
-			return err
+	case ErrNativePassword:
+		if mc.cfg.AllowNativePasswords {
+			if err = mc.writeNativeAuthPacket(cipher); 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
+
+	case ErrOldPassword:
+		if mc.cfg.AllowOldPasswords {
+			// Retry with old authentication method. Note: there are edge cases
+			// where this should work but doesn't; this is currently "wontfix":
+			// https://github.com/go-sql-driver/mysql/issues/184
+
+			// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
+			// sent and we have to keep using the cipher sent in the init packet.
+			if cipher == nil {
+				cipher = oldCipher
+			}
+
+			if err = mc.writeOldAuthPacket(cipher); err != nil {
+				return err
+			}
+			_, err = mc.readResultOK()
 		}
-		_, err = mc.readResultOK()
 	}
+
 	return err
 }
 

+ 27 - 0
driver_test.go

@@ -2046,3 +2046,30 @@ func TestPing(t *testing.T) {
 		}
 	})
 }
+
+// See Issue #799
+func TestEmptyPassword(t *testing.T) {
+	if !available {
+		t.Skipf("MySQL server not running on %s", netAddr)
+	}
+
+	dsn := fmt.Sprintf("%s:%s@%s/%s?timeout=30s", user, "", netAddr, dbname)
+	db, err := sql.Open("mysql", dsn)
+	if err == nil {
+		defer db.Close()
+		err = db.Ping()
+	}
+
+	if pass == "" {
+		if err != nil {
+			t.Fatal(err.Error())
+		}
+	} else {
+		if err == nil {
+			t.Fatal("expected authentication error")
+		}
+		if !strings.HasPrefix(err.Error(), "Error 1045") {
+			t.Fatal(err.Error())
+		}
+	}
+}

+ 39 - 42
packets.go

@@ -203,7 +203,7 @@ func (mc *mysqlConn) readInitPacket() ([]byte, string, error) {
 	}
 	pos += 2
 
-	pluginName := ""
+	pluginName := "mysql_native_password"
 	if len(data) > pos {
 		// character set [1 byte]
 		// status flags [2 bytes]
@@ -365,7 +365,6 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte, pluginName string) error {
 		pos++
 	}
 
-	// Assume native client during response
 	pos += copy(data[pos:], pluginName)
 	data[pos] = 0x00
 
@@ -542,55 +541,53 @@ func (mc *mysqlConn) writeCommandPacketUint32(command byte, arg uint32) error {
 *                              Result Packets                                 *
 ******************************************************************************/
 
+func readAuthSwitch(data []byte) ([]byte, error) {
+	if len(data) > 1 {
+		pluginEndIndex := bytes.IndexByte(data, 0x00)
+		plugin := string(data[1:pluginEndIndex])
+		cipher := data[pluginEndIndex+1:]
+
+		switch plugin {
+		case "mysql_old_password":
+			// using old_passwords
+			return cipher, ErrOldPassword
+		case "mysql_clear_password":
+			// using clear text password
+			return cipher, ErrCleartextPassword
+		case "mysql_native_password":
+			// using mysql default authentication method
+			return cipher, ErrNativePassword
+		default:
+			return cipher, ErrUnknownPlugin
+		}
+	}
+
+	// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::OldAuthSwitchRequest
+	return nil, ErrOldPassword
+}
+
 // Returns error if Packet is not an 'Result OK'-Packet
 func (mc *mysqlConn) readResultOK() ([]byte, error) {
 	data, err := mc.readPacket()
-	if err == nil {
-		// packet indicator
-		switch data[0] {
+	if err != nil {
+		return nil, err
+	}
 
-		case iOK:
-			return nil, mc.handleOkPacket(data)
+	// packet indicator
+	switch data[0] {
 
-		case iEOF:
-			if len(data) > 1 {
-				pluginEndIndex := bytes.IndexByte(data, 0x00)
-				plugin := string(data[1:pluginEndIndex])
-				cipher := data[pluginEndIndex+1:]
-
-				switch plugin {
-				case "mysql_old_password":
-					// using old_passwords
-					return cipher, ErrOldPassword
-				case "mysql_clear_password":
-					// using clear text password
-					return cipher, ErrCleartextPassword
-				case "mysql_native_password":
-					// using mysql default authentication method
-					return cipher, ErrNativePassword
-				default:
-					return cipher, ErrUnknownPlugin
-				}
-			}
+	case iOK:
+		return nil, mc.handleOkPacket(data)
 
-			// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::OldAuthSwitchRequest
-			return nil, ErrOldPassword
+	case iAuthMoreData:
+		return data[1:], nil
 
-		default: // Error otherwise
-			return nil, mc.handleErrorPacket(data)
-		}
-	}
-	return nil, err
-}
+	case iEOF:
+		return readAuthSwitch(data)
 
-func (mc *mysqlConn) readCachingSha2PasswordAuthResult() (int, error) {
-	data, err := mc.readPacket()
-	if err == nil {
-		if data[0] != 1 {
-			return 0, ErrMalformPkt
-		}
+	default: // Error otherwise
+		return nil, mc.handleErrorPacket(data)
 	}
-	return int(data[1]), err
 }
 
 // Result Set Header Packet