Skip to content

Commit

Permalink
Enable cleartext plugin (#176)
Browse files Browse the repository at this point in the history
  • Loading branch information
danschultzer authored Feb 13, 2024
1 parent edd6855 commit deb741e
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 4 deletions.
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`)
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, ""}

def take_string_nul(binary) do
[string, rest] = :binary.split(binary, <<0>>)
{string, rest}
Expand Down
146 changes: 146 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,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
8 changes: 6 additions & 2 deletions test/myxql_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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

Expand Down

0 comments on commit deb741e

Please sign in to comment.