From 62393613baaf866c56b85a89c37db1c11cf9a332 Mon Sep 17 00:00:00 2001 From: Tom Petr Date: Tue, 1 Jan 2019 17:15:22 -0500 Subject: [PATCH 1/2] implement dialog auth plugin --- AUTHORS | 1 + README.md | 2 +- auth.go | 13 +++++++ auth_test.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++ dsn.go | 2 + 5 files changed, 121 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 5ce4f7eca..a4932f7e7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -76,6 +76,7 @@ Stanley Gunawan Steven Hartland Thomas Wodarek Tom Jenkinson +Tom Petr Xiangyu Hu Xiaobing Jiang Xiuming Chen diff --git a/README.md b/README.md index 341d9194c..1250fc81a 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Valid Values: true, false Default: false ``` -`allowCleartextPasswords=true` allows using the [cleartext client side plugin](http://dev.mysql.com/doc/en/cleartext-authentication-plugin.html) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network. +`allowCleartextPasswords=true` allows using the [cleartext client side plugin](http://dev.mysql.com/doc/en/cleartext-authentication-plugin.html) or [dialog plugin](https://mariadb.com/kb/en/library/development-pluggable-authentication/#dialog-client-plugin) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network. ##### `allowNativePasswords` diff --git a/auth.go b/auth.go index fec7040d4..9995d01c1 100644 --- a/auth.go +++ b/auth.go @@ -289,6 +289,19 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) { enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey) return enc, err + case "dialog": + if !mc.cfg.AllowCleartextPasswords { + return nil, ErrCleartextPassword + } + if mc.cfg.DialogFunc != nil { + dialogPasswd, err := mc.cfg.DialogFunc(authData[0] >> 1, string(authData[1:])) + if err != nil { + return nil, err + } + return append([]byte(dialogPasswd), 0), nil + } + return append([]byte(mc.cfg.Passwd), 0), nil + default: errLog.Print("unknown auth plugin:", plugin) return nil, ErrUnknownPlugin diff --git a/auth_test.go b/auth_test.go index 1920ef39f..dce90d1a6 100644 --- a/auth_test.go +++ b/auth_test.go @@ -428,6 +428,110 @@ func TestAuthFastCleartextPasswordEmpty(t *testing.T) { } } +func TestAuthFastDialogNotAllowed(t *testing.T) { + _, mc := newRWMockConn(1) + mc.cfg.User = "root" + mc.cfg.Passwd = "secret" + + authData := []byte{70, 114, 92, 94, 1, 38, 11, 116, 63, 114, 23, 101, 126, + 103, 26, 95, 81, 17, 24, 21} + plugin := "dialog" + + // Send Client Authentication Packet + _, err := mc.auth(authData, plugin) + if err != ErrCleartextPassword { + t.Errorf("expected ErrCleartextPassword, got %v", err) + } +} + +func TestAuthFastDialogNoImpl(t *testing.T) { + conn, mc := newRWMockConn(1) + mc.cfg.User = "root" + mc.cfg.Passwd = "secret" + mc.cfg.AllowCleartextPasswords = true + + authData := []byte{70, 114, 92, 94, 1, 38, 11, 116, 63, 114, 23, 101, 126, + 103, 26, 95, 81, 17, 24, 21} + plugin := "dialog" + + // Send Client Authentication Packet + authResp, err := mc.auth(authData, plugin) + if err != nil { + t.Fatal(err) + } + err = mc.writeHandshakeResponsePacket(authResp, plugin) + if err != nil { + 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{115, 101, 99, 114, 101, 116, 0} + if writtenAuthRespLen != 7 || !bytes.Equal(writtenAuthResp, expectedAuthResp) { + t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp) + } + conn.written = nil + + // auth response + conn.data = []byte{ + 7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, // OK + } + conn.maxReads = 1 + + // Handle response to auth packet + if err := mc.handleAuthResult(authData, plugin); err != nil { + t.Errorf("got error: %v", err) + } +} + +func TestAuthFastDialog(t *testing.T) { + conn, mc := newRWMockConn(1) + mc.cfg.User = "root" + mc.cfg.AllowCleartextPasswords = true + mc.cfg.DialogFunc = func(t byte, prompt string) (string, error) { + return "secret", nil + } + + authData := []byte{70, 114, 92, 94, 1, 38, 11, 116, 63, 114, 23, 101, 126, + 103, 26, 95, 81, 17, 24, 21} + plugin := "dialog" + + // Send Client Authentication Packet + authResp, err := mc.auth(authData, plugin) + if err != nil { + t.Fatal(err) + } + err = mc.writeHandshakeResponsePacket(authResp, plugin) + if err != nil { + 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{115, 101, 99, 114, 101, 116, 0} + if writtenAuthRespLen != 7 || !bytes.Equal(writtenAuthResp, expectedAuthResp) { + t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp) + } + conn.written = nil + + // auth response + conn.data = []byte{ + 7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, // OK + } + conn.maxReads = 1 + + // Handle response to auth packet + if err := mc.handleAuthResult(authData, plugin); err != nil { + t.Errorf("got error: %v", err) + } +} + func TestAuthFastNativePasswordNotAllowed(t *testing.T) { _, mc := newRWMockConn(1) mc.cfg.User = "root" diff --git a/dsn.go b/dsn.go index b9134722e..3d9b12c56 100644 --- a/dsn.go +++ b/dsn.go @@ -60,6 +60,8 @@ type Config struct { MultiStatements bool // Allow multiple statements in one query ParseTime bool // Parse time values to time.Time RejectReadOnly bool // Reject read-only connections + + DialogFunc func(byte, string) (string, error) // Optional dialog auth implementation } // NewConfig creates a new Config and sets default values. From 8cd20d12b6c3c12487e5a344795a3c56114ee116 Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 15 Jan 2019 09:43:27 -0500 Subject: [PATCH 2/2] gofmt --- auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth.go b/auth.go index 9995d01c1..61cd77307 100644 --- a/auth.go +++ b/auth.go @@ -294,7 +294,7 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) { return nil, ErrCleartextPassword } if mc.cfg.DialogFunc != nil { - dialogPasswd, err := mc.cfg.DialogFunc(authData[0] >> 1, string(authData[1:])) + dialogPasswd, err := mc.cfg.DialogFunc(authData[0]>>1, string(authData[1:])) if err != nil { return nil, err }