Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable cleartext plugin #176

Merged
merged 4 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/myxql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think matching mysql CLI is a great starting point but curious if any other connectors have something like auth_plugin: mysql_native_password | caching_sha2_password | .... I think that'd be preferred as we'd reuse the same option when we support more methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleartext password is distinct in that it has no server side library: https://dev.mysql.com/doc/refman/8.0/en/cleartext-pluggable-authentication.html

And especially note this:

To make inadvertent use of the mysql_clear_password plugin less likely, MySQL clients must explicitly enable it.

This has been an issue with other mysql connectors: sidorares/node-mysql2#1617

Ruby mysql2 sets the env flag for the mysql client it communicates with (but uses the same enable_cleartext_plugin flag in config): brianmario/mysql2#845

The closest thing there is to decide what auth plugin to use is hinting: https://dev.mysql.com/doc/refman/8.3/en/mysql-command-options.html#option_mysql_default-auth

As I understand it, the way the auth works with cleartext password from the mysql client is that it will always go through the auth switch, and it's after the switch that it's possible to succesfully authenticate. In the case of RDS IAM it will first ask for the mysql_native_password and expects the client to send a response for this format (I tried just sending the token itself, but the server wanted a sha1 size response). After this step fails it asks for mysql_clear_password which is what we actually wanted to send.

So IMO it doesn't really make sense to treat mysql_clear_password the same way as other auth plugin. It's a special case. We could force it on our side by setting something like :auth_plugin, which arguably is more explicit, but this is not how the mysql client works, and I haven't seen other libraries do this.


The given options are passed down to DBConnection, some of the most commonly used ones are
documented below:

Expand Down
6 changes: 4 additions & 2 deletions lib/myxql/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ defmodule MyXQL.Client do
:socket_options,
:max_packet_size,
:charset,
:collation
:collation,
:enable_cleartext_plugin
]

def new(opts) do
Expand All @@ -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

Expand Down
3 changes: 3 additions & 0 deletions lib/myxql/protocol/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions lib/myxql/protocol/types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ defmodule MyXQL.Protocol.Types do
string
end

def take_string_nul(""), do: {nil, ""}
danschultzer marked this conversation as resolved.
Show resolved Hide resolved

def take_string_nul(binary) do
[string, rest] = :binary.split(binary, <<0>>)
{string, rest}
Expand Down
70 changes: 70 additions & 0 deletions test/myxql/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -447,4 +464,57 @@ defmodule MyXQL.ClientTest do

%{pid: pid, port: port}
end


defp start_cleartext_fake_server() do
start_fake_server(fn %{accept_socket: sock} ->
initial_handshake =
<<74, 0, 0, 0, 10, 56, 46, 48, 46, 51, 53, 0, 127, 24, 4, 0, 93, 42, 61, 27, 60,
38, 85, 12, 0, 255, 255, 255, 2, 0, 255, 223, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 39, 48, 10, 117, 54, 65, 74, 37, 125, 121, 93, 6, 0, 109, 121, 115, 113,
108, 95, 110, 97, 116, 105, 118, 101, 95, 112, 97, 115, 115, 119, 111, 114,
100, 0>>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is tough to understand and maintain. Could you break it down to along the lines of:

[
  <<74, 0, 0, 0>>,
  [
    # protocol version, always 0x10
    10,
    # server version
    <<"8.0.35", 0>>,
    # ...
  ]
]

I'm really curious to see where exactly the scramble was supposed to be and is empty.

Copy link
Contributor Author

@danschultzer danschultzer Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but just FYI this part doesn't matter. It's just priming the connection with a scramble, that we are going to make (knowingly) an unsuccesful authentication with. When that fails we continue with the cleartext password. This came from painfully figuring out why I couldn't just send the cleartext password with the mysql_clear_password attached to it right after the handshake. It can only happen after the auth switch from the server.

Copy link
Contributor Author

@danschultzer danschultzer Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to just add comments to each packet rather than expanding the packets. There's not really much to expand on within the packets, but this will definitely help others understand how this works.


client_auth_response =
<<98, 0, 0, 1, 10, 162, 11, 0, 255, 255, 255, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 109, 121, 115, 113, 108, 95, 99,
108, 101, 97, 114, 0, 20, 254, 122, 75, 71, 45, 200, 185, 238, 55, 229, 170,
5, 207, 204, 65, 246, 243, 144, 91, 183, 109, 121, 120, 113, 108, 95, 116,
101, 115, 116, 0, 109, 121, 115, 113, 108, 95, 110, 97, 116, 105, 118, 101,
95, 112, 97, 115, 115, 119, 111, 114, 100, 0>>

switch_auth_response =
<<22, 0, 0, 2, 254, 109, 121, 115, 113, 108, 95, 99, 108, 101, 97, 114, 95, 112,
97, 115, 115, 119, 111, 114, 100, 0>>

client_switch_auth_response =
<<7, 0, 0, 3, 115, 101, 99, 114, 101, 116, 0>>

ok_response =
<<7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0>>

client_quit = <<1, 0, 0, 0, 1>>

auth_response_invalid =
<<83, 0, 0, 1, 255, 21, 4, 35, 50, 56, 48, 48, 48, 65, 99, 99, 101, 115, 115,
32, 100, 101, 110, 105, 101, 100, 32, 102, 111, 114, 32, 117, 115, 101, 114,
32, 39, 100, 101, 102, 97, 117, 108, 116, 95, 97, 117, 116, 104, 39, 64, 39,
49, 57, 50, 46, 49, 54, 56, 46, 54, 53, 46, 49, 39, 32, 40, 117, 115, 105,
110, 103, 32, 112, 97, 115, 115, 119, 111, 114, 100, 58, 32, 89, 69, 83, 41>>

: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