From f612307f2cd843956e016936e1bbce161bbadb65 Mon Sep 17 00:00:00 2001 From: Diego Dupin Date: Tue, 22 Apr 2025 15:10:03 +0200 Subject: [PATCH 1/4] authentication has been revamp. Current implementation was requiring authentication plugin that are have multiple step to begin with iAuthMoreData, but that's only required for "caching_sha2_password" and "sha256_password" plugins. Additionally, now permits multi-authentication (in mariadb: https://mariadb.com/kb/en/create-user/#identified-viawith-authentication_plugin / mysql: https://dev.mysql.com/doc/refman/8.0/en/multifactor-authentication.html) goal is to add MariaDB main authentication plugins (like parsec, PAM, GSSAPI, ...) --- README.md | 11 ++ auth.go | 423 +++++-------------------------------------- auth_caching_sha2.go | 176 ++++++++++++++++++ auth_cleartext.go | 46 +++++ auth_ed25519.go | 64 +++++++ auth_mysql_native.go | 64 +++++++ auth_old_password.go | 108 +++++++++++ auth_plugin.go | 63 +++++++ auth_sha256.go | 130 +++++++++++++ auth_test.go | 286 ++++++++++++++++------------- connector.go | 22 ++- const.go | 6 - driver.go | 5 +- packets.go | 38 ---- 14 files changed, 882 insertions(+), 560 deletions(-) create mode 100644 auth_caching_sha2.go create mode 100644 auth_cleartext.go create mode 100644 auth_ed25519.go create mode 100644 auth_mysql_native.go create mode 100644 auth_old_password.go create mode 100644 auth_plugin.go create mode 100644 auth_sha256.go diff --git a/README.md b/README.md index 65dd898d8..56e53a356 100644 --- a/README.md +++ b/README.md @@ -534,6 +534,17 @@ See [context support in the database/sql package](https://golang.org/doc/go1.8#d > The `QueryContext`, `ExecContext`, etc. variants provided by `database/sql` will cause the connection to be closed if the provided context is cancelled or timed out before the result is received by the driver. +### Authentication Plugin System + +The driver implements a pluggable authentication system that supports various authentication methods used by MySQL and MariaDB servers. The built-in authentication plugins include: + +- `mysql_native_password` - The default MySQL authentication method +- `caching_sha2_password` - Default authentication method in MySQL 8.0+ +- `mysql_clear_password` - Cleartext authentication (requires `allowCleartextPasswords=true`) +- `mysql_old_password` - Old MySQL authentication (requires `allowOldPasswords=true`) +- `sha256_password` - SHA256 authentication +- `client_ed25519` - MariaDB Ed25519 authentication + ### `LOAD DATA LOCAL INFILE` support For this feature you need direct access to the package. Therefore you must change the import path (no `_`): ```go diff --git a/auth.go b/auth.go index 74e1bd03e..b220a7f56 100644 --- a/auth.go +++ b/auth.go @@ -9,17 +9,10 @@ package mysql import ( - "crypto/rand" + "bytes" "crypto/rsa" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "crypto/x509" - "encoding/pem" "fmt" "sync" - - "filippo.io/edwards25519" ) // server pub keys registry @@ -84,401 +77,77 @@ func getServerPubKey(name string) (pubKey *rsa.PublicKey) { return } -// Hash password using pre 4.1 (old password) method -// https://github.com/atcurtis/mariadb/blob/master/mysys/my_rnd.c -type myRnd struct { - seed1, seed2 uint32 -} - -const myRndMaxVal = 0x3FFFFFFF - -// Pseudo random number generator -func newMyRnd(seed1, seed2 uint32) *myRnd { - return &myRnd{ - seed1: seed1 % myRndMaxVal, - seed2: seed2 % myRndMaxVal, - } -} - -// Tested to be equivalent to MariaDB's floating point variant -// http://play.golang.org/p/QHvhd4qved -// http://play.golang.org/p/RG0q4ElWDx -func (r *myRnd) NextByte() byte { - r.seed1 = (r.seed1*3 + r.seed2) % myRndMaxVal - r.seed2 = (r.seed1 + r.seed2 + 33) % myRndMaxVal - - return byte(uint64(r.seed1) * 31 / myRndMaxVal) -} - -// Generate binary hash from byte string using insecure pre 4.1 method -func pwHash(password []byte) (result [2]uint32) { - var add uint32 = 7 - var tmp uint32 - - result[0] = 1345345333 - result[1] = 0x12345671 - - for _, c := range password { - // skip spaces and tabs in password - if c == ' ' || c == '\t' { - continue - } - - tmp = uint32(c) - result[0] ^= (((result[0] & 63) + add) * tmp) + (result[0] << 8) - result[1] += (result[1] << 8) ^ result[0] - add += tmp - } - - // Remove sign bit (1<<31)-1) - result[0] &= 0x7FFFFFFF - result[1] &= 0x7FFFFFFF - - return -} - -// Hash password using insecure pre 4.1 method -func scrambleOldPassword(scramble []byte, password string) []byte { - scramble = scramble[:8] - - hashPw := pwHash([]byte(password)) - hashSc := pwHash(scramble) - - r := newMyRnd(hashPw[0]^hashSc[0], hashPw[1]^hashSc[1]) - - var out [8]byte - for i := range out { - out[i] = r.NextByte() + 64 +// handleAuthResult processes the initial authentication packet and manages subsequent +// authentication flow. It reads the first authentication packet and hands off processing +// to the appropriate auth plugin. +func (mc *mysqlConn) handleAuthResult(remainingSwitch uint, initialSeed []byte, authPlugin AuthPlugin) error { + if remainingSwitch == 0 { + return fmt.Errorf("maximum of %d authentication switch reached", authMaximumSwitch) } - mask := r.NextByte() - for i := range out { - out[i] ^= mask - } - - return out[:] -} - -// Hash password using 4.1+ method (SHA1) -func scramblePassword(scramble []byte, password string) []byte { - if len(password) == 0 { - return nil - } - - // stage1Hash = SHA1(password) - crypt := sha1.New() - crypt.Write([]byte(password)) - stage1 := crypt.Sum(nil) - - // scrambleHash = SHA1(scramble + SHA1(stage1Hash)) - // inner Hash - crypt.Reset() - crypt.Write(stage1) - hash := crypt.Sum(nil) - - // outer Hash - crypt.Reset() - crypt.Write(scramble) - crypt.Write(hash) - scramble = crypt.Sum(nil) - - // token = scrambleHash XOR stage1Hash - for i := range scramble { - scramble[i] ^= stage1[i] - } - return scramble -} - -// Hash password using MySQL 8+ method (SHA256) -func scrambleSHA256Password(scramble []byte, password string) []byte { - if len(password) == 0 { - return nil - } - - // XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble)) - - crypt := sha256.New() - crypt.Write([]byte(password)) - message1 := crypt.Sum(nil) - - crypt.Reset() - crypt.Write(message1) - message1Hash := crypt.Sum(nil) - - crypt.Reset() - crypt.Write(message1Hash) - crypt.Write(scramble) - message2 := crypt.Sum(nil) - - for i := range message1 { - message1[i] ^= message2[i] - } - - return message1 -} - -func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, error) { - plain := make([]byte, len(password)+1) - copy(plain, password) - for i := range plain { - j := i % len(seed) - plain[i] ^= seed[j] - } - sha1 := sha1.New() - return rsa.EncryptOAEP(sha1, rand.Reader, pub, plain, nil) -} - -// authEd25519 does ed25519 authentication used by MariaDB. -func authEd25519(scramble []byte, password string) ([]byte, error) { - // Derived from https://github.com/MariaDB/server/blob/d8e6bb00888b1f82c031938f4c8ac5d97f6874c3/plugin/auth_ed25519/ref10/sign.c - // Code style is from https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/crypto/ed25519/ed25519.go;l=207 - h := sha512.Sum512([]byte(password)) - - s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) + data, err := mc.readPacket() if err != nil { - return nil, err - } - A := (&edwards25519.Point{}).ScalarBaseMult(s) - - mh := sha512.New() - mh.Write(h[32:]) - mh.Write(scramble) - messageDigest := mh.Sum(nil) - r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest) - if err != nil { - return nil, err + return err } - - R := (&edwards25519.Point{}).ScalarBaseMult(r) - - kh := sha512.New() - kh.Write(R.Bytes()) - kh.Write(A.Bytes()) - kh.Write(scramble) - hramDigest := kh.Sum(nil) - k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest) - if err != nil { - return nil, err + if len(data) == 0 { + return fmt.Errorf("%w: empty auth response packet", ErrMalformPkt) } - S := k.MultiplyAdd(k, s, r) - - return append(R.Bytes(), S.Bytes()...), nil -} - -func (mc *mysqlConn) sendEncryptedPassword(seed []byte, pub *rsa.PublicKey) error { - enc, err := encryptPassword(mc.cfg.Passwd, seed, pub) + data, err = authPlugin.continuationAuth(data, initialSeed, mc) if err != nil { return err } - return mc.writeAuthSwitchPacket(enc) -} - -func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) { - switch plugin { - case "caching_sha2_password": - authResp := scrambleSHA256Password(authData, mc.cfg.Passwd) - return authResp, nil - case "mysql_old_password": - if !mc.cfg.AllowOldPasswords { - return nil, ErrOldPassword - } - if len(mc.cfg.Passwd) == 0 { - return nil, nil - } - // 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 - authResp := append(scrambleOldPassword(authData[:8], mc.cfg.Passwd), 0) - return authResp, nil + switch data[0] { + case iOK: + return mc.resultUnchanged().handleOkPacket(data) + case iERR: + return mc.handleErrorPacket(data) + case iEOF: + plugin, authData := mc.parseAuthSwitchData(data, initialSeed) - case "mysql_clear_password": - if !mc.cfg.AllowCleartextPasswords { - return nil, ErrCleartextPassword + authPlugin, exists := globalPluginRegistry.GetPlugin(plugin) + if !exists { + return fmt.Errorf("this authentication plugin '%s' is not supported", plugin) } - // 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 - return append([]byte(mc.cfg.Passwd), 0), nil - case "mysql_native_password": - if !mc.cfg.AllowNativePasswords { - return nil, ErrNativePassword - } - // https://dev.mysql.com/doc/internals/en/secure-password-authentication.html - // Native password authentication only need and will need 20-byte challenge. - authResp := scramblePassword(authData[:20], mc.cfg.Passwd) - return authResp, nil - - case "sha256_password": - if len(mc.cfg.Passwd) == 0 { - return []byte{0}, nil - } - // unlike caching_sha2_password, sha256_password does not accept - // cleartext password on unix transport. - if mc.cfg.TLS != nil { - // write cleartext auth packet - return append([]byte(mc.cfg.Passwd), 0), nil + initialAuthResponse, err := authPlugin.InitAuth(authData, mc.cfg) + if err != nil { + return err } - pubKey := mc.cfg.pubKey - if pubKey == nil { - // request public key from server - return []byte{1}, nil + if err := mc.writeAuthSwitchPacket(initialAuthResponse); err != nil { + return err } - // encrypted password - enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey) - return enc, err - - case "client_ed25519": - if len(authData) != 32 { - return nil, ErrMalformPkt - } - return authEd25519(authData, mc.cfg.Passwd) + remainingSwitch-- + return mc.handleAuthResult(remainingSwitch, authData, authPlugin) default: - mc.log("unknown auth plugin:", plugin) - return nil, ErrUnknownPlugin + return ErrMalformPkt } } -func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error { - // Read Result Packet - authData, newPlugin, err := mc.readAuthResult() - if err != nil { - return err +// parseAuthSwitchData extracts the authentication plugin name and associated data +// from an authentication switch request packet. +func (mc *mysqlConn) parseAuthSwitchData(data []byte, initialSeed []byte) (string, []byte) { + if len(data) == 1 { + // Special case for the old authentication protocol + return "mysql_old_password", initialSeed } - // handle auth plugin switch, if requested - if newPlugin != "" { - // 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 authData == nil { - authData = oldAuthData - } else { - // copy data from read buffer to owned slice - copy(oldAuthData, authData) - } - - plugin = newPlugin - - authResp, err := mc.auth(authData, plugin) - if err != nil { - return err - } - if err = mc.writeAuthSwitchPacket(authResp); err != nil { - return err - } - - // Read Result Packet - authData, newPlugin, err = mc.readAuthResult() - if err != nil { - return err - } - - // Do not allow to change the auth plugin more than once - if newPlugin != "" { - return ErrMalformPkt - } + pluginEndIndex := bytes.IndexByte(data, 0x00) + if pluginEndIndex < 0 { + return "", nil } - switch plugin { - - // https://dev.mysql.com/blog-archive/preparing-your-community-connector-for-mysql-8-part-2-sha256/ - case "caching_sha2_password": - switch len(authData) { - case 0: - return nil // auth successful - case 1: - switch authData[0] { - case cachingSha2PasswordFastAuthSuccess: - if err = mc.resultUnchanged().readResultOK(); err == nil { - return nil // auth successful - } - - case cachingSha2PasswordPerformFullAuthentication: - if mc.cfg.TLS != nil || mc.cfg.Net == "unix" { - // write cleartext auth packet - err = mc.writeAuthSwitchPacket(append([]byte(mc.cfg.Passwd), 0)) - if err != nil { - return err - } - } else { - pubKey := mc.cfg.pubKey - if pubKey == nil { - // request public key from server - data, err := mc.buf.takeSmallBuffer(4 + 1) - if err != nil { - return err - } - data[4] = cachingSha2PasswordRequestPublicKey - err = mc.writePacket(data) - if err != nil { - return err - } - - if data, err = mc.readPacket(); err != nil { - return err - } - - if data[0] != iAuthMoreData { - return fmt.Errorf("unexpected resp from server for caching_sha2_password, perform full authentication") - } - - // parse public key - block, rest := pem.Decode(data[1:]) - if block == nil { - return fmt.Errorf("no pem data found, data: %s", rest) - } - pkix, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return err - } - pubKey = pkix.(*rsa.PublicKey) - } - - // send encrypted password - err = mc.sendEncryptedPassword(oldAuthData, pubKey) - if err != nil { - return err - } - } - return mc.resultUnchanged().readResultOK() - - default: - return ErrMalformPkt - } - default: - return ErrMalformPkt - } - - case "sha256_password": - switch len(authData) { - case 0: - return nil // auth successful - default: - block, _ := pem.Decode(authData) - if block == nil { - return fmt.Errorf("no Pem data found, data: %s", authData) - } - - pub, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return err - } - - // send encrypted password - err = mc.sendEncryptedPassword(oldAuthData, pub.(*rsa.PublicKey)) - if err != nil { - return err - } - return mc.resultUnchanged().readResultOK() - } - - default: - return nil // auth successful + plugin := string(data[1:pluginEndIndex]) + authData := data[pluginEndIndex+1:] + if len(authData) > 0 && authData[len(authData)-1] == 0 { + authData = authData[:len(authData)-1] } - return err + savedAuthData := make([]byte, len(authData)) + copy(savedAuthData, authData) + return plugin, savedAuthData } diff --git a/auth_caching_sha2.go b/auth_caching_sha2.go new file mode 100644 index 000000000..c50eb8a5a --- /dev/null +++ b/auth_caching_sha2.go @@ -0,0 +1,176 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package mysql + +import ( + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "fmt" +) + +// CachingSha2PasswordPlugin implements the caching_sha2_password authentication +// This plugin provides secure password-based authentication using SHA256 and RSA encryption, +// with server-side caching of password verifiers for improved performance. +type CachingSha2PasswordPlugin struct { + AuthPlugin +} + +func init() { + RegisterAuthPlugin(&CachingSha2PasswordPlugin{}) +} + +func (p *CachingSha2PasswordPlugin) PluginName() string { + return "caching_sha2_password" +} + +// InitAuth initializes the authentication process by scrambling the password. +// +// The scrambling process uses a three-step SHA256 hash: +// 1. SHA256(password) +// 2. SHA256(SHA256(password)) +// 3. XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble)) +func (p *CachingSha2PasswordPlugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) { + return scrambleSHA256Password(authData, cfg.Passwd), nil +} + +// ContinuationAuth processes the server's response to our authentication attempt. +// +// The authentication flow can take several paths: +// 1. Fast auth success (password found in cache) +// 2. Full authentication needed: +// a. With TLS: send cleartext password +// b. Without TLS: +// - Request server's public key if not cached +// - Encrypt password with RSA public key +// - Send encrypted password +func (p *CachingSha2PasswordPlugin) continuationAuth(packet []byte, authData []byte, mc *mysqlConn) ([]byte, error) { + if len(packet) == 0 { + return nil, fmt.Errorf("%w: empty auth response packet", ErrMalformPkt) + } + + switch packet[0] { + case iOK, iERR, iEOF: + return packet, nil + case iAuthMoreData: + switch len(packet) { + case 1: + return mc.readPacket() // Auth successful + + case 2: + switch packet[1] { + case 3: + // the password was found in the server's cache + return mc.readPacket() + + case 4: + // indicates full authentication is needed + // For TLS connections or Unix socket, send cleartext password + if mc.cfg.TLS != nil || mc.cfg.Net == "unix" { + err := mc.writeAuthSwitchPacket(append([]byte(mc.cfg.Passwd), 0)) + if err != nil { + return nil, fmt.Errorf("failed to send cleartext password: %w", err) + } + } else { + // For non-TLS connections, use RSA encryption + pubKey := mc.cfg.pubKey + if pubKey == nil { + // Request public key from server + packet, err := mc.buf.takeSmallBuffer(4 + 1) + if err != nil { + return nil, fmt.Errorf("failed to allocate buffer: %w", err) + } + packet[4] = 2 + if err = mc.writePacket(packet); err != nil { + return nil, fmt.Errorf("failed to request public key: %w", err) + } + + // Read public key packet + if packet, err = mc.readPacket(); err != nil { + return nil, fmt.Errorf("failed to read public key: %w", err) + } + + if packet[0] != iAuthMoreData { + return nil, fmt.Errorf("unexpected packet type %d when requesting public key", packet[0]) + } + + // Parse public key from PEM format + block, rest := pem.Decode(packet[1:]) + if block == nil { + return nil, fmt.Errorf("invalid PEM data in auth response: %q", rest) + } + + // Parse the public key + pkix, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse public key: %w", err) + } + pubKey = pkix.(*rsa.PublicKey) + } + + // Encrypt and send password + enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey) + if err != nil { + return nil, fmt.Errorf("failed to encrypt password: %w", err) + } + if err = mc.writeAuthSwitchPacket(enc); err != nil { + return nil, fmt.Errorf("failed to send encrypted password: %w", err) + } + } + return mc.readPacket() + + default: + return nil, fmt.Errorf("%w: unknown auth state %d", ErrMalformPkt, packet[1]) + } + + default: + return nil, fmt.Errorf("%w: unexpected packet length %d", ErrMalformPkt, len(packet)) + } + default: + return nil, fmt.Errorf("%w: expected auth more data packet", ErrMalformPkt) + } +} + +// scrambleSHA256Password implements MySQL 8+ password scrambling. +// +// The algorithm is: +// 1. SHA256(password) +// 2. SHA256(SHA256(SHA256(password))) +// 3. XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble)) +// +// This provides a way to verify the password without storing it in cleartext. +func scrambleSHA256Password(scramble []byte, password string) []byte { + if len(password) == 0 { + return []byte{} + } + + // First hash: SHA256(password) + crypt := sha256.New() + crypt.Write([]byte(password)) + message1 := crypt.Sum(nil) + + // Second hash: SHA256(SHA256(password)) + crypt.Reset() + crypt.Write(message1) + message1Hash := crypt.Sum(nil) + + // Third hash: SHA256(SHA256(SHA256(password)), scramble) + crypt.Reset() + crypt.Write(message1Hash) + crypt.Write(scramble) + message2 := crypt.Sum(nil) + + // XOR the first hash with the third hash + for i := range message1 { + message1[i] ^= message2[i] + } + + return message1 +} diff --git a/auth_cleartext.go b/auth_cleartext.go new file mode 100644 index 000000000..34248856f --- /dev/null +++ b/auth_cleartext.go @@ -0,0 +1,46 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package mysql + +// ClearPasswordPlugin implements the mysql_clear_password authentication. +// +// This plugin sends passwords in cleartext and should only be used: +// 1. Over TLS/SSL connections +// 2. Over Unix domain sockets +// 3. When required by authentication methods like PAM +// +// See: 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 +type ClearPasswordPlugin struct { + SimpleAuth +} + +func init() { + RegisterAuthPlugin(&ClearPasswordPlugin{}) +} + +func (p *ClearPasswordPlugin) PluginName() string { + return "mysql_clear_password" +} + +// InitAuth implements the cleartext password authentication. +// It will return an error if AllowCleartextPasswords is false. +// +// The cleartext password is sent as a null-terminated string. +// This is required by the server to support external authentication +// systems that need access to the original password. +func (p *ClearPasswordPlugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) { + if !cfg.AllowCleartextPasswords { + return nil, ErrCleartextPassword + } + + // Send password as null-terminated string + return append([]byte(cfg.Passwd), 0), nil +} diff --git a/auth_ed25519.go b/auth_ed25519.go new file mode 100644 index 000000000..87d08f778 --- /dev/null +++ b/auth_ed25519.go @@ -0,0 +1,64 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package mysql + +import ( + "crypto/sha512" + "filippo.io/edwards25519" +) + +// ClientEd25519Plugin implements the client_ed25519 authentication +type ClientEd25519Plugin struct { + SimpleAuth +} + +func init() { + RegisterAuthPlugin(&ClientEd25519Plugin{}) +} + +func (p *ClientEd25519Plugin) PluginName() string { + return "client_ed25519" +} + +func (p *ClientEd25519Plugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) { + // Derived from https://github.com/MariaDB/server/blob/d8e6bb00888b1f82c031938f4c8ac5d97f6874c3/plugin/auth_ed25519/ref10/sign.c + // Code style is from https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/crypto/ed25519/ed25519.go;l=207 + h := sha512.Sum512([]byte(cfg.Passwd)) + + s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) + if err != nil { + return nil, err + } + A := (&edwards25519.Point{}).ScalarBaseMult(s) + + mh := sha512.New() + mh.Write(h[32:]) + mh.Write(authData) + messageDigest := mh.Sum(nil) + r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest) + if err != nil { + return nil, err + } + + R := (&edwards25519.Point{}).ScalarBaseMult(r) + + kh := sha512.New() + kh.Write(R.Bytes()) + kh.Write(A.Bytes()) + kh.Write(authData) + hramDigest := kh.Sum(nil) + k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest) + if err != nil { + return nil, err + } + + S := k.MultiplyAdd(k, s, r) + + return append(R.Bytes(), S.Bytes()...), nil +} diff --git a/auth_mysql_native.go b/auth_mysql_native.go new file mode 100644 index 000000000..b5fd54683 --- /dev/null +++ b/auth_mysql_native.go @@ -0,0 +1,64 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package mysql + +import "crypto/sha1" + +// NativePasswordPlugin implements the mysql_native_password authentication +type NativePasswordPlugin struct { + SimpleAuth +} + +func init() { + RegisterAuthPlugin(&NativePasswordPlugin{}) +} + +func (p *NativePasswordPlugin) PluginName() string { + return "mysql_native_password" +} + +func (p *NativePasswordPlugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) { + if !cfg.AllowNativePasswords { + return nil, ErrNativePassword + } + if cfg.Passwd == "" { + return nil, nil + } + return p.scramblePassword(authData[:20], cfg.Passwd), nil +} + +// Hash password using 4.1+ method (SHA1) +func (p *NativePasswordPlugin) scramblePassword(scramble []byte, password string) []byte { + if len(password) == 0 { + return nil + } + + // stage1Hash = SHA1(password) + crypt := sha1.New() + crypt.Write([]byte(password)) + stage1 := crypt.Sum(nil) + + // scrambleHash = SHA1(scramble + SHA1(stage1Hash)) + // inner Hash + crypt.Reset() + crypt.Write(stage1) + hash := crypt.Sum(nil) + + // outer Hash + crypt.Reset() + crypt.Write(scramble) + crypt.Write(hash) + scramble = crypt.Sum(nil) + + // token = scrambleHash XOR stage1Hash + for i := range scramble { + scramble[i] ^= stage1[i] + } + return scramble +} diff --git a/auth_old_password.go b/auth_old_password.go new file mode 100644 index 000000000..245760e16 --- /dev/null +++ b/auth_old_password.go @@ -0,0 +1,108 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package mysql + +// OldPasswordPlugin implements the mysql_old_password authentication +type OldPasswordPlugin struct{ SimpleAuth } + +func init() { + RegisterAuthPlugin(&OldPasswordPlugin{}) +} + +func (p *OldPasswordPlugin) PluginName() string { + return "mysql_old_password" +} + +func (p *OldPasswordPlugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) { + if !cfg.AllowOldPasswords { + return nil, ErrOldPassword + } + if cfg.Passwd == "" { + return nil, nil + } + // 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 + return append(p.scrambleOldPassword(authData[:8], cfg.Passwd), 0), nil +} + +// Hash password using insecure pre 4.1 method +func (p *OldPasswordPlugin) scrambleOldPassword(scramble []byte, password string) []byte { + scramble = scramble[:8] + + hashPw := pwHash([]byte(password)) + hashSc := pwHash(scramble) + + r := newMyRnd(hashPw[0]^hashSc[0], hashPw[1]^hashSc[1]) + + var out [8]byte + for i := range out { + out[i] = r.NextByte() + 64 + } + + mask := r.NextByte() + for i := range out { + out[i] ^= mask + } + + return out[:] +} + +// Hash password using pre 4.1 (old password) method +// https://github.com/atcurtis/mariadb/blob/master/mysys/my_rnd.c +type myRnd struct { + seed1, seed2 uint32 +} + +// Tested to be equivalent to MariaDB's floating point variant +// http://play.golang.org/p/QHvhd4qved +// http://play.golang.org/p/RG0q4ElWDx +func (r *myRnd) NextByte() byte { + r.seed1 = (r.seed1*3 + r.seed2) % myRndMaxVal + r.seed2 = (r.seed1 + r.seed2 + 33) % myRndMaxVal + + return byte(uint64(r.seed1) * 31 / myRndMaxVal) +} + +const myRndMaxVal = 0x3FFFFFFF + +// Pseudo random number generator +func newMyRnd(seed1, seed2 uint32) *myRnd { + return &myRnd{ + seed1: seed1 % myRndMaxVal, + seed2: seed2 % myRndMaxVal, + } +} + +// Generate binary hash from byte string using insecure pre 4.1 method +func pwHash(password []byte) (result [2]uint32) { + var add uint32 = 7 + var tmp uint32 + + result[0] = 1345345333 + result[1] = 0x12345671 + + for _, c := range password { + // skip spaces and tabs in password + if c == ' ' || c == '\t' { + continue + } + + tmp = uint32(c) + result[0] ^= (((result[0] & 63) + add) * tmp) + (result[0] << 8) + result[1] += (result[1] << 8) ^ result[0] + add += tmp + } + + // Remove sign bit (1<<31)-1) + result[0] &= 0x7FFFFFFF + result[1] &= 0x7FFFFFFF + + return +} diff --git a/auth_plugin.go b/auth_plugin.go new file mode 100644 index 000000000..509d566f0 --- /dev/null +++ b/auth_plugin.go @@ -0,0 +1,63 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package mysql + +// AuthPlugin represents an authentication plugin for MySQL/MariaDB +type AuthPlugin interface { + // PluginName returns the name of the authentication plugin + PluginName() string + + // InitAuth initializes the authentication process and returns the initial response + // authData is the challenge data from the server + // password is the password for authentication + InitAuth(authData []byte, cfg *Config) ([]byte, error) + + // continuationAuth processes the authentication response from the server + // packet is the data from the server's auth response + // authData is the initial auth data from the server + // conn is the MySQL connection (for performing additional interactions if needed) + continuationAuth(packet []byte, authData []byte, conn *mysqlConn) ([]byte, error) +} + +type SimpleAuth struct { + AuthPlugin +} + +func (s SimpleAuth) continuationAuth(packet []byte, authData []byte, conn *mysqlConn) ([]byte, error) { + return packet, nil +} + +// pluginRegistry is a registry of available authentication plugins +type pluginRegistry struct { + plugins map[string]AuthPlugin +} + +// NewPluginRegistry creates a new plugin registry +func newPluginRegistry() *pluginRegistry { + registry := &pluginRegistry{ + plugins: make(map[string]AuthPlugin), + } + return registry +} + +// Register adds a plugin to the registry +func (r *pluginRegistry) Register(plugin AuthPlugin) { + r.plugins[plugin.PluginName()] = plugin +} + +// GetPlugin returns the plugin for the given name +func (r *pluginRegistry) GetPlugin(name string) (AuthPlugin, bool) { + plugin, ok := r.plugins[name] + return plugin, ok +} + +// RegisterAuthPlugin registers the plugin to the global plugin registry +func RegisterAuthPlugin(plugin AuthPlugin) { + globalPluginRegistry.Register(plugin) +} diff --git a/auth_sha256.go b/auth_sha256.go new file mode 100644 index 000000000..caab0e70d --- /dev/null +++ b/auth_sha256.go @@ -0,0 +1,130 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package mysql + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/pem" + "fmt" +) + +// Sha256PasswordPlugin implements the sha256_password authentication +// This plugin provides secure password-based authentication using SHA256 and RSA encryption. +type Sha256PasswordPlugin struct{ AuthPlugin } + +func init() { + RegisterAuthPlugin(&Sha256PasswordPlugin{}) +} + +func (p *Sha256PasswordPlugin) PluginName() string { + return "sha256_password" +} + +// InitAuth initializes the authentication process. +// +// The function follows these rules: +// 1. If no password is configured, sends a single byte indicating empty password +// 2. If TLS is enabled, sends the password in cleartext +// 3. If a public key is available, encrypts the password and sends it +// 4. Otherwise, requests the server's public key +func (p *Sha256PasswordPlugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) { + if len(cfg.Passwd) == 0 { + return []byte{0}, nil + } + + // Unlike caching_sha2_password, sha256_password does not accept + // cleartext password on unix transport. + if cfg.TLS != nil { + // Write cleartext auth packet + return append([]byte(cfg.Passwd), 0), nil + } + + if cfg.pubKey == nil { + // Request public key from server + return []byte{1}, nil + } + + // Encrypt password using the public key + enc, err := encryptPassword(cfg.Passwd, authData, cfg.pubKey) + if err != nil { + return nil, fmt.Errorf("failed to encrypt password: %w", err) + } + return enc, nil +} + +// ContinuationAuth processes the server's response to our authentication attempt. +// +// The server can respond in three ways: +// 1. OK packet - Authentication successful +// 2. Error packet - Authentication failed +// 3. More data packet - Contains the server's public key for password encryption +func (p *Sha256PasswordPlugin) continuationAuth(packet []byte, authData []byte, mc *mysqlConn) ([]byte, error) { + + switch packet[0] { + case iOK, iERR, iEOF: + return packet, nil + + case iAuthMoreData: + // Parse public key from PEM format + block, rest := pem.Decode(packet[1:]) + if block == nil { + return nil, fmt.Errorf("invalid PEM data in auth response: %q", rest) + } + + // Parse the public key + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse public key: %w", err) + } + + // Send encrypted password + enc, err := encryptPassword(mc.cfg.Passwd, authData, pub.(*rsa.PublicKey)) + if err != nil { + return nil, fmt.Errorf("failed to encrypt password with server key: %w", err) + } + + // Send the encrypted password + if err = mc.writeAuthSwitchPacket(enc); err != nil { + return nil, fmt.Errorf("failed to send encrypted password: %w", err) + } + + return mc.readPacket() + + default: + return nil, fmt.Errorf("%w: unexpected packet type %d", ErrMalformPkt, packet[0]) + } +} + +// encryptPassword encrypts the password using RSA-OAEP with SHA1 hash. +// +// The process: +// 1. XORs the password with the auth seed to prevent replay attacks +// 2. Encrypts the XORed password using RSA-OAEP with SHA1 +// +// The encryption uses OAEP padding which is more secure than PKCS#1 v1.5 padding. +func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, error) { + if pub == nil { + return nil, fmt.Errorf("public key is nil") + } + + // Create the plaintext by XORing password with seed + plain := make([]byte, len(password)+1) + copy(plain, password) + for i := range plain { + j := i % len(seed) + plain[i] ^= seed[j] + } + + // Encrypt using RSA-OAEP with SHA1 + sha1Hash := sha1.New() + return rsa.EncryptOAEP(sha1Hash, rand.Reader, pub, plain, nil) +} diff --git a/auth_test.go b/auth_test.go index 46e1e3b4e..c085c36fd 100644 --- a/auth_test.go +++ b/auth_test.go @@ -50,8 +50,12 @@ func TestScrambleOldPass(t *testing.T) { {"123\t456", "575c47505b5b5559"}, {"C0mpl!ca ted#PASS123", "5d5d554849584a45"}, } + + // Send Client Authentication Packet + authPlugin := OldPasswordPlugin{} + for _, tuple := range vectors { - ours := scrambleOldPassword(scramble, tuple.pass) + ours := authPlugin.scrambleOldPassword(scramble, tuple.pass) if tuple.out != fmt.Sprintf("%x", ours) { t.Errorf("Failed old password %q", tuple.pass) } @@ -85,7 +89,8 @@ func TestAuthFastCachingSHA256PasswordCached(t *testing.T) { plugin := "caching_sha2_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -114,8 +119,7 @@ func TestAuthFastCachingSHA256PasswordCached(t *testing.T) { } conn.maxReads = 1 - // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } } @@ -130,7 +134,8 @@ func TestAuthFastCachingSHA256PasswordEmpty(t *testing.T) { plugin := "caching_sha2_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -156,8 +161,7 @@ func TestAuthFastCachingSHA256PasswordEmpty(t *testing.T) { } conn.maxReads = 1 - // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } } @@ -172,12 +176,14 @@ func TestAuthFastCachingSHA256PasswordFullRSA(t *testing.T) { plugin := "caching_sha2_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + conn.data = []byte{0x01, 0x00, 0x00, 0x00, 0xff} + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } - err = mc.writeHandshakeResponsePacket(authResp, plugin) - if err != nil { + + if err = mc.writeHandshakeResponsePacket(authResp, plugin); err != nil { t.Fatal(err) } @@ -207,8 +213,7 @@ func TestAuthFastCachingSHA256PasswordFullRSA(t *testing.T) { } conn.maxReads = 3 - // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } @@ -228,7 +233,8 @@ func TestAuthFastCachingSHA256PasswordFullRSAWithKey(t *testing.T) { plugin := "caching_sha2_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -261,7 +267,7 @@ func TestAuthFastCachingSHA256PasswordFullRSAWithKey(t *testing.T) { conn.maxReads = 2 // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } @@ -280,7 +286,8 @@ func TestAuthFastCachingSHA256PasswordFullSecure(t *testing.T) { plugin := "caching_sha2_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -317,7 +324,7 @@ func TestAuthFastCachingSHA256PasswordFullSecure(t *testing.T) { conn.maxReads = 3 // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } @@ -336,7 +343,8 @@ func TestAuthFastCleartextPasswordNotAllowed(t *testing.T) { plugin := "mysql_clear_password" // Send Client Authentication Packet - _, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + _, err := authPlugin.InitAuth(authData, mc.cfg) if err != ErrCleartextPassword { t.Errorf("expected ErrCleartextPassword, got %v", err) } @@ -353,7 +361,8 @@ func TestAuthFastCleartextPassword(t *testing.T) { plugin := "mysql_clear_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -379,8 +388,7 @@ func TestAuthFastCleartextPassword(t *testing.T) { } conn.maxReads = 1 - // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } } @@ -396,7 +404,8 @@ func TestAuthFastCleartextPasswordEmpty(t *testing.T) { plugin := "mysql_clear_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -422,8 +431,7 @@ func TestAuthFastCleartextPasswordEmpty(t *testing.T) { } conn.maxReads = 1 - // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } } @@ -439,7 +447,8 @@ func TestAuthFastNativePasswordNotAllowed(t *testing.T) { plugin := "mysql_native_password" // Send Client Authentication Packet - _, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + _, err := authPlugin.InitAuth(authData, mc.cfg) if err != ErrNativePassword { t.Errorf("expected ErrNativePassword, got %v", err) } @@ -455,7 +464,8 @@ func TestAuthFastNativePassword(t *testing.T) { plugin := "mysql_native_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -482,8 +492,7 @@ func TestAuthFastNativePassword(t *testing.T) { } conn.maxReads = 1 - // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } } @@ -498,7 +507,8 @@ func TestAuthFastNativePasswordEmpty(t *testing.T) { plugin := "mysql_native_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -524,8 +534,7 @@ func TestAuthFastNativePasswordEmpty(t *testing.T) { } conn.maxReads = 1 - // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } } @@ -540,7 +549,8 @@ func TestAuthFastSHA256PasswordEmpty(t *testing.T) { plugin := "sha256_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -569,7 +579,7 @@ func TestAuthFastSHA256PasswordEmpty(t *testing.T) { conn.maxReads = 2 // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } @@ -588,7 +598,8 @@ func TestAuthFastSHA256PasswordRSA(t *testing.T) { plugin := "sha256_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -617,7 +628,7 @@ func TestAuthFastSHA256PasswordRSA(t *testing.T) { conn.maxReads = 2 // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } @@ -637,7 +648,8 @@ func TestAuthFastSHA256PasswordRSAWithKey(t *testing.T) { plugin := "sha256_password" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -651,7 +663,7 @@ func TestAuthFastSHA256PasswordRSAWithKey(t *testing.T) { conn.maxReads = 1 // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } } @@ -670,7 +682,8 @@ func TestAuthFastSHA256PasswordSecure(t *testing.T) { plugin := "sha256_password" // send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -698,8 +711,7 @@ func TestAuthFastSHA256PasswordSecure(t *testing.T) { conn.data = []byte{7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0} conn.maxReads = 1 - // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, authPlugin); err != nil { t.Errorf("got error: %v", err) } @@ -726,9 +738,7 @@ func TestAuthSwitchCachingSHA256PasswordCached(t *testing.T) { authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } @@ -759,9 +769,7 @@ func TestAuthSwitchCachingSHA256PasswordEmpty(t *testing.T) { authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } @@ -795,12 +803,9 @@ func TestAuthSwitchCachingSHA256PasswordFullRSA(t *testing.T) { authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } - expectedReplyPrefix := []byte{ // 1. Packet: Hash 32, 0, 0, 3, 219, 72, 64, 97, 56, 197, 167, 203, 64, 236, 168, 80, 223, @@ -840,12 +845,9 @@ func TestAuthSwitchCachingSHA256PasswordFullRSAWithKey(t *testing.T) { authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } - expectedReplyPrefix := []byte{ // 1. Packet: Hash 32, 0, 0, 3, 219, 72, 64, 97, 56, 197, 167, 203, 64, 236, 168, 80, 223, @@ -883,12 +885,9 @@ func TestAuthSwitchCachingSHA256PasswordFullSecure(t *testing.T) { authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } - expectedReply := []byte{ // 1. Packet: Hash 32, 0, 0, 3, 219, 72, 64, 97, 56, 197, 167, 203, 64, 236, 168, 80, 223, @@ -911,8 +910,7 @@ func TestAuthSwitchCleartextPasswordNotAllowed(t *testing.T) { conn.maxReads = 1 authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - err := mc.handleAuthResult(authData, plugin) + err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}) if err != ErrCleartextPassword { t.Errorf("expected ErrCleartextPassword, got %v", err) } @@ -933,12 +931,9 @@ func TestAuthSwitchCleartextPassword(t *testing.T) { authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } - expectedReply := []byte{7, 0, 0, 3, 115, 101, 99, 114, 101, 116, 0} if !bytes.Equal(conn.written, expectedReply) { t.Errorf("got unexpected data: %v", conn.written) @@ -960,12 +955,9 @@ func TestAuthSwitchCleartextPasswordEmpty(t *testing.T) { authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } - expectedReply := []byte{1, 0, 0, 3, 0} if !bytes.Equal(conn.written, expectedReply) { t.Errorf("got unexpected data: %v", conn.written) @@ -983,8 +975,7 @@ func TestAuthSwitchNativePasswordNotAllowed(t *testing.T) { conn.maxReads = 1 authData := []byte{96, 71, 63, 8, 1, 58, 75, 12, 69, 95, 66, 60, 117, 31, 48, 31, 89, 39, 55, 31} - plugin := "caching_sha2_password" - err := mc.handleAuthResult(authData, plugin) + err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}) if err != ErrNativePassword { t.Errorf("expected ErrNativePassword, got %v", err) } @@ -1007,9 +998,7 @@ func TestAuthSwitchNativePassword(t *testing.T) { authData := []byte{96, 71, 63, 8, 1, 58, 75, 12, 69, 95, 66, 60, 117, 31, 48, 31, 89, 39, 55, 31} - plugin := "caching_sha2_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } @@ -1037,12 +1026,9 @@ func TestAuthSwitchNativePasswordEmpty(t *testing.T) { authData := []byte{96, 71, 63, 8, 1, 58, 75, 12, 69, 95, 66, 60, 117, 31, 48, 31, 89, 39, 55, 31} - plugin := "caching_sha2_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } - expectedReply := []byte{0, 0, 0, 3} if !bytes.Equal(conn.written, expectedReply) { t.Errorf("got unexpected data: %v", conn.written) @@ -1058,8 +1044,7 @@ func TestAuthSwitchOldPasswordNotAllowed(t *testing.T) { conn.maxReads = 1 authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35, 84, 96, 101, 92, 123, 121, 107} - plugin := "mysql_native_password" - err := mc.handleAuthResult(authData, plugin) + err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}) if err != ErrOldPassword { t.Errorf("expected ErrOldPassword, got %v", err) } @@ -1074,8 +1059,7 @@ func TestOldAuthSwitchNotAllowed(t *testing.T) { conn.maxReads = 1 authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35, 84, 96, 101, 92, 123, 121, 107} - plugin := "mysql_native_password" - err := mc.handleAuthResult(authData, plugin) + err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}) if err != ErrOldPassword { t.Errorf("expected ErrOldPassword, got %v", err) } @@ -1097,9 +1081,7 @@ func TestAuthSwitchOldPassword(t *testing.T) { authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35, 84, 96, 101, 92, 123, 121, 107} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } @@ -1124,9 +1106,7 @@ func TestOldAuthSwitch(t *testing.T) { authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35, 84, 96, 101, 92, 123, 121, 107} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, authData, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } @@ -1149,11 +1129,7 @@ func TestAuthSwitchOldPasswordEmpty(t *testing.T) { conn.queuedReplies = [][]byte{{8, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0}} conn.maxReads = 2 - authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35, - 84, 96, 101, 92, 123, 121, 107} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, []byte{}, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } @@ -1176,11 +1152,7 @@ func TestOldAuthSwitchPasswordEmpty(t *testing.T) { conn.queuedReplies = [][]byte{{8, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0}} conn.maxReads = 2 - authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35, - 84, 96, 101, 92, 123, 121, 107} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, []byte{}, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } @@ -1205,11 +1177,7 @@ func TestAuthSwitchSHA256PasswordEmpty(t *testing.T) { } conn.maxReads = 3 - authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, - 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, []byte{}, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } @@ -1240,11 +1208,7 @@ func TestAuthSwitchSHA256PasswordRSA(t *testing.T) { } conn.maxReads = 3 - authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, - 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, []byte{}, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } @@ -1276,11 +1240,7 @@ func TestAuthSwitchSHA256PasswordRSAWithKey(t *testing.T) { } conn.maxReads = 2 - authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, - 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, []byte{}, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } @@ -1312,11 +1272,7 @@ func TestAuthSwitchSHA256PasswordSecure(t *testing.T) { } conn.maxReads = 2 - authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19, - 47, 43, 9, 41, 112, 67, 110} - plugin := "mysql_native_password" - - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err := mc.handleAuthResult(5, []byte{}, &NativePasswordPlugin{}); err != nil { t.Errorf("got error: %v", err) } @@ -1339,7 +1295,8 @@ func TestEd25519Auth(t *testing.T) { plugin := "client_ed25519" // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authPlugin, _ := globalPluginRegistry.GetPlugin(plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { t.Fatal(err) } @@ -1348,11 +1305,6 @@ func TestEd25519Auth(t *testing.T) { t.Fatal(err) } - // check written auth response - authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1 - authRespEnd := authRespStart + 1 + len(authResp) - writtenAuthRespLen := conn.written[authRespStart] - writtenAuthResp := conn.written[authRespStart+1 : authRespEnd] expectedAuthResp := []byte{ 232, 61, 201, 63, 67, 63, 51, 53, 86, 73, 238, 35, 170, 117, 146, 214, 26, 17, 35, 9, 8, 132, 245, 141, 48, 99, 66, 58, 36, 228, 48, @@ -1360,11 +1312,11 @@ func TestEd25519Auth(t *testing.T) { 68, 117, 56, 135, 171, 47, 20, 14, 133, 79, 15, 229, 124, 160, 176, 100, 138, 14, } - if writtenAuthRespLen != 64 { - t.Fatalf("expected 64 bytes from client, got %d", writtenAuthRespLen) + if len(authResp) != 64 { + t.Fatalf("expected 64 bytes from client, got %d", len(authResp)) } - if !bytes.Equal(writtenAuthResp, expectedAuthResp) { - t.Fatalf("auth response did not match expected value:\n%v\n%v", writtenAuthResp, expectedAuthResp) + if !bytes.Equal(authResp, expectedAuthResp) { + t.Fatalf("auth response did not match expected value:\n%v\n%v", authResp, expectedAuthResp) } conn.written = nil @@ -1375,7 +1327,87 @@ func TestEd25519Auth(t *testing.T) { conn.maxReads = 1 // Handle response to auth packet - if err := mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(5, []byte{}, authPlugin); err != nil { t.Errorf("got error: %v", err) } } + +// test 2 authentication switch +func TestMultiAuthSimpleSwitch(t *testing.T) { + conn, mc := newRWMockConn(2) + mc.cfg.Passwd = "secret" + mc.cfg.AllowCleartextPasswords = true + mc.cfg.pubKey = testPubKeyRSA + + // auth switch request + conn.data = []byte{38, 0, 0, 2, 254, 115, 104, 97, 50, 53, 54, 95, 112, 97, + 115, 115, 119, 111, 114, 100, 0, 78, 82, 62, 40, 100, 1, 59, 31, 44, 69, + 33, 112, 8, 81, 51, 96, 65, 82, 16, 114, 0} + + conn.queuedReplies = [][]byte{ + // cleartext password + {22, 0, 0, 4, 254, 109, 121, 115, 113, 108, 95, 99, 108, + 101, 97, 114, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0}, + + // OK + {7, 0, 0, 6, 0, 0, 0, 2, 0, 0, 0}, + } + conn.maxReads = 5 + + if err := mc.handleAuthResult(5, []byte{}, &NativePasswordPlugin{}); err != nil { + t.Errorf("got error: %v", err) + } + + // caching_sha2_password + if !bytes.HasPrefix(conn.written, []byte{0, 1, 0, 3}) { + t.Errorf("got unexpected data: %v", conn.written) + } + + if !bytes.HasSuffix(conn.written, []byte{7, 0, 0, 5, 115, 101, 99, 114, 101, 116, 0}) { // cleartext password + t.Errorf("got unexpected data: %v", conn.written) + } + +} + +// test 2 authentication switch +func TestMultiAuthSwitch(t *testing.T) { + conn, mc := newRWMockConn(2) + mc.cfg.Passwd = "secret" + mc.cfg.AllowCleartextPasswords = true + + // auth switch request + conn.data = []byte{38, 0, 0, 2, 254, 115, 104, 97, 50, 53, 54, 95, 112, 97, + 115, 115, 119, 111, 114, 100, 0, 78, 82, 62, 40, 100, 1, 59, 31, 44, 69, + 33, 112, 8, 81, 51, 96, 65, 82, 16, 114, 0} + + conn.queuedReplies = [][]byte{ + // Pub Key Response + append([]byte{byte(1 + len(testPubKey)), 1, 0, 4, 1}, testPubKey...), + + // cleartext password + {22, 0, 0, 6, 254, 109, 121, 115, 113, 108, 95, 99, 108, + 101, 97, 114, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0}, + + // OK + {7, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0}, + } + conn.maxReads = 5 + + if err := mc.handleAuthResult(5, []byte{}, &NativePasswordPlugin{}); err != nil { + t.Errorf("got error: %v", err) + } + + expectedReplyPrefix := []byte{ + // 1. Packet: Pub Key Request + 1, 0, 0, 3, 1, + + // 2. Packet: Encrypted Password + 0, 1, 0, 5, // [changing bytes] + } + if !bytes.HasPrefix(conn.written, expectedReplyPrefix) { + t.Errorf("got unexpected data: %v", conn.written) + } + if !bytes.HasSuffix(conn.written, []byte{7, 0, 0, 7, 115, 101, 99, 114, 101, 116, 0}) { // cleartext password + t.Errorf("got unexpected data: %v", conn.written) + } +} diff --git a/connector.go b/connector.go index bc1d46afc..d090b7ff2 100644 --- a/connector.go +++ b/connector.go @@ -18,6 +18,10 @@ import ( "strings" ) +const ( + authMaximumSwitch = 5 +) + type connector struct { cfg *Config // immutable private copy. encodedAttributes string // Encoded connection attributes. @@ -140,18 +144,16 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { if plugin == "" { plugin = defaultAuthPlugin } + authPlugin, exists := globalPluginRegistry.GetPlugin(plugin) + if !exists { + return nil, fmt.Errorf("this authentication plugin '%s' is not supported", plugin) + } // Send Client Authentication Packet - authResp, err := mc.auth(authData, plugin) + authResp, err := authPlugin.InitAuth(authData, mc.cfg) if err != nil { - // try the default auth plugin, if using the requested plugin failed - c.cfg.Logger.Print("could not use requested auth plugin '"+plugin+"': ", err.Error()) - plugin = defaultAuthPlugin - authResp, err = mc.auth(authData, plugin) - if err != nil { - mc.cleanup() - return nil, err - } + mc.cleanup() + return nil, err } if err = mc.writeHandshakeResponsePacket(authResp, plugin); err != nil { mc.cleanup() @@ -159,7 +161,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { } // Handle response to auth packet, switch methods if possible - if err = mc.handleAuthResult(authData, plugin); err != nil { + if err = mc.handleAuthResult(authMaximumSwitch, authData, authPlugin); 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. diff --git a/const.go b/const.go index 4aadcd642..8a8471bc0 100644 --- a/const.go +++ b/const.go @@ -184,9 +184,3 @@ const ( statusInTransReadonly statusSessionStateChanged ) - -const ( - cachingSha2PasswordRequestPublicKey = 2 - cachingSha2PasswordFastAuthSuccess = 3 - cachingSha2PasswordPerformFullAuthentication = 4 -) diff --git a/driver.go b/driver.go index 105316b81..e7845f61e 100644 --- a/driver.go +++ b/driver.go @@ -39,8 +39,9 @@ type DialFunc func(addr string) (net.Conn, error) type DialContextFunc func(ctx context.Context, addr string) (net.Conn, error) var ( - dialsLock sync.RWMutex - dials map[string]DialContextFunc + dialsLock sync.RWMutex + dials map[string]DialContextFunc + globalPluginRegistry = newPluginRegistry() ) // RegisterDialContext registers a custom dial function. It can then be used by the diff --git a/packets.go b/packets.go index e6e1704b3..9581a9b94 100644 --- a/packets.go +++ b/packets.go @@ -493,44 +493,6 @@ func (mc *mysqlConn) writeCommandPacketUint32(command byte, arg uint32) error { * Result Packets * ******************************************************************************/ -func (mc *mysqlConn) readAuthResult() ([]byte, string, error) { - data, err := mc.readPacket() - if err != nil { - return nil, "", err - } - - // packet indicator - switch data[0] { - - case iOK: - // resultUnchanged, since auth happens before any queries or - // commands have been executed. - return nil, "", mc.resultUnchanged().handleOkPacket(data) - - case iAuthMoreData: - return data[1:], "", err - - case iEOF: - if len(data) == 1 { - // https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::OldAuthSwitchRequest - return nil, "mysql_old_password", nil - } - pluginEndIndex := bytes.IndexByte(data, 0x00) - if pluginEndIndex < 0 { - return nil, "", ErrMalformPkt - } - plugin := string(data[1:pluginEndIndex]) - authData := data[pluginEndIndex+1:] - if len(authData) > 0 && authData[len(authData)-1] == 0 { - authData = authData[:len(authData)-1] - } - return authData, plugin, nil - - default: // Error otherwise - return nil, "", mc.handleErrorPacket(data) - } -} - // Returns error if Packet is not a 'Result OK'-Packet func (mc *okHandler) readResultOK() error { data, err := mc.conn().readPacket() From fde44b6541b7e93a427aca029662657c383f7969 Mon Sep 17 00:00:00 2001 From: Diego Dupin Date: Fri, 25 Apr 2025 15:26:19 +0200 Subject: [PATCH 2/4] Ensure proper error handling for invalid public key types in authentication flows --- auth_caching_sha2.go | 9 +++++++-- auth_sha256.go | 10 +++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/auth_caching_sha2.go b/auth_caching_sha2.go index c50eb8a5a..d66db6ef6 100644 --- a/auth_caching_sha2.go +++ b/auth_caching_sha2.go @@ -112,7 +112,12 @@ func (p *CachingSha2PasswordPlugin) continuationAuth(packet []byte, authData []b if err != nil { return nil, fmt.Errorf("failed to parse public key: %w", err) } - pubKey = pkix.(*rsa.PublicKey) + + var ok bool + pubKey, ok = pkix.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("server sent an invalid public key type: %T", pkix) + } } // Encrypt and send password @@ -142,7 +147,7 @@ func (p *CachingSha2PasswordPlugin) continuationAuth(packet []byte, authData []b // // The algorithm is: // 1. SHA256(password) -// 2. SHA256(SHA256(SHA256(password))) +// 2. SHA256(SHA256(password)) // 3. XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble)) // // This provides a way to verify the password without storing it in cleartext. diff --git a/auth_sha256.go b/auth_sha256.go index caab0e70d..711bb2d75 100644 --- a/auth_sha256.go +++ b/auth_sha256.go @@ -68,6 +68,9 @@ func (p *Sha256PasswordPlugin) InitAuth(authData []byte, cfg *Config) ([]byte, e // 2. Error packet - Authentication failed // 3. More data packet - Contains the server's public key for password encryption func (p *Sha256PasswordPlugin) continuationAuth(packet []byte, authData []byte, mc *mysqlConn) ([]byte, error) { + if len(packet) == 0 { + return nil, fmt.Errorf("%w: empty auth response packet", ErrMalformPkt) + } switch packet[0] { case iOK, iERR, iEOF: @@ -86,8 +89,13 @@ func (p *Sha256PasswordPlugin) continuationAuth(packet []byte, authData []byte, return nil, fmt.Errorf("failed to parse public key: %w", err) } + pubKey, ok := pub.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("server sent an invalid public key type: %T", pub) + } + // Send encrypted password - enc, err := encryptPassword(mc.cfg.Passwd, authData, pub.(*rsa.PublicKey)) + enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey) if err != nil { return nil, fmt.Errorf("failed to encrypt password with server key: %w", err) } From 0b6cb9af07a50dfe12a98a864208dd58f05e278c Mon Sep 17 00:00:00 2001 From: Diego Dupin Date: Fri, 25 Apr 2025 15:38:41 +0200 Subject: [PATCH 3/4] fix README authentication section --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 56e53a356..00d4f9892 100644 --- a/README.md +++ b/README.md @@ -538,13 +538,13 @@ See [context support in the database/sql package](https://golang.org/doc/go1.8#d The driver implements a pluggable authentication system that supports various authentication methods used by MySQL and MariaDB servers. The built-in authentication plugins include: -- `mysql_native_password` - The default MySQL authentication method -- `caching_sha2_password` - Default authentication method in MySQL 8.0+ -- `mysql_clear_password` - Cleartext authentication (requires `allowCleartextPasswords=true`) -- `mysql_old_password` - Old MySQL authentication (requires `allowOldPasswords=true`) -- `sha256_password` - SHA256 authentication -- `client_ed25519` - MariaDB Ed25519 authentication - +* `mysql_native_password` - The default MySQL authentication method +* `caching_sha2_password` - Default authentication method in MySQL 8.0+ +* `mysql_clear_password` - Cleartext authentication (requires `allowCleartextPasswords=true`) +* `mysql_old_password` - Old MySQL authentication (requires `allowOldPasswords=true`) +* `sha256_password` - SHA256 authentication +* `client_ed25519` - MariaDB Ed25519 authentication + ### `LOAD DATA LOCAL INFILE` support For this feature you need direct access to the package. Therefore you must change the import path (no `_`): ```go From 05b4bcf9960b30ffb842fceac28a41b8697e29e9 Mon Sep 17 00:00:00 2001 From: Diego Dupin Date: Fri, 25 Apr 2025 15:39:18 +0200 Subject: [PATCH 4/4] use named constants for caching_sha2_password --- auth_caching_sha2.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/auth_caching_sha2.go b/auth_caching_sha2.go index d66db6ef6..fc1597235 100644 --- a/auth_caching_sha2.go +++ b/auth_caching_sha2.go @@ -16,6 +16,12 @@ import ( "fmt" ) +// Authentication response constants +const ( + cachingSha2FastAuth = 3 // Password found in cache + cachingSha2FullAuthNeeded = 4 // Full authentication needed +) + // CachingSha2PasswordPlugin implements the caching_sha2_password authentication // This plugin provides secure password-based authentication using SHA256 and RSA encryption, // with server-side caching of password verifiers for improved performance. @@ -66,11 +72,11 @@ func (p *CachingSha2PasswordPlugin) continuationAuth(packet []byte, authData []b case 2: switch packet[1] { - case 3: + case cachingSha2FastAuth: // the password was found in the server's cache return mc.readPacket() - case 4: + case cachingSha2FullAuthNeeded: // indicates full authentication is needed // For TLS connections or Unix socket, send cleartext password if mc.cfg.TLS != nil || mc.cfg.Net == "unix" {