diff --git a/cmake/plugins.cmake b/cmake/plugins.cmake index f7c406a3..70baea43 100644 --- a/cmake/plugins.cmake +++ b/cmake/plugins.cmake @@ -38,6 +38,9 @@ IF(WIN32) ENDIF() # AUTHENTICATION +IF(WIN32 OR WITH_SSL STREQUAL "OPENSSL") + REGISTER_PLUGIN("AUTH_SHA256PW" "${CC_SOURCE_DIR}/plugins/auth/sha256_pw.c" "sha256_password_client_plugin" "DYNAMIC" "" 0) +ENDIF() REGISTER_PLUGIN("AUTH_NATIVE" "${CC_SOURCE_DIR}/plugins/auth/my_auth.c" "native_password_client_plugin" "STATIC" "" 0) REGISTER_PLUGIN("AUTH_OLDPASSWORD" "${CC_SOURCE_DIR}/plugins/auth/old_password.c" "old_password_client_plugin" "STATIC" "" 1) SET(DIALOG_SOURCES ${CC_SOURCE_DIR}/plugins/auth/dialog.c ${CC_SOURCE_DIR}/libmariadb/get_password.c) diff --git a/plugins/auth/CMakeLists.txt b/plugins/auth/CMakeLists.txt index e90341c1..a43f7939 100644 --- a/plugins/auth/CMakeLists.txt +++ b/plugins/auth/CMakeLists.txt @@ -26,6 +26,29 @@ IF(AUTH_DIALOG_PLUGIN_TYPE MATCHES "DYNAMIC") INSTALL_PLUGIN(dialog ${CC_BINARY_DIR}/plugins/auth) ENDIF() +# SHA256 plugin +IF(AUTH_SHA256PW_PLUGIN_TYPE MATCHES "DYNAMIC") + ADD_DEFINITIONS(-DHAVE_SHA256PW_DYNAMIC=1) + + IF(WIN32) + SET_VERSION_INFO("TARGET:sha256_password" + "FILE_TYPE:VFT_DLL" + "SOURCE_FILE:plugins/auth/sha256_pw.c" + "ORIGINAL_FILE_NAME:sha256_password.dll" + "FILE_DESCRIPTION:Authentication plugin") + ENDIF() + SET(SHA256PW_SOURCES ${sha256_RC} sha256_pw.c) + IF(WIN32) + SET(SHA256PW_SOURCES ${SHA256PW_SOURCES} ${CC_SOURCE_DIR}/plugins/plugin.def) + ENDIF() + ADD_LIBRARY(sha256_password MODULE ${SHA256PW_SOURCES}) + IF(WITH_SSL STREQUAL "OPENSSL") + TARGET_LINK_LIBRARIES(sha256_password ${SSL_LIBRARIES}) + ENDIF() + SET_TARGET_PROPERTIES(sha256_password PROPERTIES PREFIX "") + SIGN_TARGET(sha256_password) + INSTALL_PLUGIN(sha256_password ${CC_BINARY_DIR}/plugins/auth) +ENDIF() # old_password plugin IF(AUTH_OLDPASSWORD_PLUGIN_TYPE MATCHES "DYNAMIC") ADD_DEFINITIONS(-DHAVE_OLDPASSWORD_DYNAMIC=1) diff --git a/plugins/auth/sha256_pw.c b/plugins/auth/sha256_pw.c new file mode 100644 index 00000000..ef7d9fed --- /dev/null +++ b/plugins/auth/sha256_pw.c @@ -0,0 +1,326 @@ +/************************************************************************************ + Copyright (C) 2017 MariaDB Corporation AB + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not see + or write to the Free Software Foundation, Inc., + 51 Franklin St., Fifth Floor, Boston, MA 02110, USA + *************************************************************************************/ +#ifndef _WIN32 +#define _GNU_SOURCE 1 +#endif + +#ifdef _WIN32 +#if !defined(HAVE_OPENSSL) +#define HAVE_WINCRYPT +#endif +#endif + +#if defined(HAVE_OPENSSL) || defined(HAVE_WINCRYPT) + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32 +#include +#endif + +#if defined(HAVE_OPENSSL) +#include +#include +#include +#elif defined(HAVE_WINCRYPT) +#include +#endif + +#define MAX_PW_LEN 1024 + +/* function prototypes */ +static int auth_sha256_client(MYSQL_PLUGIN_VIO *vio, MYSQL *mysql); +static int auth_sha256_init(char *unused1, + size_t unused2, + int unused3, + va_list); + + +#ifndef HAVE_SHA256PW_DYNAMIC +struct st_mysql_client_plugin_AUTHENTICATION sha256_password_client_plugin= +#else +struct st_mysql_client_plugin_AUTHENTICATION _mysql_client_plugin_declaration_ = +#endif +{ + MYSQL_CLIENT_AUTHENTICATION_PLUGIN, + MYSQL_CLIENT_AUTHENTICATION_PLUGIN_INTERFACE_VERSION, + "sha256_password", + "Georg Richter", + "SHA256 Authentication Plugin", + {0,1,0}, + "LGPL", + NULL, + auth_sha256_init, + NULL, + NULL, + auth_sha256_client +}; + +#ifdef HAVE_WINCRYPT +static LPBYTE ma_load_pem(const char *buffer, DWORD *buffer_len) +{ + LPBYTE der_buffer= NULL; + DWORD der_buffer_length; + + if (buffer_len == NULL || *buffer_len == 0) + return NULL; + /* calculate the length of DER binary */ + if (!CryptStringToBinaryA(buffer, *buffer_len, CRYPT_STRING_BASE64HEADER, + NULL, &der_buffer_length, NULL, NULL)) + goto end; + /* allocate DER binary buffer */ + if (!(der_buffer= (LPBYTE)LocalAlloc(0, der_buffer_length))) + goto end; + /* convert to DER binary */ + if (!CryptStringToBinaryA(buffer, *buffer_len, CRYPT_STRING_BASE64HEADER, + der_buffer, &der_buffer_length, NULL, NULL)) + goto end; + + *buffer_len= der_buffer_length; + + return der_buffer; + +end: + if (der_buffer) + LocalFree(der_buffer); + *buffer_len= 0; + return NULL; +} +#endif + +char *load_pub_key_file(const char *filename, int *pub_key_size) +{ + FILE *fp= NULL; + char *buffer= NULL; + unsigned char error= 1; + + if (!pub_key_size) + return NULL; + + if (!(fp= fopen(filename, "r"))) + goto end; + + if (fseek(fp, 0, SEEK_END)) + goto end; + + *pub_key_size= ftell(fp); + rewind(fp); + + if (!(buffer= malloc(*pub_key_size + 1))) + goto end; + + if (!fread(buffer, *pub_key_size, 1, fp)) + goto end; + + error= 0; + +end: + if (fp) + fclose(fp); + if (error && buffer) + { + free(buffer); + buffer= NULL; + } + return buffer; +} + + +static int auth_sha256_client(MYSQL_PLUGIN_VIO *vio, MYSQL *mysql) +{ + unsigned char *packet; + int packet_length; + int rc= CR_ERROR; + char passwd[MAX_PW_LEN]; + unsigned char rsa_enc_pw[MAX_PW_LEN]; + unsigned int rsa_size; + unsigned int pwlen, i; + +#if defined(HAVE_OPENSSL) + RSA *pubkey= NULL; + BIO *bio; +#elif defined(HAVE_WINCRYPT) + HCRYPTKEY pubkey= 0; + HCRYPTPROV hProv= 0; + LPBYTE der_buffer= NULL; + DWORD der_buffer_len= 0; + CERT_PUBLIC_KEY_INFO *publicKeyInfo= NULL; + DWORD ParamSize= sizeof(DWORD); + int publicKeyInfoLen; +#endif + char *filebuffer= NULL; + + /* read error */ + if ((packet_length= vio->read_packet(vio, &packet)) < 0) + return CR_ERROR; + + if (packet_length != SCRAMBLE_LENGTH + 1) + return CR_SERVER_HANDSHAKE_ERR; + + memmove(mysql->scramble_buff, packet, SCRAMBLE_LENGTH); + mysql->scramble_buff[SCRAMBLE_LENGTH]= 0; + + /* if a tls session is active we need to send plain password */ + if (mysql_get_ssl_cipher(mysql)) + { + if (vio->write_packet(vio, (unsigned char *)mysql->passwd, strlen(mysql->passwd) + 1)) + return CR_ERROR; + return CR_OK; + } + + /* send empty packet if no password was provided */ + if (!mysql->passwd || !mysql->passwd[0]) + { + if (vio->write_packet(vio, 0, 0)) + return CR_ERROR; + return CR_OK; + } + + /* read public key file (if specified) */ + if (mysql->options.extension && + mysql->options.extension->server_public_key) + { + filebuffer= load_pub_key_file(mysql->options.extension->server_public_key, + &packet_length); + } + + /* if no public key file was specified or if we couldn't read the file, + we ask server to send public key */ + if (!filebuffer) + { + unsigned char buf= 1; + if (vio->write_packet(vio, &buf, 1)) + return CR_ERROR; + if ((packet_length=vio->read_packet(vio, &packet)) == -1) + return CR_ERROR; + } +#if defined(HAVE_OPENSSL) + bio= BIO_new_mem_buf(filebuffer ? (unsigned char *)filebuffer : packet, + packet_length); + if ((pubkey= PEM_read_bio_RSA_PUBKEY(bio, NULL, NULL, NULL))) + rsa_size= RSA_size(pubkey); + BIO_free(bio); + ERR_clear_error(); +#elif defined(HAVE_WINCRYPT) + der_buffer_len= packet_length; + /* Load pem and convert it to binary object. New length will be returned + in der_buffer_len */ + if (!(der_buffer= ma_load_pem(filebuffer ? filebuffer : packet, &der_buffer_len))) + goto error; + + /* Create context and load public key */ + if (!CryptDecodeObjectEx(X509_ASN_ENCODING, X509_PUBLIC_KEY_INFO, + der_buffer, der_buffer_len, + CRYPT_ENCODE_ALLOC_FLAG, NULL, + &publicKeyInfo, &publicKeyInfoLen)) + goto error; + LocalFree(der_buffer); + + if (!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, + CRYPT_VERIFYCONTEXT)) + goto error; + if (!CryptImportPublicKeyInfo(hProv, X509_ASN_ENCODING, + publicKeyInfo, &pubkey)) + goto error; + + /* Get rsa_size */ + CryptGetKeyParam(pubkey, KP_KEYLEN, &rsa_size, &ParamSize, 0); + rsa_size /= 8; +#endif + if (!pubkey) + return CR_ERROR; + + pwlen= strlen(mysql->passwd) + 1; /* include terminating zero */ + if (pwlen > MAX_PW_LEN) + goto error; + memcpy(passwd, mysql->passwd, pwlen); + + /* xor password with scramble */ + for (i=0; i < pwlen; i++) + passwd[i]^= *(mysql->scramble_buff + i % SCRAMBLE_LENGTH); + + /* encrypt scrambled password */ +#if defined(HAVE_OPENSSL) + if (RSA_public_encrypt(pwlen, (unsigned char *)passwd, rsa_enc_pw, pubkey, RSA_PKCS1_OAEP_PADDING) < 0) + goto error; +#elif defined(HAVE_WINCRYPT) + if (!CryptEncrypt(pubkey, 0, TRUE, CRYPT_OAEP, passwd, &pwlen, MAX_PW_LEN)) + goto error; + /* Windows encrypts as little-endian, while server (openssl) expects + big-endian, so we have to revert the string */ + for (i= 0; i < rsa_size / 2; i++) + { + rsa_enc_pw[i]= passwd[rsa_size - 1 - i]; + rsa_enc_pw[rsa_size - 1 - i]= passwd[i]; + } +#endif + if (vio->write_packet(vio, rsa_enc_pw, rsa_size)) + goto error; + + rc= CR_OK; +error: +#if defined(HAVE_OPENSSL) + if (pubkey) + RSA_free(pubkey); +#elif defined(HAVE_WINCRYPT) + CryptReleaseContext(hProv, 0); +#endif + free(filebuffer); + return rc; +} +/* }}} */ + +/* {{{ static int auth_sha256_init */ +/* + Initialization routine + + SYNOPSIS + auth_sha256_init + unused1 + unused2 + unused3 + unused4 + + DESCRIPTION + Init function checks if the caller provides own dialog function. + The function name must be mariadb_auth_dialog or + mysql_authentication_dialog_ask. If the function cannot be found, + we will use owr own simple command line input. + + RETURN + 0 success + */ +static int auth_sha256_init(char *unused1 __attribute__((unused)), + size_t unused2 __attribute__((unused)), + int unused3 __attribute__((unused)), + va_list unused4 __attribute__((unused))) +{ + return 0; +} +/* }}} */ + +#endif /* defined(HAVE_OPENSSL) || defined(HAVE_WINCRYPT) */ diff --git a/unittest/libmariadb/connection.c b/unittest/libmariadb/connection.c index 2295f90b..29b2a931 100644 --- a/unittest/libmariadb/connection.c +++ b/unittest/libmariadb/connection.c @@ -34,7 +34,7 @@ static int test_conc66(MYSQL *my) FILE *fp; char query[1024]; - if (!(fp= fopen("./ma_test.cnf", "w"))) + if (!(fp= fopen("./my.cnf", "w"))) return FAIL; fprintf(fp, "[notmygroup]\n"); @@ -49,7 +49,7 @@ static int test_conc66(MYSQL *my) rc= mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "conc-66"); check_mysql_rc(rc, mysql); - rc= mysql_options(mysql, MYSQL_READ_DEFAULT_FILE, "./ma_test.cnf"); + rc= mysql_options(mysql, MYSQL_READ_DEFAULT_FILE, "./my.cnf"); check_mysql_rc(rc, mysql); sprintf(query, "GRANT ALL ON %s.* TO 'conc66'@'%s' IDENTIFIED BY 'test\";#test'", schema, hostname ? hostname : "localhost"); @@ -69,7 +69,6 @@ static int test_conc66(MYSQL *my) check_mysql_rc(rc, my); mysql_close(mysql); - remove("./ma_test.cnf"); return OK; } @@ -449,7 +448,7 @@ static int test_bug31669(MYSQL *mysql) static int test_bug33831(MYSQL *mysql) { FAIL_IF(my_test_connect(mysql, hostname, username, - password, schema, port, socketname, 0), + password, schema, port, socketname, 0), "Error expected"); return OK; @@ -694,7 +693,7 @@ int test_connection_timeout3(MYSQL *unused __attribute__((unused))) mysql_options(mysql, MYSQL_OPT_WRITE_TIMEOUT, (unsigned int *)&read_write_timeout); mysql_options(mysql, MYSQL_INIT_COMMAND, "set @a:=SLEEP(6)"); start= time(NULL); - if (my_test_connect(mysql, hostname, username, password, schema, port, socketname, CLIENT_REMEMBER_OPTIONS)) + if (my_test_connect(mysql, hostname, username, password, schema, port, NULL, CLIENT_REMEMBER_OPTIONS)) { diag("timeout error expected"); elapsed= time(NULL) - start; @@ -711,7 +710,7 @@ int test_connection_timeout3(MYSQL *unused __attribute__((unused))) mysql_options(mysql, MYSQL_OPT_READ_TIMEOUT, (unsigned int *)&read_write_timeout); mysql_options(mysql, MYSQL_OPT_WRITE_TIMEOUT, (unsigned int *)&read_write_timeout); - if (!my_test_connect(mysql, hostname, username, password, schema, port, socketname, CLIENT_REMEMBER_OPTIONS)) + if (!my_test_connect(mysql, hostname, username, password, schema, port, NULL, CLIENT_REMEMBER_OPTIONS)) { diag("Error: %s", mysql_error(mysql)); return FAIL; @@ -824,8 +823,6 @@ static int test_bind_address(MYSQL *my) return FAIL; } diag("%s", mysql_get_host_info(mysql)); - sprintf(query, "DROP USER '%s'@'%s'", username, bind_addr); - rc= mysql_query(my, query); mysql_close(mysql); return OK; } @@ -837,7 +834,7 @@ static int test_get_options(MYSQL *unused __attribute__((unused))) MYSQL_OPT_PROTOCOL, MYSQL_OPT_READ_TIMEOUT, MYSQL_OPT_WRITE_TIMEOUT, 0}; my_bool options_bool[]= {MYSQL_OPT_RECONNECT, MYSQL_REPORT_DATA_TRUNCATION, MYSQL_OPT_COMPRESS, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, MYSQL_SECURE_AUTH, -#ifdef _WIN32 +#ifdef _WIN32 MYSQL_OPT_NAMED_PIPE, #endif 0}; @@ -922,10 +919,6 @@ static int test_sess_track_db(MYSQL *mysql) int rc; const char *data; size_t len; - char query[140]; - - diag("fixes not merged"); - return SKIP; if (!(mysql->server_capabilities & CLIENT_SESSION_TRACKING)) { @@ -941,17 +934,13 @@ static int test_sess_track_db(MYSQL *mysql) "session_track_get_first failed"); FAIL_IF(strncmp(data, "mysql", len), "Expected new schema 'mysql'"); - snprintf(query, 139, "USE %s", schema); - rc= mysql_query(mysql, query); + rc= mysql_query(mysql, "USE test"); check_mysql_rc(rc, mysql); - FAIL_IF(strcmp(mysql->db, schema), "Expected new schema 'testc'"); + FAIL_IF(strcmp(mysql->db, "test"), "Expected new schema 'test'"); FAIL_IF(mysql_session_track_get_first(mysql, SESSION_TRACK_SCHEMA, &data, &len), "session_track_get_first failed"); - FAIL_IF(strncmp(data, schema, len), "Expected new schema 'testc'"); - - rc= mysql_query(mysql, "DROP SCHEMA testc"); - check_mysql_rc(rc, mysql); + FAIL_IF(strncmp(data, "test", len), "Expected new schema 'test'"); diag("charset: %s", mysql->charset->csname); rc= mysql_query(mysql, "SET NAMES utf8"); @@ -988,7 +977,7 @@ static int test_sess_track_db(MYSQL *mysql) return OK; } -#ifndef WIN32 + static int test_unix_socket_close(MYSQL *unused __attribute__((unused))) { MYSQL *mysql= mysql_init(NULL); @@ -1016,7 +1005,6 @@ static int test_unix_socket_close(MYSQL *unused __attribute__((unused))) mysql_close(mysql); return OK; } -#endif static int test_reset(MYSQL *mysql) { @@ -1037,13 +1025,13 @@ static int test_reset(MYSQL *mysql) rc= mysql_reset_connection(mysql); check_mysql_rc(rc, mysql); - FAIL_IF(mysql_affected_rows(mysql) != ~(my_ulonglong)0, "Expected 0 rows"); + FAIL_IF(mysql_affected_rows(mysql) != ~(unsigned long)0, "Expected 0 rows"); rc= mysql_query(mysql, "SELECT a FROM t1"); check_mysql_rc(rc, mysql); rc= mysql_query(mysql, "SELECT 1 FROM DUAL"); - FAIL_IF(!rc, "Error expected"); + FAIL_IF(!rc, "Error expected"); rc= mysql_reset_connection(mysql); check_mysql_rc(rc, mysql); @@ -1064,38 +1052,60 @@ static int test_reset(MYSQL *mysql) mysql_free_result(res); - rc= mysql_query(mysql, "DROP TABLE t1"); - check_mysql_rc(rc, mysql); - return OK; } -static int test_mdev12446(MYSQL *my __attribute__((unused))) +static int test_auth256(MYSQL *my) { - /* - if specified file didn't exist, valgrind reported a leak, - if no file was specified and no default file is installed, - C/C crashed due to double free. - */ MYSQL *mysql= mysql_init(NULL); - mysql_options(mysql, MYSQL_READ_DEFAULT_FILE, "file.notfound"); - FAIL_IF(!my_test_connect(mysql, hostname, username, password, schema, - port, socketname, 0), mysql_error(mysql)); + int rc; + MYSQL_RES *res; + int num_rows= 0; + + rc= mysql_query(my, "SELECT * FROM information_schema.plugins where plugin_name='sha256_password'"); + check_mysql_rc(rc, mysql); + + res= mysql_store_result(my); + num_rows= mysql_num_rows(res); + mysql_free_result(res); + + if (!num_rows) + { + diag("server doesn't support sha256 authentication"); + return SKIP; + } + + rc= mysql_query(my, "DROP USER IF EXISTS sha256user@localhost"); + check_mysql_rc(rc, mysql); + + rc= mysql_query(my, "CREATE user sha256user@localhost identified with sha256_password by 'foo'"); + check_mysql_rc(rc, my); + if (!mysql_real_connect(mysql, hostname, "sha256user", "foo", NULL, port, socketname, 0)) + { + diag("error: %s", mysql_error(mysql)); + mysql_close(mysql); + return FAIL; + } mysql_close(mysql); + mysql= mysql_init(NULL); - mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "notfound"); - FAIL_IF(!my_test_connect(mysql, hostname, username, password, schema, - port, socketname, 0), mysql_error(mysql)); + mysql_options(mysql, MYSQL_SERVER_PUBLIC_KEY, "rsa_public_key.pem"); + if (!mysql_real_connect(mysql, hostname, "sha256user", "foo", NULL, port, socketname, 0)) + { + diag("error: %s", mysql_error(mysql)); + mysql_close(mysql); + return FAIL; + } mysql_close(mysql); + rc= mysql_query(my, "DROP USER sha256user@localhost"); + check_mysql_rc(rc, mysql); return OK; } struct my_tests_st my_tests[] = { - {"test_mdev12446", test_mdev12446, TEST_CONNECTION_DEFAULT, 0, NULL, NULL}, + {"test_auth256", test_auth256, TEST_CONNECTION_DEFAULT, 0, NULL, NULL}, {"test_reset", test_reset, TEST_CONNECTION_DEFAULT, 0, NULL, NULL}, -#ifndef WIN32 {"test_unix_socket_close", test_unix_socket_close, TEST_CONNECTION_NONE, 0, NULL, NULL}, -#endif {"test_sess_track_db", test_sess_track_db, TEST_CONNECTION_DEFAULT, 0, NULL, NULL}, {"test_get_options", test_get_options, TEST_CONNECTION_DEFAULT, 0, NULL, NULL}, {"test_wrong_bind_address", test_wrong_bind_address, TEST_CONNECTION_DEFAULT, 0, NULL, NULL}, @@ -1114,7 +1124,6 @@ struct my_tests_st my_tests[] = { {"test_connection_timeout", test_connection_timeout, TEST_CONNECTION_NONE, 0, NULL, NULL}, {"test_connection_timeout2", test_connection_timeout2, TEST_CONNECTION_NONE, 0, NULL, NULL}, {"test_connection_timeout3", test_connection_timeout3, TEST_CONNECTION_NONE, 0, NULL, NULL}, - {NULL, NULL, 0, 0, NULL, NULL} };