diff --git a/deps/sqlite3/sqlite3_pass_exts.patch b/deps/sqlite3/sqlite3_pass_exts.patch index 83d940212..89b613119 100644 --- a/deps/sqlite3/sqlite3_pass_exts.patch +++ b/deps/sqlite3/sqlite3_pass_exts.patch @@ -1,6 +1,6 @@ --- sqlite3.c 2024-03-22 19:22:47.046093173 +0100 +++ sqlite3-pass-exts.c 2024-03-22 19:24:09.557303716 +0100 -@@ -26313,6 +26313,183 @@ +@@ -26275,6 +26275,207 @@ sqlite3ResultStrAccum(context, &sRes); } @@ -27,30 +27,35 @@ +int check_args_types(int argc, sqlite3_value** argv) { + int inv_type = sqlite3_value_type(argv[0]) != SQLITE_TEXT; + -+ if (inv_type == 0 && argc == 2) { -+ return ++ if (inv_type == 0 && argc >= 2) { ++ inv_type = + sqlite3_value_type(argv[1]) != SQLITE_TEXT && + sqlite3_value_type(argv[1]) != SQLITE_BLOB; -+ } else { -+ return inv_type; + } ++ ++ if (inv_type == 0 && argc >= 3) { ++ inv_type = sqlite3_value_type(argv[2]) != SQLITE_INTEGER; ++ } ++ ++ return inv_type; +} + +int check_args_lengths(int argc, sqlite3_value** argv) { -+ int inv_size = 1; -+ + int pass_size = sqlite3_value_bytes(argv[0]); -+ if (pass_size > 0) { -+ inv_size = 0; ++ if (pass_size <= 0) { ++ return 1; + } + -+ if (inv_size == 0 && argc == 2) { ++ if (argc >= 2) { + int salt_size = sqlite3_value_bytes(argv[1]); -+ -+ return salt_size <= 0 || salt_size > DEF_SALT_SIZE; -+ } else { -+ return inv_size; ++ if (salt_size <= 0 || salt_size > DEF_SALT_SIZE) { ++ return 1; ++ } + } ++ ++ /* argc == 3: rounds is an integer; range is checked in caching_sha2_passwordFunc(). */ ++ ++ return 0; +} + +/** @@ -103,15 +108,19 @@ +/** + * @brief SQLite3 extension function for hash generation. + * @details Computes a hash equivalent to the one generated by MySQL for 'caching_sha2_password'. ++ * Output format matches MySQL's storage: '$A$$' where is the ++ * 3-char zero-padded uppercase hex of (rounds/1000) — see MySQL ++ * sql/auth/sha2_password.cc::Caching_sha2_password::digest_round_separator(). + * @param context SQLite3 context used for returning computation result. -+ * @param argc Number of arguments; either 1 or 2. One for random salt, two providing salt. -+ * @param argv Argument list; expected to hold either 1 or 2 arguments: -+ * 1. Password to be hashed; with len > 0 and of type 'SQLITE_TEXT'. -+ * 1. Optional salt; with (len > 0 && len <= 20) and of type ('SQLITE_TEXT' || 'SQLITE_BLOB'). If no salt is -+ * provided a randomly generated salt with length 20 will be used. ++ * @param argc Number of arguments; 1, 2, or 3. ++ * @param argv Argument list: ++ * 1. Password to be hashed; len > 0, type 'SQLITE_TEXT'. ++ * 2. Optional salt; len in (0,20], type 'SQLITE_TEXT' or 'SQLITE_BLOB'. Random if omitted. ++ * 3. Optional rounds; integer in [5000,4095000] in steps of 1000 (matches MySQL's ++ * caching_sha2_password_digest_rounds). Defaults to 5000 when omitted. + */ +static void caching_sha2_passwordFunc(sqlite3_context* context, int argc, sqlite3_value** argv) { -+ if (argc < 1 || argc > 2) { ++ if (argc < 1 || argc > 3) { + sqlite3_result_text(context, "Invalid number of arguments", -1, SQLITE_TRANSIENT); + return; + } else { @@ -125,11 +134,24 @@ + } + } + ++ /* Resolve rounds: default 5000 (MySQL 8.0 historical default), overridable via ++ argv[2]. Range matches MySQL's caching_sha2_password_digest_rounds bounds. */ ++ long rounds = 5000; ++ if (argc == 3) { ++ rounds = (long)sqlite3_value_int64(argv[2]); ++ if (rounds < 5000 || rounds > 4095000 || (rounds % 1000) != 0) { ++ sqlite3_result_text(context, ++ "Invalid rounds: expected multiple of 1000 in [5000,4095000]", ++ -1, SQLITE_TRANSIENT); ++ return; ++ } ++ } ++ + unsigned int salt_size = DEF_SALT_SIZE; + const char* cpass = (const char*)sqlite3_value_text(argv[0]); + unsigned char salt[DEF_SALT_SIZE + 1] = { 0 }; + -+ if (argc == 2) { ++ if (argc >= 2) { + salt_size = sqlite3_value_bytes(argv[1]); + const void* b_salt = sqlite3_value_blob(argv[1]); + @@ -163,17 +185,19 @@ + } + } + -+ #define BASE_SHA2_SALT "$5$rounds=5000$" -+ #define BASE_SHA2_HASH "$A$005$" ++ /* Build the sha256_crypt salt-prefix "$5$rounds=$" and the MySQL hash prefix ++ "$A$$" dynamically from the resolved rounds value. Buffers fit comfortably: ++ "$5$rounds=4095000$" is 18 chars + 20 salt + NUL = 39 (<100). */ ++ char sha2_salt[100] = { 0 }; ++ char sha2_hash[100] = { 0 }; ++ int salt_prefix_len = snprintf(sha2_salt, sizeof(sha2_salt), "$5$rounds=%ld$", rounds); ++ snprintf(sha2_hash, sizeof(sha2_hash), "$A$%03X$", (unsigned int)(rounds / 1000)); + + char sha2_buf[100] = { 0 }; -+ char sha2_salt[100] = { BASE_SHA2_SALT }; -+ + strcat(sha2_salt, (const char*)salt); + sha256_crypt_r(cpass, sha2_salt, sha2_buf, sizeof(sha2_buf)); + -+ char sha2_hash[100] = { BASE_SHA2_HASH }; -+ const char* sha256 = sha2_buf + salt_size + strlen(BASE_SHA2_SALT) + 1; ++ const char* sha256 = sha2_buf + salt_size + salt_prefix_len + 1; + + strcat(sha2_hash, (const char*)salt); + strcat(sha2_hash, sha256); @@ -184,13 +208,14 @@ /* ** current_time() ** -@@ -133269,6 +133269,9 @@ +@@ -133230,6 +133431,10 @@ FUNCTION(substr, 3, 0, 0, substrFunc ), FUNCTION(substring, 2, 0, 0, substrFunc ), FUNCTION(substring, 3, 0, 0, substrFunc ), + FUNCTION(mysql_native_password, 1, 0, 0, mysql_native_passwordFunc ), + FUNCTION(caching_sha2_password, 1, 0, 0, caching_sha2_passwordFunc ), + FUNCTION(caching_sha2_password, 2, 0, 0, caching_sha2_passwordFunc ), ++ FUNCTION(caching_sha2_password, 3, 0, 0, caching_sha2_passwordFunc ), WAGGREGATE(sum, 1,0,0, sumStep, sumFinalize, sumFinalize, sumInverse, 0), WAGGREGATE(total, 1,0,0, sumStep,totalFinalize,totalFinalize,sumInverse, 0), WAGGREGATE(avg, 1,0,0, sumStep, avgFinalize, avgFinalize, sumInverse, 0), diff --git a/test/tap/tests/test_sqlite3_pass_exts-t.cpp b/test/tap/tests/test_sqlite3_pass_exts-t.cpp index 9cef59e10..a354cc443 100644 --- a/test/tap/tests/test_sqlite3_pass_exts-t.cpp +++ b/test/tap/tests/test_sqlite3_pass_exts-t.cpp @@ -262,8 +262,29 @@ int test_pass_match(MYSQL* admin, const user_def_t& def) { mysql_free_result(myres); } else if (def.auth == "caching_sha2_password") { + // MySQL stores caching_sha2_password as '$A$$' (70 ASCII bytes). + // def.hash is the hex-encoding of those 70 bytes (140 chars). The 3 ASCII chars + // at offset 3..5 (hex offset 6..11) carry rounds/1000 in 3-char zero-padded + // uppercase hex. Decode and pass the resolved rounds value to the SQLite + // extension so the regenerated digest uses the same iteration count as the + // backend (matters once MySQL ships with default rounds != 5000). + long rounds = 5000; + if (def.hash.size() >= 12) { + string rounds_field; + for (size_t i = 6; i < 12; i += 2) { + rounds_field += static_cast(stoi(def.hash.substr(i, 2), nullptr, 16)); + } + try { + rounds = stol(rounds_field, nullptr, 16) * 1000; + } catch (const std::exception&) { + diag("Failed to parse rounds field '%s' from MySQL hash '%s'", + rounds_field.c_str(), def.hash.c_str()); + } + } + const string GEN_SHA2_PASS { - "SELECT HEX(CACHING_SHA2_PASSWORD('" + def.pass + "', UNHEX('" + def.salt + "')))" + "SELECT HEX(CACHING_SHA2_PASSWORD('" + def.pass + "', UNHEX('" + def.salt + "'), " + + std::to_string(rounds) + "))" }; MYSQL_QUERY_T(admin, GEN_SHA2_PASS.c_str()); @@ -274,8 +295,8 @@ int test_pass_match(MYSQL* admin, const user_def_t& def) { ok( def.hash == admin_hash, - "MySQL hash should match ProxySQL generated mysql:'%s', admin:'%s'", - def.hash.c_str(), admin_hash.c_str() + "MySQL hash should match ProxySQL generated mysql:'%s', admin:'%s', rounds:%ld", + def.hash.c_str(), admin_hash.c_str(), rounds ); mysql_free_result(myres); @@ -404,13 +425,20 @@ const vector INV_INPUTS { "ProxySQL Admin Error: wrong number of arguments to function CACHING_SHA2_PASSWORD()" }, { - "SELECT CACHING_SHA2_PASSWORD('00', '00', '00')", 1, + "SELECT CACHING_SHA2_PASSWORD('00', '00', 5000, 'extra')", 1, "ProxySQL Admin Error: wrong number of arguments to function CACHING_SHA2_PASSWORD()" }, { "SELECT CACHING_SHA2_PASSWORD('', '')", 0, "Invalid argument size" }, { "SELECT CACHING_SHA2_PASSWORD('', '000000000000000000000')", 0, "Invalid argument size" }, { "SELECT CACHING_SHA2_PASSWORD(2, '00')", 0, "Invalid argument type" }, { "SELECT CACHING_SHA2_PASSWORD('00', 2)", 0, "Invalid argument type" }, + { "SELECT CACHING_SHA2_PASSWORD('00', '00', '00')", 0, "Invalid argument type" }, + { "SELECT CACHING_SHA2_PASSWORD('00', '00', 1000)", 0, + "Invalid rounds: expected multiple of 1000 in [5000,4095000]" }, + { "SELECT CACHING_SHA2_PASSWORD('00', '00', 5500)", 0, + "Invalid rounds: expected multiple of 1000 in [5000,4095000]" }, + { "SELECT CACHING_SHA2_PASSWORD('00', '00', 4096000)", 0, + "Invalid rounds: expected multiple of 1000 in [5000,4095000]" }, };