From e91548e657d2ac11b1f962ec0b150d558ee05f06 Mon Sep 17 00:00:00 2001 From: Vamsi Atluri Date: Sun, 23 Aug 2020 18:54:49 -0700 Subject: [PATCH 1/4] mysql/client: Initial handshake should not validate auth plugin During the initial handshake, we are checking if the default auth plugin of the server is supported by the client. In cases where the server supports multiple auth plugins, this check is causing the handshake to fail without giving the server a chance to send an Authentication Method Switch Request. This check happens anyway when handling the auth method switch request and an error is thrown when server requests an unsupported auth method. Signed-off-by: Vamsi Atluri --- go/mysql/client.go | 5 +---- go/mysql/conn.go | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/go/mysql/client.go b/go/mysql/client.go index 7785760deab..cb03c42b155 100644 --- a/go/mysql/client.go +++ b/go/mysql/client.go @@ -504,10 +504,7 @@ func (c *Conn) parseInitialHandshakePacket(data []byte) (uint32, []byte, error) // 5.6.2 that don't have a null terminated string. authPluginName = string(data[pos : len(data)-1]) } - - if authPluginName != MysqlNativePassword { - return 0, nil, NewSQLError(CRMalformedPacket, SSUnknownSQLState, "parseInitialHandshakePacket: only support %v auth plugin name, but got %v", MysqlNativePassword, authPluginName) - } + c.DefaultAuthPluginName = authPluginName } return capabilities, authPluginData, nil diff --git a/go/mysql/conn.go b/go/mysql/conn.go index 81d7c64cec3..4df8348bdb0 100644 --- a/go/mysql/conn.go +++ b/go/mysql/conn.go @@ -105,6 +105,10 @@ type Conn struct { // and CapabilityClientFoundRows. Capabilities uint32 + // DefaultAuthPluginName is the name of server's default authentication plugin. + // It is set during the initial handshake. + DefaultAuthPluginName string + // CharacterSet is the character set used by the other side of the // connection. // It is set during the initial handshake. From 1302db066ac873390f1a3e9ef259dbb976d4a185 Mon Sep 17 00:00:00 2001 From: Vamsi Atluri Date: Sun, 13 Sep 2020 01:09:59 -0700 Subject: [PATCH 2/4] mysql/client: Add support for caching_sha2_password plugin caching_sha2_password plugin, unlike mysql_native_password, is a multi step process. Client hashes the password using SHA2 algorithm and sends it to the server. Server responds with either an AUTH_MORE_DATA (0x01) packet or an Error packet. Error packet is sent when authentication fails during "fast" auth (more on this below). The second byte of AUTH_MORE_DATA packet will either be 0x03 or 0x04. 0x03 represents a successful "fast" auth meaning that the server has a cached hash of the password for the user and it matches the hash sent by the client. Server will send an OK packet next. Server sends 0x04 when the hash of the password is not yet cached. In this case, client has to do a "full" authentication by sending un-hashed password. If the client is connected using SSL or a Unix socket, client can write the password in clear text. If this is not the case, client has to request a public key from the server to encrypt the password. Client should obfuscate the password using xor operation before encrypting it with public key and sending it to the server. Server will respond with an OK packet if autentication is successful or Error packet otherwise. Signed-off-by: Vamsi Atluri --- go/mysql/auth_server.go | 58 +++++++- go/mysql/auth_server_static.go | 2 +- go/mysql/auth_server_static_test.go | 4 +- go/mysql/client.go | 206 ++++++++++++++++++++-------- go/mysql/conn.go | 11 +- go/mysql/constants.go | 12 ++ 6 files changed, 228 insertions(+), 65 deletions(-) diff --git a/go/mysql/auth_server.go b/go/mysql/auth_server.go index 5df328e1896..b568537e16e 100644 --- a/go/mysql/auth_server.go +++ b/go/mysql/auth_server.go @@ -19,7 +19,9 @@ package mysql import ( "bytes" "crypto/rand" + "crypto/rsa" "crypto/sha1" + "crypto/sha256" "encoding/hex" "net" "strings" @@ -117,8 +119,8 @@ func NewSalt() ([]byte, error) { return salt, nil } -// ScramblePassword computes the hash of the password using 4.1+ method. -func ScramblePassword(salt, password []byte) []byte { +// ScrambleMysqlNativePassword computes the hash of the password using 4.1+ method. +func ScrambleMysqlNativePassword(salt, password []byte) []byte { if len(password) == 0 { return nil } @@ -189,6 +191,58 @@ func isPassScrambleMysqlNativePassword(reply, salt []byte, mysqlNativePassword s return bytes.Equal(candidateHash2, hash) } +// ScrambleCachingSha2Password computes the hash of the password using SHA256 as required by +// caching_sha2_password plugin for "fast" authentication +func ScrambleCachingSha2Password(salt []byte, password []byte) []byte { + if len(password) == 0 { + return nil + } + + // stage1Hash = SHA256(password) + crypt := sha256.New() + crypt.Write(password) + stage1 := crypt.Sum(nil) + + // scrambleHash = SHA256(SHA256(stage1Hash) + salt) + crypt.Reset() + crypt.Write(stage1) + innerHash := crypt.Sum(nil) + + crypt.Reset() + crypt.Write(innerHash) + crypt.Write(salt) + scramble := crypt.Sum(nil) + + // token = stage1Hash XOR scrambleHash + for i := range stage1 { + stage1[i] ^= scramble[i] + } + + return stage1 +} + +// EncryptPasswordWithPublicKey obfuscates the password and encrypts it with server's public key as required by +// caching_sha2_password plugin for "full" authentication +func EncryptPasswordWithPublicKey(salt []byte, password []byte, pub *rsa.PublicKey) ([]byte, error) { + if len(password) == 0 { + return nil, nil + } + + buffer := make([]byte, len(password)+1) + copy(buffer, password) + for i := range buffer { + buffer[i] ^= salt[i%len(salt)] + } + + sha1Hash := sha1.New() + enc, err := rsa.EncryptOAEP(sha1Hash, rand.Reader, pub, buffer, nil) + if err != nil { + return nil, err + } + + return enc, nil +} + // Constants for the dialog plugin. const ( mysqlDialogMessage = "Enter password: " diff --git a/go/mysql/auth_server_static.go b/go/mysql/auth_server_static.go index acb90678f54..ef95febb292 100644 --- a/go/mysql/auth_server_static.go +++ b/go/mysql/auth_server_static.go @@ -244,7 +244,7 @@ func (a *AuthServerStatic) ValidateHash(salt []byte, user string, authResponse [ return &StaticUserData{entry.UserData, entry.Groups}, nil } } else { - computedAuthResponse := ScramblePassword(salt, []byte(entry.Password)) + computedAuthResponse := ScrambleMysqlNativePassword(salt, []byte(entry.Password)) // Validate the password. if matchSourceHost(remoteAddr, entry.SourceHost) && bytes.Equal(authResponse, computedAuthResponse) { return &StaticUserData{entry.UserData, entry.Groups}, nil diff --git a/go/mysql/auth_server_static_test.go b/go/mysql/auth_server_static_test.go index 2586da2f60b..9bb3de197f8 100644 --- a/go/mysql/auth_server_static_test.go +++ b/go/mysql/auth_server_static_test.go @@ -92,7 +92,7 @@ func TestValidateHashGetter(t *testing.T) { t.Fatalf("error generating salt: %v", err) } - scrambled := ScramblePassword(salt, []byte("password")) + scrambled := ScrambleMysqlNativePassword(salt, []byte("password")) getter, err := auth.ValidateHash(salt, "mysql_user", scrambled, addr) if err != nil { t.Fatalf("error validating password: %v", err) @@ -274,7 +274,7 @@ func TestStaticPasswords(t *testing.T) { t.Fatalf("error generating salt: %v", err) } - scrambled := ScramblePassword(salt, []byte(c.password)) + scrambled := ScrambleMysqlNativePassword(salt, []byte(c.password)) _, err = auth.ValidateHash(salt, c.user, scrambled, addr) if c.success { diff --git a/go/mysql/client.go b/go/mysql/client.go index cb03c42b155..645f8d60514 100644 --- a/go/mysql/client.go +++ b/go/mysql/client.go @@ -17,7 +17,10 @@ limitations under the License. package mysql import ( + "crypto/rsa" "crypto/tls" + "crypto/x509" + "encoding/pem" "fmt" "net" "strconv" @@ -234,6 +237,7 @@ func (c *Conn) clientHandshake(characterSet uint8, params *ConnParams) error { return err } c.fillFlavor(params) + c.salt = salt // Sanity check. if capabilities&CapabilityClientProtocol41 == 0 { @@ -290,7 +294,12 @@ func (c *Conn) clientHandshake(characterSet uint8, params *ConnParams) error { } // Password encryption. - scrambledPassword := ScramblePassword(salt, []byte(params.Pass)) + var scrambledPassword []byte + if c.authPluginName == CachingSha2Password { + scrambledPassword = ScrambleCachingSha2Password(salt, []byte(params.Pass)) + } else { + scrambledPassword = ScrambleMysqlNativePassword(salt, []byte(params.Pass)) + } // Build and send our handshake response 41. // Note this one will never have SSL flag on. @@ -299,54 +308,8 @@ func (c *Conn) clientHandshake(characterSet uint8, params *ConnParams) error { } // Read the server response. - response, err := c.readPacket() - if err != nil { - return NewSQLError(CRServerLost, SSUnknownSQLState, "%v", err) - } - switch response[0] { - case OKPacket: - // OK packet, we are authenticated. Save the user, keep going. - c.User = params.Uname - case AuthSwitchRequestPacket: - // Server is asking to use a different auth method. We - // only support cleartext plugin. - pluginName, salt, err := parseAuthSwitchRequest(response) - if err != nil { - return NewSQLError(CRServerHandshakeErr, SSUnknownSQLState, "cannot parse auth switch request: %v", err) - } - - if pluginName == MysqlClearPassword { - // Write the cleartext password packet. - if err := c.writeClearTextPassword(params); err != nil { - return err - } - } else if pluginName == MysqlNativePassword { - // Write the mysql_native_password packet. - if err := c.writeMysqlNativePassword(params, salt); err != nil { - return err - } - } else { - return NewSQLError(CRServerHandshakeErr, SSUnknownSQLState, "server asked for unsupported auth method: %v", pluginName) - } - - // Wait for OK packet. - response, err = c.readPacket() - if err != nil { - return NewSQLError(CRServerLost, SSUnknownSQLState, "%v", err) - } - switch response[0] { - case OKPacket: - // OK packet, we are authenticated. Save the user, keep going. - c.User = params.Uname - case ErrPacket: - return ParseErrorPacket(response) - default: - return NewSQLError(CRServerHandshakeErr, SSUnknownSQLState, "initial server response cannot be parsed: %v", response) - } - case ErrPacket: - return ParseErrorPacket(response) - default: - return NewSQLError(CRServerHandshakeErr, SSUnknownSQLState, "initial server response cannot be parsed: %v", response) + if err := c.handleAuthResponse(params); err != nil { + return err } // If the server didn't support DbName in its handshake, set @@ -504,7 +467,7 @@ func (c *Conn) parseInitialHandshakePacket(data []byte) (uint32, []byte, error) // 5.6.2 that don't have a null terminated string. authPluginName = string(data[pos : len(data)-1]) } - c.DefaultAuthPluginName = authPluginName + c.authPluginName = authPluginName } return capabilities, authPluginData, nil @@ -588,7 +551,7 @@ func (c *Conn) writeHandshakeResponse41(capabilities uint32, scrambledPassword [ lenNullString(params.Uname) + // length of scrambled password is handled below. len(scrambledPassword) + - 21 + // "mysql_native_password" string. + len(c.authPluginName) + 1 // terminating zero. // Add the DB name if the server supports it. @@ -637,7 +600,7 @@ func (c *Conn) writeHandshakeResponse41(capabilities uint32, scrambledPassword [ } // Assume native client during response - pos = writeNullString(data, pos, MysqlNativePassword) + pos = writeNullString(data, pos, c.authPluginName) // Sanity-check the length. if pos != len(data) { @@ -650,6 +613,110 @@ func (c *Conn) writeHandshakeResponse41(capabilities uint32, scrambledPassword [ return nil } +// handleAuthResponse parses server's response after client sends the password for authentication +// and handles next steps for AuthSwitchRequestPacket and AuthMoreDataPacket. +func (c *Conn) handleAuthResponse(params *ConnParams) error { + response, err := c.readPacket() + if err != nil { + return NewSQLError(CRServerLost, SSUnknownSQLState, "%v", err) + } + + switch response[0] { + case OKPacket: + // OK packet, we are authenticated. Save the user, keep going. + c.User = params.Uname + case AuthSwitchRequestPacket: + // Server is asking to use a different auth method + if err = c.handleAuthSwitchPacket(params, response); err != nil { + return err + } + case AuthMoreDataPacket: + // Server is requesting more data - maybe un-scrambled password + if err := c.handleAuthMoreDataPacket(response[1], params); err != nil { + return err + } + case ErrPacket: + return ParseErrorPacket(response) + default: + return NewSQLError(CRServerHandshakeErr, SSUnknownSQLState, "initial server response cannot be parsed: %v", response) + } + + return nil +} + +// handleAuthSwitchPacket scrambles password for the plugin requested by the server and retries authentication +func (c *Conn) handleAuthSwitchPacket(params *ConnParams, response []byte) error { + var err error + var salt []byte + c.authPluginName, salt, err = parseAuthSwitchRequest(response) + if err != nil { + return NewSQLError(CRServerHandshakeErr, SSUnknownSQLState, "cannot parse auth switch request: %v", err) + } + if salt != nil { + c.salt = salt + } + switch c.authPluginName { + case MysqlClearPassword: + if err := c.writeClearTextPassword(params); err != nil { + return err + } + case MysqlNativePassword: + scrambledPassword := ScrambleMysqlNativePassword(c.salt, []byte(params.Pass)) + if err := c.writeScrambledPassword(scrambledPassword); err != nil { + return err + } + case CachingSha2Password: + scrambledPassword := ScrambleCachingSha2Password(c.salt, []byte(params.Pass)) + if err := c.writeScrambledPassword(scrambledPassword); err != nil { + return err + } + default: + return NewSQLError(CRServerHandshakeErr, SSUnknownSQLState, "server asked for unsupported auth method: %v", c.authPluginName) + } + + // The response could be an OKPacket, AuthMoreDataPacket or ErrPacket + return c.handleAuthResponse(params) +} + +// handleAuthMoreDataPacket handles response of CachingSha2Password authentication and sends full password to the +// server if requested +func (c *Conn) handleAuthMoreDataPacket(data byte, params *ConnParams) error { + switch data { + case CachingSha2FastAuth: + // User credentials are verified using the cache ("Fast" path). + // Next packet should be an OKPacket + return c.handleAuthResponse(params) + case CachingSha2FullAuth: + // User credentials are not cached, we have to exchange full password. + if c.Capabilities&CapabilityClientSSL > 0 || params.UnixSocket != "" { + // If we are using an SSL connection or Unix socket, write clear text password + if err := c.writeClearTextPassword(params); err != nil { + return err + } + } else { + // If we are not using an SSL connection or Unix socket, we have to fetch a public key + // from the server to encrypt password + pub, err := c.requestPublicKey() + if err != nil { + return err + } + // Encrypt password with public key + enc, err := EncryptPasswordWithPublicKey(c.salt, []byte(params.Pass), pub) + if err != nil { + return vterrors.Errorf(vtrpc.Code_INTERNAL, "error encrypting password with public key: %v", err) + } + // Write encrypted password + if err := c.writeScrambledPassword(enc); err != nil { + return err + } + } + // Next packet should either be an OKPacket or ErrPacket + return c.handleAuthResponse(params) + default: + return NewSQLError(CRServerHandshakeErr, SSUnknownSQLState, "cannot parse AuthMoreDataPacket: %v", data) + } +} + func parseAuthSwitchRequest(data []byte) (string, []byte, error) { pos := 1 pluginName, pos, ok := readNullString(data, pos) @@ -665,6 +732,34 @@ func parseAuthSwitchRequest(data []byte) (string, []byte, error) { return pluginName, salt, nil } +// requestPublicKey requests a public key from the server +func (c *Conn) requestPublicKey() (rsaKey *rsa.PublicKey, err error) { + // get public key from server + data, pos := c.startEphemeralPacketWithHeader(1) + data[pos] = 0x02 + if err := c.writeEphemeralPacket(); err != nil { + return nil, vterrors.Errorf(vtrpc.Code_INTERNAL, "error sending public key request packet: %v", err) + } + + response, err := c.readPacket() + if err != nil { + return nil, NewSQLError(CRServerLost, SSUnknownSQLState, "%v", err) + } + + // Server should respond with a AuthMoreDataPacket containing the public key + if response[0] != AuthMoreDataPacket { + return nil, ParseErrorPacket(response) + } + + block, _ := pem.Decode(response[1:]) + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, vterrors.Errorf(vtrpc.Code_INTERNAL, "failed to parse public key from server: %v", err) + } + + return pub.(*rsa.PublicKey), nil +} + // writeClearTextPassword writes the clear text password. // Returns a SQLError. func (c *Conn) writeClearTextPassword(params *ConnParams) error { @@ -678,15 +773,14 @@ func (c *Conn) writeClearTextPassword(params *ConnParams) error { return c.writeEphemeralPacket() } -// writeMysqlNativePassword writes the encrypted mysql_native_password format +// writeScrambledPassword writes the encrypted mysql_native_password format // Returns a SQLError. -func (c *Conn) writeMysqlNativePassword(params *ConnParams, salt []byte) error { - scrambledPassword := ScramblePassword(salt, []byte(params.Pass)) +func (c *Conn) writeScrambledPassword(scrambledPassword []byte) error { data, pos := c.startEphemeralPacketWithHeader(len(scrambledPassword)) pos += copy(data[pos:], scrambledPassword) // Sanity check. if pos != len(data) { - return vterrors.Errorf(vtrpc.Code_INTERNAL, "error building MysqlNativePassword packet: got %v bytes expected %v", pos, len(data)) + return vterrors.Errorf(vtrpc.Code_INTERNAL, "error building %v packet: got %v bytes expected %v", c.authPluginName, pos, len(data)) } return c.writeEphemeralPacket() } diff --git a/go/mysql/conn.go b/go/mysql/conn.go index 4df8348bdb0..3f5132d0a2a 100644 --- a/go/mysql/conn.go +++ b/go/mysql/conn.go @@ -105,10 +105,6 @@ type Conn struct { // and CapabilityClientFoundRows. Capabilities uint32 - // DefaultAuthPluginName is the name of server's default authentication plugin. - // It is set during the initial handshake. - DefaultAuthPluginName string - // CharacterSet is the character set used by the other side of the // connection. // It is set during the initial handshake. @@ -123,6 +119,13 @@ type Conn struct { // It is set during the initial handshake. UserData Getter + // salt is sent by the server during initial handshake to be used for authentication + salt []byte + + // authPluginName is the name of server's authentication plugin. + // It is set during the initial handshake. + authPluginName string + // schemaName is the default database name to use. It is set // during handshake, and by ComInitDb packets. Both client and // servers maintain it. This member is private because it's diff --git a/go/mysql/constants.go b/go/mysql/constants.go index 50d6bca7f3d..d43f6e20343 100644 --- a/go/mysql/constants.go +++ b/go/mysql/constants.go @@ -34,6 +34,9 @@ const ( // MysqlClearPassword transmits the password in the clear. MysqlClearPassword = "mysql_clear_password" + // CachingSha2Password uses a salt and transmits a SHA256 hash on the wire. + CachingSha2Password = "caching_sha2_password" + // MysqlDialog uses the dialog plugin on the client side. // It transmits data in the clear. MysqlDialog = "dialog" @@ -141,12 +144,21 @@ const ( // ComQuit is COM_QUIT. ComQuit = 0x01 + // AuthMoreDataPacket is sent when + AuthMoreDataPacket = 0x01 + // ComInitDB is COM_INIT_DB. ComInitDB = 0x02 // ComQuery is COM_QUERY. ComQuery = 0x03 + // CachingSha2FastAuth is sent before OKPacket when server authenticates using cache + CachingSha2FastAuth = 0x03 + + // CachingSha2FullAuth is sent when server requests un-scrambled password to authenticate + CachingSha2FullAuth = 0x04 + // ComPing is COM_PING. ComPing = 0x0e From b3f93223a863946315013b100a7a970d6545f3c9 Mon Sep 17 00:00:00 2001 From: Vamsi Atluri Date: Sat, 20 Feb 2021 21:27:18 -0800 Subject: [PATCH 3/4] Add test case for caching_sha2_password Signed-off-by: Vamsi Atluri --- go/mysql/endtoend/client_test.go | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/go/mysql/endtoend/client_test.go b/go/mysql/endtoend/client_test.go index a1698833d0a..5af68962cc5 100644 --- a/go/mysql/endtoend/client_test.go +++ b/go/mysql/endtoend/client_test.go @@ -339,3 +339,44 @@ func TestSessionTrackGTIDs(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, qr.SessionStateChanges) } + +func TestCachingSha2Password(t *testing.T) { + ctx := context.Background() + + // connect as an existing user to create a user account with caching_sha2_password + params := connParams + conn, err := mysql.Connect(ctx, ¶ms) + expectNoError(t, err) + defer conn.Close() + + qr, err := conn.ExecuteFetch(`select true from information_schema.PLUGINS where PLUGIN_NAME='caching_sha2_password' and PLUGIN_STATUS='ACTIVE'`, 1, false) + if err != nil { + t.Errorf("select true from information_schema.PLUGINS failed: %v", err) + } + + if len(qr.Rows) != 1 { + t.Skip("Server does not support caching_sha2_password plugin") + } + + // create a user using caching_sha2_password password + if _, err = conn.ExecuteFetch(`create user 'sha2user'@'localhost' identified with caching_sha2_password by 'password';`, 0, false); err != nil { + t.Fatalf("Create user with caching_sha2_password failed: %v", err) + } + conn.Close() + + // connect as sha2user + params.Uname = "sha2user" + params.Pass = "password" + params.DbName = "information_schema" + conn, err = mysql.Connect(ctx, ¶ms) + expectNoError(t, err) + defer conn.Close() + + if qr, err = conn.ExecuteFetch(`select user()`, 1, true); err != nil { + t.Fatalf("select user() failed: %v", err) + } + + if len(qr.Rows) != 1 || qr.Rows[0][0].ToString() != "sha2user@localhost" { + t.Errorf("Logged in user is not sha2user") + } +} From f92c55c0e3ab48b5ae781c87da58f359007f1998 Mon Sep 17 00:00:00 2001 From: Vamsi Atluri Date: Sun, 21 Feb 2021 12:51:28 -0800 Subject: [PATCH 4/4] Move auth packet types into a const block Signed-off-by: Vamsi Atluri --- go/mysql/constants.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/go/mysql/constants.go b/go/mysql/constants.go index 1f79d7b0a0f..a77569ad0f2 100644 --- a/go/mysql/constants.go +++ b/go/mysql/constants.go @@ -188,21 +188,12 @@ const ( // ComQuit is COM_QUIT. ComQuit = 0x01 - // AuthMoreDataPacket is sent when - AuthMoreDataPacket = 0x01 - // ComInitDB is COM_INIT_DB. ComInitDB = 0x02 // ComQuery is COM_QUERY. ComQuery = 0x03 - // CachingSha2FastAuth is sent before OKPacket when server authenticates using cache - CachingSha2FastAuth = 0x03 - - // CachingSha2FullAuth is sent when server requests un-scrambled password to authenticate - CachingSha2FullAuth = 0x04 - // ComPing is COM_PING. ComPing = 0x0e @@ -242,9 +233,6 @@ const ( // EOFPacket is the header of the EOF packet. EOFPacket = 0xfe - // AuthSwitchRequestPacket is used to switch auth method. - AuthSwitchRequestPacket = 0xfe - // ErrPacket is the header of the error packet. ErrPacket = 0xff @@ -252,6 +240,21 @@ const ( NullValue = 0xfb ) +// Auth packet types +const ( + // AuthMoreDataPacket is sent when server requires more data to authenticate + AuthMoreDataPacket = 0x01 + + // CachingSha2FastAuth is sent before OKPacket when server authenticates using cache + CachingSha2FastAuth = 0x03 + + // CachingSha2FullAuth is sent when server requests un-scrambled password to authenticate + CachingSha2FullAuth = 0x04 + + // AuthSwitchRequestPacket is used to switch auth method. + AuthSwitchRequestPacket = 0xfe +) + // Error codes for client-side errors. // Originally found in include/mysql/errmsg.h and // https://dev.mysql.com/doc/refman/5.7/en/error-messages-client.html