Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 5dd643c

Browse files
committedApr 16, 2025·
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, ...)
1 parent 98f445c commit 5dd643c

15 files changed

+883
-581
lines changed
 

‎AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Daniel Montoya <dsmontoyam at gmail.com>
3737
Daniel Nichter <nil at codenode.com>
3838
Daniël van Eeden <git at myname.nl>
3939
Dave Protasowski <dprotaso at gmail.com>
40+
Diego Dupin <diego.dupin at gmail.com>
4041
Dirkjan Bussink <d.bussink at gmail.com>
4142
DisposaBoy <disposaboy at dby.me>
4243
Egor Smolyakov <egorsmkv at gmail.com>

‎README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,17 @@ See [context support in the database/sql package](https://golang.org/doc/go1.8#d
534534
> 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.
535535
536536

537+
### Authentication Plugin System
538+
539+
The driver implements a pluggable authentication system that supports various authentication methods used by MySQL and MariaDB servers. The built-in authentication plugins include:
540+
541+
- `mysql_native_password` - The default MySQL authentication method
542+
- `caching_sha2_password` - Default authentication method in MySQL 8.0+
543+
- `mysql_clear_password` - Cleartext authentication (requires `allowCleartextPasswords=true`)
544+
- `mysql_old_password` - Old MySQL authentication (requires `allowOldPasswords=true`)
545+
- `sha256_password` - SHA256 authentication
546+
- `client_ed25519` - MariaDB Ed25519 authentication
547+
537548
### `LOAD DATA LOCAL INFILE` support
538549
For this feature you need direct access to the package. Therefore you must change the import path (no `_`):
539550
```go

‎auth.go

Lines changed: 46 additions & 398 deletions
Large diffs are not rendered by default.

‎auth_caching_sha2.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2+
//
3+
// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved.
4+
//
5+
// This Source Code Form is subject to the terms of the Mozilla Public
6+
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7+
// You can obtain one at http://mozilla.org/MPL/2.0/.
8+
9+
package mysql
10+
11+
import (
12+
"crypto/rsa"
13+
"crypto/sha256"
14+
"crypto/x509"
15+
"encoding/pem"
16+
"fmt"
17+
)
18+
19+
// CachingSha2PasswordPlugin implements the caching_sha2_password authentication
20+
// This plugin provides secure password-based authentication using SHA256 and RSA encryption,
21+
// with server-side caching of password verifiers for improved performance.
22+
type CachingSha2PasswordPlugin struct {
23+
AuthPlugin
24+
}
25+
26+
func init() {
27+
RegisterAuthPlugin(&CachingSha2PasswordPlugin{})
28+
}
29+
30+
func (p *CachingSha2PasswordPlugin) GetPluginName() string {
31+
return "caching_sha2_password"
32+
}
33+
34+
// InitAuth initializes the authentication process by scrambling the password.
35+
//
36+
// The scrambling process uses a three-step SHA256 hash:
37+
// 1. SHA256(password)
38+
// 2. SHA256(SHA256(password))
39+
// 3. XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble))
40+
func (p *CachingSha2PasswordPlugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) {
41+
return scrambleSHA256Password(authData, cfg.Passwd), nil
42+
}
43+
44+
// ContinuationAuth processes the server's response to our authentication attempt.
45+
//
46+
// The authentication flow can take several paths:
47+
// 1. Fast auth success (password found in cache)
48+
// 2. Full authentication needed:
49+
// a. With TLS: send cleartext password
50+
// b. Without TLS:
51+
// - Request server's public key if not cached
52+
// - Encrypt password with RSA public key
53+
// - Send encrypted password
54+
func (p *CachingSha2PasswordPlugin) ContinuationAuth(packet []byte, authData []byte, mc *mysqlConn) ([]byte, error) {
55+
if len(packet) == 0 {
56+
return nil, fmt.Errorf("%w: empty auth response packet", ErrMalformPkt)
57+
}
58+
59+
switch packet[0] {
60+
case iOK, iERR, iEOF:
61+
return packet, nil
62+
case iAuthMoreData:
63+
switch len(packet) {
64+
case 1:
65+
return mc.readPacket() // Auth successful
66+
67+
case 2:
68+
switch packet[1] {
69+
case 3:
70+
// the password was found in the server's cache
71+
return mc.readPacket()
72+
73+
case 4:
74+
// indicates full authentication is needed
75+
// For TLS connections or Unix socket, send cleartext password
76+
if mc.cfg.TLS != nil || mc.cfg.Net == "unix" {
77+
err := mc.writeAuthSwitchPacket(append([]byte(mc.cfg.Passwd), 0))
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to send cleartext password: %w", err)
80+
}
81+
} else {
82+
// For non-TLS connections, use RSA encryption
83+
pubKey := mc.cfg.pubKey
84+
if pubKey == nil {
85+
// Request public key from server
86+
packet, err := mc.buf.takeSmallBuffer(4 + 1)
87+
if err != nil {
88+
return nil, fmt.Errorf("failed to allocate buffer: %w", err)
89+
}
90+
packet[4] = 2
91+
if err = mc.writePacket(packet); err != nil {
92+
return nil, fmt.Errorf("failed to request public key: %w", err)
93+
}
94+
95+
// Read public key packet
96+
if packet, err = mc.readPacket(); err != nil {
97+
return nil, fmt.Errorf("failed to read public key: %w", err)
98+
}
99+
100+
if packet[0] != iAuthMoreData {
101+
return nil, fmt.Errorf("unexpected packet type %d when requesting public key", packet[0])
102+
}
103+
104+
// Parse public key from PEM format
105+
block, rest := pem.Decode(packet[1:])
106+
if block == nil {
107+
return nil, fmt.Errorf("invalid PEM data in auth response: %q", rest)
108+
}
109+
110+
// Parse the public key
111+
pkix, err := x509.ParsePKIXPublicKey(block.Bytes)
112+
if err != nil {
113+
return nil, fmt.Errorf("failed to parse public key: %w", err)
114+
}
115+
pubKey = pkix.(*rsa.PublicKey)
116+
}
117+
118+
// Encrypt and send password
119+
enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey)
120+
if err != nil {
121+
return nil, fmt.Errorf("failed to encrypt password: %w", err)
122+
}
123+
if err = mc.writeAuthSwitchPacket(enc); err != nil {
124+
return nil, fmt.Errorf("failed to send encrypted password: %w", err)
125+
}
126+
}
127+
return mc.readPacket()
128+
129+
default:
130+
return nil, fmt.Errorf("%w: unknown auth state %d", ErrMalformPkt, packet[1])
131+
}
132+
133+
default:
134+
return nil, fmt.Errorf("%w: unexpected packet length %d", ErrMalformPkt, len(packet))
135+
}
136+
default:
137+
return nil, fmt.Errorf("%w: expected auth more data packet", ErrMalformPkt)
138+
}
139+
}
140+
141+
// scrambleSHA256Password implements MySQL 8+ password scrambling.
142+
//
143+
// The algorithm is:
144+
// 1. SHA256(password)
145+
// 2. SHA256(SHA256(SHA256(password)))
146+
// 3. XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble))
147+
//
148+
// This provides a way to verify the password without storing it in cleartext.
149+
func scrambleSHA256Password(scramble []byte, password string) []byte {
150+
if len(password) == 0 {
151+
return []byte{}
152+
}
153+
154+
// First hash: SHA256(password)
155+
crypt := sha256.New()
156+
crypt.Write([]byte(password))
157+
message1 := crypt.Sum(nil)
158+
159+
// Second hash: SHA256(SHA256(password))
160+
crypt.Reset()
161+
crypt.Write(message1)
162+
message1Hash := crypt.Sum(nil)
163+
164+
// Third hash: SHA256(SHA256(SHA256(password)), scramble)
165+
crypt.Reset()
166+
crypt.Write(message1Hash)
167+
crypt.Write(scramble)
168+
message2 := crypt.Sum(nil)
169+
170+
// XOR the first hash with the third hash
171+
for i := range message1 {
172+
message1[i] ^= message2[i]
173+
}
174+
175+
return message1
176+
}

‎auth_cleartext.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2+
//
3+
// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved.
4+
//
5+
// This Source Code Form is subject to the terms of the Mozilla Public
6+
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7+
// You can obtain one at http://mozilla.org/MPL/2.0/.
8+
9+
package mysql
10+
11+
// ClearPasswordPlugin implements the mysql_clear_password authentication.
12+
//
13+
// This plugin sends passwords in cleartext and should only be used:
14+
// 1. Over TLS/SSL connections
15+
// 2. Over Unix domain sockets
16+
// 3. When required by authentication methods like PAM
17+
//
18+
// See: http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
19+
//
20+
// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
21+
type ClearPasswordPlugin struct {
22+
SimpleAuth
23+
}
24+
25+
func init() {
26+
RegisterAuthPlugin(&ClearPasswordPlugin{})
27+
}
28+
29+
func (p *ClearPasswordPlugin) GetPluginName() string {
30+
return "mysql_clear_password"
31+
}
32+
33+
// InitAuth implements the cleartext password authentication.
34+
// It will return an error if AllowCleartextPasswords is false.
35+
//
36+
// The cleartext password is sent as a null-terminated string.
37+
// This is required by the server to support external authentication
38+
// systems that need access to the original password.
39+
func (p *ClearPasswordPlugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) {
40+
if !cfg.AllowCleartextPasswords {
41+
return nil, ErrCleartextPassword
42+
}
43+
44+
// Send password as null-terminated string
45+
return append([]byte(cfg.Passwd), 0), nil
46+
}

‎auth_ed25519.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2+
//
3+
// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved.
4+
//
5+
// This Source Code Form is subject to the terms of the Mozilla Public
6+
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7+
// You can obtain one at http://mozilla.org/MPL/2.0/.
8+
9+
package mysql
10+
11+
import (
12+
"crypto/sha512"
13+
"filippo.io/edwards25519"
14+
)
15+
16+
// ClientEd25519Plugin implements the client_ed25519 authentication
17+
type ClientEd25519Plugin struct {
18+
SimpleAuth
19+
}
20+
21+
func init() {
22+
RegisterAuthPlugin(&ClientEd25519Plugin{})
23+
}
24+
25+
func (p *ClientEd25519Plugin) GetPluginName() string {
26+
return "client_ed25519"
27+
}
28+
29+
func (p *ClientEd25519Plugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) {
30+
// Derived from https://github.com/MariaDB/server/blob/d8e6bb00888b1f82c031938f4c8ac5d97f6874c3/plugin/auth_ed25519/ref10/sign.c
31+
// Code style is from https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/crypto/ed25519/ed25519.go;l=207
32+
h := sha512.Sum512([]byte(cfg.Passwd))
33+
34+
s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32])
35+
if err != nil {
36+
return nil, err
37+
}
38+
A := (&edwards25519.Point{}).ScalarBaseMult(s)
39+
40+
mh := sha512.New()
41+
mh.Write(h[32:])
42+
mh.Write(authData)
43+
messageDigest := mh.Sum(nil)
44+
r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
R := (&edwards25519.Point{}).ScalarBaseMult(r)
50+
51+
kh := sha512.New()
52+
kh.Write(R.Bytes())
53+
kh.Write(A.Bytes())
54+
kh.Write(authData)
55+
hramDigest := kh.Sum(nil)
56+
k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest)
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
S := k.MultiplyAdd(k, s, r)
62+
63+
return append(R.Bytes(), S.Bytes()...), nil
64+
}

‎auth_mysql_native.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2+
//
3+
// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved.
4+
//
5+
// This Source Code Form is subject to the terms of the Mozilla Public
6+
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7+
// You can obtain one at http://mozilla.org/MPL/2.0/.
8+
9+
package mysql
10+
11+
import "crypto/sha1"
12+
13+
// NativePasswordPlugin implements the mysql_native_password authentication
14+
type NativePasswordPlugin struct {
15+
SimpleAuth
16+
}
17+
18+
func init() {
19+
RegisterAuthPlugin(&NativePasswordPlugin{})
20+
}
21+
22+
func (p *NativePasswordPlugin) GetPluginName() string {
23+
return "mysql_native_password"
24+
}
25+
26+
func (p *NativePasswordPlugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) {
27+
if !cfg.AllowNativePasswords {
28+
return nil, ErrNativePassword
29+
}
30+
if cfg.Passwd == "" {
31+
return nil, nil
32+
}
33+
return p.scramblePassword(authData[:20], cfg.Passwd), nil
34+
}
35+
36+
// Hash password using 4.1+ method (SHA1)
37+
func (p *NativePasswordPlugin) scramblePassword(scramble []byte, password string) []byte {
38+
if len(password) == 0 {
39+
return nil
40+
}
41+
42+
// stage1Hash = SHA1(password)
43+
crypt := sha1.New()
44+
crypt.Write([]byte(password))
45+
stage1 := crypt.Sum(nil)
46+
47+
// scrambleHash = SHA1(scramble + SHA1(stage1Hash))
48+
// inner Hash
49+
crypt.Reset()
50+
crypt.Write(stage1)
51+
hash := crypt.Sum(nil)
52+
53+
// outer Hash
54+
crypt.Reset()
55+
crypt.Write(scramble)
56+
crypt.Write(hash)
57+
scramble = crypt.Sum(nil)
58+
59+
// token = scrambleHash XOR stage1Hash
60+
for i := range scramble {
61+
scramble[i] ^= stage1[i]
62+
}
63+
return scramble
64+
}

‎auth_old_password.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2+
//
3+
// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved.
4+
//
5+
// This Source Code Form is subject to the terms of the Mozilla Public
6+
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7+
// You can obtain one at http://mozilla.org/MPL/2.0/.
8+
9+
package mysql
10+
11+
// OldPasswordPlugin implements the mysql_old_password authentication
12+
type OldPasswordPlugin struct{ SimpleAuth }
13+
14+
func init() {
15+
RegisterAuthPlugin(&OldPasswordPlugin{})
16+
}
17+
18+
func (p *OldPasswordPlugin) GetPluginName() string {
19+
return "mysql_old_password"
20+
}
21+
22+
func (p *OldPasswordPlugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) {
23+
if !cfg.AllowOldPasswords {
24+
return nil, ErrOldPassword
25+
}
26+
if cfg.Passwd == "" {
27+
return nil, nil
28+
}
29+
// Note: there are edge cases where this should work but doesn't;
30+
// this is currently "wontfix":
31+
// https://github.com/go-sql-driver/mysql/issues/184
32+
return append(p.scrambleOldPassword(authData[:8], cfg.Passwd), 0), nil
33+
}
34+
35+
// Hash password using insecure pre 4.1 method
36+
func (p *OldPasswordPlugin) scrambleOldPassword(scramble []byte, password string) []byte {
37+
scramble = scramble[:8]
38+
39+
hashPw := pwHash([]byte(password))
40+
hashSc := pwHash(scramble)
41+
42+
r := newMyRnd(hashPw[0]^hashSc[0], hashPw[1]^hashSc[1])
43+
44+
var out [8]byte
45+
for i := range out {
46+
out[i] = r.NextByte() + 64
47+
}
48+
49+
mask := r.NextByte()
50+
for i := range out {
51+
out[i] ^= mask
52+
}
53+
54+
return out[:]
55+
}
56+
57+
// Hash password using pre 4.1 (old password) method
58+
// https://github.com/atcurtis/mariadb/blob/master/mysys/my_rnd.c
59+
type myRnd struct {
60+
seed1, seed2 uint32
61+
}
62+
63+
// Tested to be equivalent to MariaDB's floating point variant
64+
// http://play.golang.org/p/QHvhd4qved
65+
// http://play.golang.org/p/RG0q4ElWDx
66+
func (r *myRnd) NextByte() byte {
67+
r.seed1 = (r.seed1*3 + r.seed2) % myRndMaxVal
68+
r.seed2 = (r.seed1 + r.seed2 + 33) % myRndMaxVal
69+
70+
return byte(uint64(r.seed1) * 31 / myRndMaxVal)
71+
}
72+
73+
const myRndMaxVal = 0x3FFFFFFF
74+
75+
// Pseudo random number generator
76+
func newMyRnd(seed1, seed2 uint32) *myRnd {
77+
return &myRnd{
78+
seed1: seed1 % myRndMaxVal,
79+
seed2: seed2 % myRndMaxVal,
80+
}
81+
}
82+
83+
// Generate binary hash from byte string using insecure pre 4.1 method
84+
func pwHash(password []byte) (result [2]uint32) {
85+
var add uint32 = 7
86+
var tmp uint32
87+
88+
result[0] = 1345345333
89+
result[1] = 0x12345671
90+
91+
for _, c := range password {
92+
// skip spaces and tabs in password
93+
if c == ' ' || c == '\t' {
94+
continue
95+
}
96+
97+
tmp = uint32(c)
98+
result[0] ^= (((result[0] & 63) + add) * tmp) + (result[0] << 8)
99+
result[1] += (result[1] << 8) ^ result[0]
100+
add += tmp
101+
}
102+
103+
// Remove sign bit (1<<31)-1)
104+
result[0] &= 0x7FFFFFFF
105+
result[1] &= 0x7FFFFFFF
106+
107+
return
108+
}

‎auth_plugin.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2+
//
3+
// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved.
4+
//
5+
// This Source Code Form is subject to the terms of the Mozilla Public
6+
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7+
// You can obtain one at http://mozilla.org/MPL/2.0/.
8+
9+
package mysql
10+
11+
// AuthPlugin represents an authentication plugin for MySQL/MariaDB
12+
type AuthPlugin interface {
13+
// GetPluginName returns the name of the authentication plugin
14+
GetPluginName() string
15+
16+
// InitAuth initializes the authentication process and returns the initial response
17+
// authData is the challenge data from the server
18+
// password is the password for authentication
19+
InitAuth(authData []byte, cfg *Config) ([]byte, error)
20+
21+
// ContinuationAuth processes the authentication response from the server
22+
// packet is the data from the server's auth response
23+
// authData is the initial auth data from the server
24+
// conn is the MySQL connection (for performing additional interactions if needed)
25+
ContinuationAuth(packet []byte, authData []byte, conn *mysqlConn) ([]byte, error)
26+
}
27+
28+
type SimpleAuth struct {
29+
AuthPlugin
30+
}
31+
32+
func (s SimpleAuth) ContinuationAuth(packet []byte, authData []byte, conn *mysqlConn) ([]byte, error) {
33+
return packet, nil
34+
}
35+
36+
// PluginRegistry is a registry of available authentication plugins
37+
type PluginRegistry struct {
38+
plugins map[string]AuthPlugin
39+
}
40+
41+
// NewPluginRegistry creates a new plugin registry
42+
func NewPluginRegistry() *PluginRegistry {
43+
registry := &PluginRegistry{
44+
plugins: make(map[string]AuthPlugin),
45+
}
46+
return registry
47+
}
48+
49+
// Register adds a plugin to the registry
50+
func (r *PluginRegistry) Register(plugin AuthPlugin) {
51+
r.plugins[plugin.GetPluginName()] = plugin
52+
}
53+
54+
// GetPlugin returns the plugin for the given name
55+
func (r *PluginRegistry) GetPlugin(name string) (AuthPlugin, bool) {
56+
plugin, ok := r.plugins[name]
57+
return plugin, ok
58+
}
59+
60+
// RegisterAuthPlugin registers the plugin to the global plugin registry
61+
func RegisterAuthPlugin(plugin AuthPlugin) {
62+
globalPluginRegistry.Register(plugin)
63+
}

‎auth_sha256.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2+
//
3+
// Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved.
4+
//
5+
// This Source Code Form is subject to the terms of the Mozilla Public
6+
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7+
// You can obtain one at http://mozilla.org/MPL/2.0/.
8+
9+
package mysql
10+
11+
import (
12+
"crypto/rand"
13+
"crypto/rsa"
14+
"crypto/sha1"
15+
"crypto/x509"
16+
"encoding/pem"
17+
"fmt"
18+
)
19+
20+
// Sha256PasswordPlugin implements the sha256_password authentication
21+
// This plugin provides secure password-based authentication using SHA256 and RSA encryption.
22+
type Sha256PasswordPlugin struct{ AuthPlugin }
23+
24+
func init() {
25+
RegisterAuthPlugin(&Sha256PasswordPlugin{})
26+
}
27+
28+
func (p *Sha256PasswordPlugin) GetPluginName() string {
29+
return "sha256_password"
30+
}
31+
32+
// InitAuth initializes the authentication process.
33+
//
34+
// The function follows these rules:
35+
// 1. If no password is configured, sends a single byte indicating empty password
36+
// 2. If TLS is enabled, sends the password in cleartext
37+
// 3. If a public key is available, encrypts the password and sends it
38+
// 4. Otherwise, requests the server's public key
39+
func (p *Sha256PasswordPlugin) InitAuth(authData []byte, cfg *Config) ([]byte, error) {
40+
if len(cfg.Passwd) == 0 {
41+
return []byte{0}, nil
42+
}
43+
44+
// Unlike caching_sha2_password, sha256_password does not accept
45+
// cleartext password on unix transport.
46+
if cfg.TLS != nil {
47+
// Write cleartext auth packet
48+
return append([]byte(cfg.Passwd), 0), nil
49+
}
50+
51+
if cfg.pubKey == nil {
52+
// Request public key from server
53+
return []byte{1}, nil
54+
}
55+
56+
// Encrypt password using the public key
57+
enc, err := encryptPassword(cfg.Passwd, authData, cfg.pubKey)
58+
if err != nil {
59+
return nil, fmt.Errorf("failed to encrypt password: %w", err)
60+
}
61+
return enc, nil
62+
}
63+
64+
// ContinuationAuth processes the server's response to our authentication attempt.
65+
//
66+
// The server can respond in three ways:
67+
// 1. OK packet - Authentication successful
68+
// 2. Error packet - Authentication failed
69+
// 3. More data packet - Contains the server's public key for password encryption
70+
func (p *Sha256PasswordPlugin) ContinuationAuth(packet []byte, authData []byte, mc *mysqlConn) ([]byte, error) {
71+
72+
switch packet[0] {
73+
case iOK, iERR, iEOF:
74+
return packet, nil
75+
76+
case iAuthMoreData:
77+
// Parse public key from PEM format
78+
block, rest := pem.Decode(packet[1:])
79+
if block == nil {
80+
return nil, fmt.Errorf("invalid PEM data in auth response: %q", rest)
81+
}
82+
83+
// Parse the public key
84+
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
85+
if err != nil {
86+
return nil, fmt.Errorf("failed to parse public key: %w", err)
87+
}
88+
89+
// Send encrypted password
90+
enc, err := encryptPassword(mc.cfg.Passwd, authData, pub.(*rsa.PublicKey))
91+
if err != nil {
92+
return nil, fmt.Errorf("failed to encrypt password with server key: %w", err)
93+
}
94+
95+
// Send the encrypted password
96+
if err = mc.writeAuthSwitchPacket(enc); err != nil {
97+
return nil, fmt.Errorf("failed to send encrypted password: %w", err)
98+
}
99+
100+
return mc.readPacket()
101+
102+
default:
103+
return nil, fmt.Errorf("%w: unexpected packet type %d", ErrMalformPkt, packet[0])
104+
}
105+
}
106+
107+
// encryptPassword encrypts the password using RSA-OAEP with SHA1 hash.
108+
//
109+
// The process:
110+
// 1. XORs the password with the auth seed to prevent replay attacks
111+
// 2. Encrypts the XORed password using RSA-OAEP with SHA1
112+
//
113+
// The encryption uses OAEP padding which is more secure than PKCS#1 v1.5 padding.
114+
func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, error) {
115+
if pub == nil {
116+
return nil, fmt.Errorf("public key is nil")
117+
}
118+
119+
// Create the plaintext by XORing password with seed
120+
plain := make([]byte, len(password)+1)
121+
copy(plain, password)
122+
for i := range plain {
123+
j := i % len(seed)
124+
plain[i] ^= seed[j]
125+
}
126+
127+
// Encrypt using RSA-OAEP with SHA1
128+
sha1Hash := sha1.New()
129+
return rsa.EncryptOAEP(sha1Hash, rand.Reader, pub, plain, nil)
130+
}

‎auth_test.go

Lines changed: 159 additions & 127 deletions
Large diffs are not rendered by default.

‎connector.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import (
1818
"strings"
1919
)
2020

21+
const (
22+
authMaximumSwitch = 5
23+
)
24+
2125
type connector struct {
2226
cfg *Config // immutable private copy.
2327
encodedAttributes string // Encoded connection attributes.
@@ -140,26 +144,24 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
140144
if plugin == "" {
141145
plugin = defaultAuthPlugin
142146
}
147+
authPlugin, exists := globalPluginRegistry.GetPlugin(plugin)
148+
if !exists {
149+
return nil, fmt.Errorf("this authentication plugin '%s' is not supported", plugin)
150+
}
143151

144152
// Send Client Authentication Packet
145-
authResp, err := mc.auth(authData, plugin)
153+
authResp, err := authPlugin.InitAuth(authData, mc.cfg)
146154
if err != nil {
147-
// try the default auth plugin, if using the requested plugin failed
148-
c.cfg.Logger.Print("could not use requested auth plugin '"+plugin+"': ", err.Error())
149-
plugin = defaultAuthPlugin
150-
authResp, err = mc.auth(authData, plugin)
151-
if err != nil {
152-
mc.cleanup()
153-
return nil, err
154-
}
155+
mc.cleanup()
156+
return nil, err
155157
}
156158
if err = mc.writeHandshakeResponsePacket(authResp, plugin); err != nil {
157159
mc.cleanup()
158160
return nil, err
159161
}
160162

161163
// Handle response to auth packet, switch methods if possible
162-
if err = mc.handleAuthResult(authData, plugin); err != nil {
164+
if err = mc.handleAuthResult(authMaximumSwitch, authData, authPlugin); err != nil {
163165
// Authentication failed and MySQL has already closed the connection
164166
// (https://dev.mysql.com/doc/internals/en/authentication-fails.html).
165167
// Do not send COM_QUIT, just cleanup and return the error.

‎const.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,3 @@ const (
184184
statusInTransReadonly
185185
statusSessionStateChanged
186186
)
187-
188-
const (
189-
cachingSha2PasswordRequestPublicKey = 2
190-
cachingSha2PasswordFastAuthSuccess = 3
191-
cachingSha2PasswordPerformFullAuthentication = 4
192-
)

‎driver.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ type DialFunc func(addr string) (net.Conn, error)
3939
type DialContextFunc func(ctx context.Context, addr string) (net.Conn, error)
4040

4141
var (
42-
dialsLock sync.RWMutex
43-
dials map[string]DialContextFunc
42+
dialsLock sync.RWMutex
43+
dials map[string]DialContextFunc
44+
globalPluginRegistry = NewPluginRegistry()
4445
)
4546

4647
// RegisterDialContext registers a custom dial function. It can then be used by the

‎packets.go

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -482,44 +482,6 @@ func (mc *mysqlConn) writeCommandPacketUint32(command byte, arg uint32) error {
482482
* Result Packets *
483483
******************************************************************************/
484484

485-
func (mc *mysqlConn) readAuthResult() ([]byte, string, error) {
486-
data, err := mc.readPacket()
487-
if err != nil {
488-
return nil, "", err
489-
}
490-
491-
// packet indicator
492-
switch data[0] {
493-
494-
case iOK:
495-
// resultUnchanged, since auth happens before any queries or
496-
// commands have been executed.
497-
return nil, "", mc.resultUnchanged().handleOkPacket(data)
498-
499-
case iAuthMoreData:
500-
return data[1:], "", err
501-
502-
case iEOF:
503-
if len(data) == 1 {
504-
// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::OldAuthSwitchRequest
505-
return nil, "mysql_old_password", nil
506-
}
507-
pluginEndIndex := bytes.IndexByte(data, 0x00)
508-
if pluginEndIndex < 0 {
509-
return nil, "", ErrMalformPkt
510-
}
511-
plugin := string(data[1:pluginEndIndex])
512-
authData := data[pluginEndIndex+1:]
513-
if len(authData) > 0 && authData[len(authData)-1] == 0 {
514-
authData = authData[:len(authData)-1]
515-
}
516-
return authData, plugin, nil
517-
518-
default: // Error otherwise
519-
return nil, "", mc.handleErrorPacket(data)
520-
}
521-
}
522-
523485
// Returns error if Packet is not a 'Result OK'-Packet
524486
func (mc *okHandler) readResultOK() error {
525487
data, err := mc.conn().readPacket()

0 commit comments

Comments
 (0)
Please sign in to comment.