--- sqlite3.c 2024-03-22 19:22:47.046093173 +0100 +++ sqlite3-pass-exts.c 2024-03-22 19:24:09.557303716 +0100 @@ -26275,6 +26275,207 @@ sqlite3ResultStrAccum(context, &sRes); } +#define DEF_SALT_SIZE 20 +#define SHA_DIGEST_LENGTH 20 + +/// Forward declarations +//////////////////////////////////////////////////////////////////////////////// + +// ctype.h +extern int toupper (int __c) __THROW; + +// SHA256_crypt +char * sha256_crypt_r (const char *key, const char *salt, char *buffer, int buflen); + +// OpenSSL +unsigned char *SHA1(const unsigned char *d, size_t n, unsigned char *md); +int RAND_bytes(unsigned char *buf, int num); +unsigned long ERR_get_error(void); +char *ERR_error_string(unsigned long e, char *buf); + +//////////////////////////////////////////////////////////////////////////////// + +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) { + inv_type = + sqlite3_value_type(argv[1]) != SQLITE_TEXT && + sqlite3_value_type(argv[1]) != SQLITE_BLOB; + } + + 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 pass_size = sqlite3_value_bytes(argv[0]); + if (pass_size <= 0) { + return 1; + } + + if (argc >= 2) { + int salt_size = sqlite3_value_bytes(argv[1]); + 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; +} + +/** + * @brief SQLite3 extension function for hash generation. + * @details Computes a hash equivalent to the one generated by MySQL for 'mysql_native_password'. + * @param context SQLite3 context used for returning computation result. + * @param argc Number of arguments; expected to be 1. + * @param argv Argument list; expected to hold one argument with len > 0 of type 'SQLITE_TEXT'. + */ +static void mysql_native_passwordFunc(sqlite3_context* context, int argc, sqlite3_value** argv) { + if (argc != 1) { + sqlite3_result_text(context, "Invalid number of arguments", -1, SQLITE_TRANSIENT); + return; + } else { + if (check_args_types(argc, argv)) { + sqlite3_result_text(context, "Invalid argument type", -1, SQLITE_TRANSIENT); + return; + } + if (check_args_lengths(argc, argv)) { + sqlite3_result_text(context, "Invalid argument size", -1, SQLITE_TRANSIENT); + return; + } + } + + const unsigned char* input = sqlite3_value_text(argv[0]); + int input_len = strlen((const char*)input); + + unsigned char hash1[SHA_DIGEST_LENGTH] = { 0 }; + unsigned char hash2[SHA_DIGEST_LENGTH] = { 0 }; + + SHA1(input, input_len, hash1); + SHA1(hash1, SHA_DIGEST_LENGTH, hash2); + + char hex_hash[2 * SHA_DIGEST_LENGTH + 2]; + unsigned int i = 0; + + for (i = 0; i < SHA_DIGEST_LENGTH; i++) { + sprintf(hex_hash + 2 * i + 1, "%02x", hash2[i]); + + hex_hash[2 * i + 1] = toupper(hex_hash[2 * i + 1]); + hex_hash[2 * i + 1 + 1] = toupper(hex_hash[2 * i + 1 + 1]); + } + + hex_hash[0] = '*'; + hex_hash[2 * SHA_DIGEST_LENGTH + 1] = '\0'; + + sqlite3_result_text(context, hex_hash, -1, SQLITE_TRANSIENT); +} + +/** + * @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; 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 > 3) { + sqlite3_result_text(context, "Invalid number of arguments", -1, SQLITE_TRANSIENT); + return; + } else { + if (check_args_types(argc, argv)) { + sqlite3_result_text(context, "Invalid argument type", -1, SQLITE_TRANSIENT); + return; + } + if (check_args_lengths(argc, argv)) { + sqlite3_result_text(context, "Invalid argument size", -1, SQLITE_TRANSIENT); + return; + } + } + + /* 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) { + salt_size = sqlite3_value_bytes(argv[1]); + const void* b_salt = sqlite3_value_blob(argv[1]); + + memcpy(salt, b_salt, salt_size); + } else { + unsigned char salt_buf[DEF_SALT_SIZE + 1] = { 0 }; + + if (RAND_bytes(salt_buf, DEF_SALT_SIZE) != 1) { + const char t_msg[] = { "SALT creation failed (%lu:'%s')" }; + char err_buf[256] = { 0 }; + char err_msg[sizeof(err_buf)/sizeof(char) + sizeof(t_msg)/sizeof(char) + 20] = { 0 }; + + const unsigned long err = ERR_get_error(); + ERR_error_string(err, err_buf); + + sprintf(err_msg, t_msg, err, err_buf); + sqlite3_result_text(context, err_msg, -1, SQLITE_TRANSIENT); + return; + } else { + unsigned int i = 0; + + for (i = 0; i < sizeof(salt_buf)/sizeof(unsigned char); i++) { + salt_buf[i] = salt_buf[i] & 0x7f; + + if (salt_buf[i] == '\0' || salt_buf[i] == '$') { + salt_buf[i] = salt_buf[i] + 1; + } + } + + memcpy(salt, salt_buf, salt_size); + } + } + + /* 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 }; + strcat(sha2_salt, (const char*)salt); + sha256_crypt_r(cpass, sha2_salt, sha2_buf, sizeof(sha2_buf)); + + const char* sha256 = sha2_buf + salt_size + salt_prefix_len + 1; + + strcat(sha2_hash, (const char*)salt); + strcat(sha2_hash, sha256); + + sqlite3_result_text(context, sha2_hash, -1, SQLITE_TRANSIENT); +} + /* ** current_time() ** @@ -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),