Skip to content

Commit

Permalink
Implement caching_sha2_password auth
Browse files Browse the repository at this point in the history
Note that this only implements it if the server requests it by default.
We don't have any tests for a server that defaults to native, and then a
user that auth switches to caching_sha2 (I'm not sure Trilogy supports
that yet either). I'll worry about that later.
  • Loading branch information
composerinteralia committed Apr 23, 2024
1 parent ae2d212 commit 29f158e
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 9 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ jobs:
- uses: actions/checkout@v4
- name: Set up MySQL users
run: |
mysql --user=root --host=127.0.0.1 < ${{ github.workspace }}/test/mysql/native_user.sql
mysql --user=root --host=127.0.0.1 --execute 'CREATE DATABASE test'
mysql --user=root --host=127.0.0.1 < ${{ github.workspace }}/test/mysql/native_user.sql
[[ "$MYSQL_VERSION" == "8.0" ]] && mysql --user=root --host=127.0.0.1 < ${{ github.workspace }}/test/mysql/caching_sha2_user.sql
- name: Set up Ruby
uses: ruby/setup-ruby@v1
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ https://github.com/trilogy-libraries/trilogy
## TODO

- Multi-result
- caching_sha2_password auth
- SSL options
- Charset option and more encodings
- #connected_host, #connection_options, #query_with_flags, #set_server_option, #server_info, #in_transaction, #gtid
Expand Down
63 changes: 59 additions & 4 deletions lib/nocturne/protocol/handshake.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ def engage
)
elsif packet.err?
raise Protocol.error(packet, ConnectionError)
elsif packet.int8 == 0xFE # auth switch
elsif packet.tag == 0xFE # auth switch
packet.skip(1)
plugin = packet.nulstr
data = packet.eof_str
auth_switch(plugin, data)
elsif packet.tag == 1 # auth more data
packet.skip(1)
auth_more_data(packet)
else
raise "unkwown packet"
end
Expand Down Expand Up @@ -117,12 +121,16 @@ def client_handshake
packet.nulstr(@options[:username] || "root")

if @auth_plugin_name == "mysql_native_password" && password?
packet.int8(20)
packet.str(mysql_native_password(@auth_plugin_data))
authdata = mysql_native_password(@auth_plugin_data)
elsif @auth_plugin_name == "caching_sha2_password" && password?
authdata = caching_sha2_password(@auth_plugin_data)
else
packet.int8(0)
authdata = ""
end

packet.int8(authdata.length)
packet.str(authdata)

packet.nulstr(@options[:database]) if @options[:database]
packet.nulstr(@auth_plugin_name)
end
Expand All @@ -133,6 +141,8 @@ def auth_switch(plugin, data)
case plugin
when "mysql_native_password"
packet.str(mysql_native_password(data)) if password?
when "caching_sha2_password"
packet.str(caching_sha2_password(data)) if password?
when "mysql_clear_password"
raise AuthPluginError, "cleartext plugin not enabled" unless @options[:enable_cleartext_plugin]
packet.str(@options[:password]) if password?
Expand All @@ -146,6 +156,38 @@ def auth_switch(plugin, data)
end
end

def auth_more_data(packet)
if !@options[:ssl] && !@options[:socket]
raise ConnectionError, "caching_sha2_password requires either TCP with TLS or a unix socket"
end

case packet.int8
when 4
@conn.write_packet do |packet|
packet.str(@options[:password])
packet.str("\0")
end
when 3
# Fast auth OK
else
raise "unexpected packet"
end

@conn.read_packet do |packet|
if packet.ok?
packet.skip(1)
@conn.update_status(
affected_rows: packet.lenenc_int,
last_insert_id: packet.lenenc_int,
status_flags: packet.int16,
warnings: packet.int16
)
elsif packet.err?
raise Protocol.error(packet, ConnectionError)
end
end
end

def password?
@options[:password] && @options[:password].length > 0
end
Expand All @@ -162,6 +204,19 @@ def mysql_native_password(scramble)

bytes.pack("C*")
end

def caching_sha2_password(nonce)
nonce = nonce.strip!
password_digest = Digest::SHA256.digest(@options[:password] || "")
password_double_digest = Digest::SHA256.digest(password_digest)
scramble_digest = Digest::SHA256.digest(password_double_digest + nonce)

bytes = password_digest.length.times.map do |i|
password_digest.getbyte(i) ^ scramble_digest.getbyte(i)
end

bytes.pack("C*")
end
end
end
end
4 changes: 4 additions & 0 deletions lib/nocturne/read/payload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ def ok?
def err?
@payload.getbyte(0) == 0xFF
end

def tag
@payload.getbyte(0)
end
end
end
end
3 changes: 0 additions & 3 deletions test/auth_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

class AuthTest < NocturneTest
def has_caching_sha2?
skip("haven't implemented this yet")

server_version = new_tcp_client.server_version
server_version.split(".", 2)[0].to_i >= 8
end
Expand Down Expand Up @@ -64,7 +62,6 @@ def test_connect_without_ssl_or_unix_socket_caching_sha2_raises
new_tcp_client options
end

assert_includes err.message, "TRILOGY_UNSUPPORTED"
assert_includes err.message, "caching_sha2_password requires either TCP with TLS or a unix socket"
end

Expand Down
3 changes: 3 additions & 0 deletions test/mysql/caching_sha2_user.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE USER 'caching_sha2'@'%';
GRANT ALL PRIVILEGES ON test.* TO 'caching_sha2'@'%';
ALTER USER 'caching_sha2'@'%' IDENTIFIED /*!80000 WITH caching_sha2_password */ BY 'password';

0 comments on commit 29f158e

Please sign in to comment.