From 29f158e6d9317735b2d6a2f7e74e12f528209523 Mon Sep 17 00:00:00 2001 From: Daniel Colson Date: Tue, 23 Apr 2024 11:12:18 -0400 Subject: [PATCH] Implement caching_sha2_password auth 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. --- .github/workflows/main.yml | 3 +- README.md | 1 - lib/nocturne/protocol/handshake.rb | 63 ++++++++++++++++++++++++++++-- lib/nocturne/read/payload.rb | 4 ++ test/auth_test.rb | 3 -- test/mysql/caching_sha2_user.sql | 3 ++ 6 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 test/mysql/caching_sha2_user.sql diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8979674..9d794dd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/README.md b/README.md index 43c5a11..29408ab 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/nocturne/protocol/handshake.rb b/lib/nocturne/protocol/handshake.rb index f41ce76..88a5c1c 100644 --- a/lib/nocturne/protocol/handshake.rb +++ b/lib/nocturne/protocol/handshake.rb @@ -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 @@ -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 @@ -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? @@ -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 @@ -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 diff --git a/lib/nocturne/read/payload.rb b/lib/nocturne/read/payload.rb index 792e0b1..610fb4f 100644 --- a/lib/nocturne/read/payload.rb +++ b/lib/nocturne/read/payload.rb @@ -123,6 +123,10 @@ def ok? def err? @payload.getbyte(0) == 0xFF end + + def tag + @payload.getbyte(0) + end end end end diff --git a/test/auth_test.rb b/test/auth_test.rb index 2cba82b..7f57943 100644 --- a/test/auth_test.rb +++ b/test/auth_test.rb @@ -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 @@ -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 diff --git a/test/mysql/caching_sha2_user.sql b/test/mysql/caching_sha2_user.sql new file mode 100644 index 0000000..8a78c4d --- /dev/null +++ b/test/mysql/caching_sha2_user.sql @@ -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';