From deb741e50e9a59ead3cb9c826b56e72fce03596c Mon Sep 17 00:00:00 2001 From: Dan Schultzer <1254724+danschultzer@users.noreply.github.com> Date: Tue, 13 Feb 2024 03:28:37 -0800 Subject: [PATCH] Enable cleartext plugin (#176) --- lib/myxql.ex | 3 + lib/myxql/client.ex | 6 +- lib/myxql/protocol/auth.ex | 3 + lib/myxql/protocol/types.ex | 2 + test/myxql/client_test.exs | 146 ++++++++++++++++++++++++++++++++++++ test/myxql_test.exs | 8 +- 6 files changed, 164 insertions(+), 4 deletions(-) diff --git a/lib/myxql.ex b/lib/myxql.ex index 07f7048..c636d71 100644 --- a/lib/myxql.ex +++ b/lib/myxql.ex @@ -23,6 +23,7 @@ defmodule MyXQL do | {:ping_timeout, timeout()} | {:prepare, :force_named | :named | :unnamed} | {:disconnect_on_error_codes, [atom()]} + | {:enable_cleartext_plugin, boolean()} | DBConnection.start_option() @type option() :: DBConnection.option() @@ -100,6 +101,8 @@ defmodule MyXQL do will disconnect the connection. See "Disconnecting on Errors" section below for more information. + * `:enable_cleartext_plugin` - Set to `true` to send password as cleartext (default: `false`) + The given options are passed down to DBConnection, some of the most commonly used ones are documented below: diff --git a/lib/myxql/client.ex b/lib/myxql/client.ex index e76c13c..afd5982 100644 --- a/lib/myxql/client.ex +++ b/lib/myxql/client.ex @@ -25,7 +25,8 @@ defmodule MyXQL.Client do :socket_options, :max_packet_size, :charset, - :collation + :collation, + :enable_cleartext_plugin ] def new(opts) do @@ -45,7 +46,8 @@ defmodule MyXQL.Client do socket_options: Keyword.merge([mode: :binary, packet: :raw, active: false], opts[:socket_options] || []), charset: Keyword.get(opts, :charset), - collation: Keyword.get(opts, :collation) + collation: Keyword.get(opts, :collation), + enable_cleartext_plugin: Keyword.get(opts, :enable_cleartext_plugin, false) } end diff --git a/lib/myxql/protocol/auth.ex b/lib/myxql/protocol/auth.ex index 4fb4f8e..6168a63 100644 --- a/lib/myxql/protocol/auth.ex +++ b/lib/myxql/protocol/auth.ex @@ -33,6 +33,9 @@ defmodule MyXQL.Protocol.Auth do config.password == nil -> "" + auth_plugin_name == "mysql_clear_password" and config.enable_cleartext_plugin -> + config.password <> <<0>> + auth_plugin_name == "mysql_native_password" -> mysql_native_password(config.password, initial_auth_plugin_data) diff --git a/lib/myxql/protocol/types.ex b/lib/myxql/protocol/types.ex index f5034b2..572331d 100644 --- a/lib/myxql/protocol/types.ex +++ b/lib/myxql/protocol/types.ex @@ -66,6 +66,8 @@ defmodule MyXQL.Protocol.Types do string end + def take_string_nul(""), do: {nil, ""} + def take_string_nul(binary) do [string, rest] = :binary.split(binary, <<0>>) {string, rest} diff --git a/test/myxql/client_test.exs b/test/myxql/client_test.exs index 22af36b..9b408de 100644 --- a/test/myxql/client_test.exs +++ b/test/myxql/client_test.exs @@ -78,6 +78,23 @@ defmodule MyXQL.ClientTest do Client.com_quit(client) end + # mysql_clear_password + + test "mysql_clear_password" do + opts = [username: "mysql_clear", password: "secret", enable_cleartext_plugin: true] ++ @opts + %{port: port} = start_cleartext_fake_server() + opts = Keyword.put(opts, :port, port) + assert {:ok, client} = Client.connect(opts) + Client.com_quit(client) + end + + test "mysql_clear_password (bad password)" do + opts = [username: "mysql_clear", password: "bad", enable_cleartext_plugin: true] ++ @opts + %{port: port} = start_cleartext_fake_server() + opts = Keyword.put(opts, :port, port) + {:error, err_packet(message: "Access denied" <> _)} = Client.connect(opts) + end + # sha256_password @tag sha256_password: true, public_key_exchange: true @@ -447,4 +464,133 @@ defmodule MyXQL.ClientTest do %{pid: pid, port: port} end + + defp start_cleartext_fake_server() do + start_fake_server(fn %{accept_socket: sock} -> + # The initial handshake which the mysql server always sends. Usually, like in this + # case, it contains scramble data with `mysql_native_password`. + initial_handshake = + [ + # packet size + <<74, 0, 0>>, + # packet sequence + 0, + # protocol version, always 0x10 + 10, + # mysql version + ["8.0.35", 0], + # thread id + <<127, 24, 4, 0>>, + # auth_plugin_data_1 + <<93, 42, 61, 27, 60, 38, 85, 12>>, + # filler + 0, + # capability flags 1 + <<255, 255>>, + # charset + <<255>>, + # status flags + <<2, 0>>, + # capability flags 2 + <<255, 223>>, + # auth_plugin_data_len + 21, + # reserved + <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, + <<39, 48, 10, 117, 54, 65, 74, 37, 125, 121, 93, 6, 0>>, + # auth_plugin_name + ["mysql_native_password", 0] + ] + + # Client will use the scramble to attempt authentication with `mysql_native_password` + # (or whichever default auth plugin is used). This will fail, but must be done before + # we can continue with `mysql_clear_password`. + client_auth_response = + IO.iodata_to_binary([ + # packet header + <<98, 0, 0>>, + # packet sequence + 1, + # capability flags + <<10, 162, 11, 0>>, + # max packet size + <<255, 255, 255, 0>>, + # charset + 45, + # filler + <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, + # username + ["mysql_clear", 0], + # auth response + [ + 20, + <<254, 122, 75, 71, 45, 200, 185, 238, 55, 229, 170, 5, 207, 204, 65, 246, 243, 144, + 91, 183>> + ], + # database + ["myxql_test", 0], + # auth plugin name + ["mysql_native_password", 0] + ]) + + # The server now requests `mysql_clear_password`. Notably there's no scramable data here. + switch_auth_response = [ + # packet size + <<22, 0, 0>>, + # packet sequence + 2, + 254, + ["mysql_clear_password", 0] + ] + + # Client sends the cleartext password + client_switch_auth_response = + IO.iodata_to_binary([ + # packet size + <<7, 0, 0>>, + # packet sequence + 3, + # password + ["secret", 0] + ]) + + ok_response = [ + # packet size + <<7, 0, 0>>, + # packet sequence + 4, + # ok packet + <<0, 0, 0, 2, 0, 0, 0>> + ] + + client_quit = <<1, 0, 0, 0, 1>> + + auth_response_invalid = [ + # packet size + <<83, 0, 0>>, + # packet sequence + 1, + # err packet header + 255, + # error code + <<21, 4>>, + # error message + "#28000Access denied for user 'default_auth'@'192.168.65.1' (using password: YES)" + ] + + :gen_tcp.send(sock, initial_handshake) + + case :gen_tcp.recv(sock, 0) do + {:ok, ^client_auth_response} -> + :ok = :gen_tcp.send(sock, switch_auth_response) + {:ok, ^client_switch_auth_response} = :gen_tcp.recv(sock, 0) + :ok = :gen_tcp.send(sock, ok_response) + {:ok, ^client_quit} = :gen_tcp.recv(sock, 0) + :ok = :gen_tcp.send(sock, ok_response) + + {:ok, _other} -> + :ok = :gen_tcp.send(sock, auth_response_invalid) + end + end) + end end diff --git a/test/myxql_test.exs b/test/myxql_test.exs index 30885f7..39f7ce9 100644 --- a/test/myxql_test.exs +++ b/test/myxql_test.exs @@ -156,7 +156,9 @@ defmodule MyXQLTest do test "#{@protocol}: query with multiple rows", c do %MyXQL.Result{num_rows: 2} = - MyXQL.query!(c.conn, "INSERT INTO integers VALUES (10), (20)", [], query_type: @protocol) + MyXQL.query!(c.conn, "INSERT INTO integers VALUES (10), (20)", [], + query_type: @protocol + ) assert {:ok, %MyXQL.Result{columns: ["x"], rows: [[10], [20]]}} = MyXQL.query(c.conn, "SELECT * FROM integers") @@ -168,7 +170,9 @@ defmodule MyXQLTest do values = Enum.map_join(1..num, ", ", &"(#{&1})") result = - MyXQL.query!(c.conn, "INSERT INTO integers VALUES " <> values, [], query_type: @protocol) + MyXQL.query!(c.conn, "INSERT INTO integers VALUES " <> values, [], + query_type: @protocol + ) assert result.num_rows == num