diff --git a/dbcon/joblist/CMakeLists.txt b/dbcon/joblist/CMakeLists.txt index 3808f92c7..77fca4a5e 100644 --- a/dbcon/joblist/CMakeLists.txt +++ b/dbcon/joblist/CMakeLists.txt @@ -1,5 +1,5 @@ -include_directories( ${ENGINE_COMMON_INCLUDES} ) +include_directories( ${ENGINE_COMMON_INCLUDES} ${ENGINE_SRC_DIR}/tools/passwd) ########### next target ############### @@ -55,7 +55,8 @@ set(joblist_LIB_SRCS tupleunion.cpp unique32generator.cpp virtualtable.cpp - windowfunctionstep.cpp) + windowfunctionstep.cpp + ${ENGINE_SRC_DIR}/tools/passwd/secrets.cpp) add_library(joblist SHARED ${joblist_LIB_SRCS}) diff --git a/dbcon/joblist/resourcemanager.cpp b/dbcon/joblist/resourcemanager.cpp index b9f207d5b..76f9f5498 100644 --- a/dbcon/joblist/resourcemanager.cpp +++ b/dbcon/joblist/resourcemanager.cpp @@ -37,6 +37,7 @@ using namespace boost; #include "jl_logger.h" #include "cgroupconfigurator.h" #include "liboamcpp.h" +#include "secrets.h" using namespace config; @@ -266,6 +267,11 @@ ResourceManager::ResourceManager(bool runningInExeMgr) : fAllowedDiskAggregation = getBoolVal(fRowAggregationStr, "AllowDiskBasedAggregation", defaultAllowDiskAggregation); + if (!load_encryption_keys()) + { + Logger log; + log.logMessage(logging::LOG_TYPE_ERROR, "Error loading CEJ password encryption keys"); + } } int ResourceManager::getEmPriority() const @@ -381,7 +387,9 @@ bool ResourceManager::getMysqldInfo( // MCS will read username and pass from disk if the config changed. bool reReadConfig = true; u = getStringVal("CrossEngineSupport", "User", hostUserUnassignedValue, reReadConfig); - w = getStringVal("CrossEngineSupport", "Password", "", reReadConfig); + std::string encryptedPW = getStringVal("CrossEngineSupport", "Password", "", reReadConfig); + //This will return back the plaintext password if there is no MCSDATADIR/.secrets file present + w = decrypt_password(encryptedPW); // MCS will not read username and pass from disk if the config changed. h = getStringVal("CrossEngineSupport", "Host", hostUserUnassignedValue); p = getUintVal("CrossEngineSupport", "Port", 0); diff --git a/debian/mariadb-plugin-columnstore.install b/debian/mariadb-plugin-columnstore.install index eca3098d7..39f4e8c72 100644 --- a/debian/mariadb-plugin-columnstore.install +++ b/debian/mariadb-plugin-columnstore.install @@ -22,6 +22,8 @@ usr/bin/controllernode usr/bin/cpimport usr/bin/cpimport.bin usr/bin/cplogger +usr/bin/cspasswd +usr/bin/cskeys usr/bin/dbbuilder usr/bin/dbrmctl usr/bin/ddlcleanup diff --git a/mcsconfig.h.in b/mcsconfig.h.in index e80223f0e..9f6ba06e6 100644 --- a/mcsconfig.h.in +++ b/mcsconfig.h.in @@ -393,4 +393,6 @@ #define MCSMYCNFDIR "${MARIADB_MYCNFDIR}" +#define MCSDATADIR "${ENGINE_DATADIR}" + #endif diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index d5b4d95af..00b8f85c3 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -11,3 +11,4 @@ add_subdirectory(cleartablelock) add_subdirectory(ddlcleanup) add_subdirectory(idbmeminfo) add_subdirectory(rebuildEM) +add_subdirectory(passwd) diff --git a/tools/passwd/CMakeLists.txt b/tools/passwd/CMakeLists.txt new file mode 100644 index 000000000..cc9e7e330 --- /dev/null +++ b/tools/passwd/CMakeLists.txt @@ -0,0 +1,17 @@ + +include_directories( ${ENGINE_COMMON_INCLUDES} ) + +########### next target ############### + +set(cspasswd_SRCS cspasswd.cpp secrets.cpp) +set(cskeys_SRCS cskeys.cpp secrets.cpp) + +add_executable(cspasswd ${cspasswd_SRCS}) +add_executable(cskeys ${cskeys_SRCS}) + +target_link_libraries(cspasswd ${ENGINE_LDFLAGS} ${MARIADB_CLIENT_LIBS} ${ENGINE_EXEC_LIBS}) +target_link_libraries(cskeys ${ENGINE_LDFLAGS} ${MARIADB_CLIENT_LIBS} ${ENGINE_EXEC_LIBS}) + +install(TARGETS cspasswd DESTINATION ${ENGINE_BINDIR} COMPONENT columnstore-engine) +install(TARGETS cskeys DESTINATION ${ENGINE_BINDIR} COMPONENT columnstore-engine) + diff --git a/tools/passwd/cskeys.cpp b/tools/passwd/cskeys.cpp new file mode 100644 index 000000000..3a3295e95 --- /dev/null +++ b/tools/passwd/cskeys.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2016 MariaDB Corporation Ab + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file and at www.mariadb.com/bsl11. + * + * Change Date: 2025-05-25 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2 or later of the General + * Public License. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "secrets.h" +#include "mcsconfig.h" + +using std::string; +using ByteVec = std::vector; + +struct option options[] = +{ + {"help", no_argument, nullptr, 'h'}, + {"user", required_argument, nullptr, 'u'}, + {nullptr, 0, nullptr, 0 } +}; + +const string default_user = "mysql"; + +ByteVec generate_encryption_key(); + +void print_usage(const char* executable, const char* default_directory) +{ + const char msg[] = + R"(usage: %s [-h|--help] [directory] +This utility generates a random AES encryption key and init vector and writes +them to disk. The data is written to the file '%s', in the specified +directory. The key and init vector are used by the utility 'cspasswd' to +encrypt passwords used in Columnstore configuration files, as well as by Columnstore +itself to decrypt the passwords. +Re-creating the file invalidates all existing encrypted passwords in the +configuration files. + -h, --help Display this help + -u, --user Designate the owner of the generated file (default: '%s') + directory : The directory where to store the file in (default: '%s') +)"; + printf(msg, executable, SECRETS_FILENAME, default_user.c_str(), default_directory); +} + +int main(int argc, char** argv) +{ + CSPasswdLogging* keysLog = CSPasswdLogging::get(); + const string default_directory = string(MCSDATADIR); + string username = default_user; + + int c; + while ((c = getopt_long(argc, argv, "hu:", options, nullptr)) != -1) + { + switch (c) + { + case 'h': + print_usage(argv[0], default_directory.c_str()); + return EXIT_SUCCESS; + + case 'u': + username = optarg; + break; + + default: + print_usage(argv[0], default_directory.c_str()); + return EXIT_FAILURE; + } + } + + string filepath = default_directory; + if (optind < argc) + { + filepath = argv[optind]; + } + filepath.append("/").append(SECRETS_FILENAME); + + // Check that the file doesn't exist. + errno = 0; + auto filepathc = filepath.c_str(); + if (access(filepathc, F_OK) == 0) + { + printf("Secrets file '%s' already exists. Delete it before generating a new encryption key.\n", + filepathc); + return EXIT_FAILURE; + } + else if (errno != ENOENT) + { + printf("stat() for secrets file '%s' failed unexpectedly. Error %i, %s.\n", + filepathc, errno, strerror(errno)); + return EXIT_FAILURE; + } + + int rval = EXIT_FAILURE; + auto new_key = generate_encryption_key(); + if (!new_key.empty() && secrets_write_keys(new_key, filepath, username)) + { + rval = EXIT_SUCCESS; + } + return rval; +} + +ByteVec generate_encryption_key() +{ + int keylen = EVP_CIPHER_key_length(secrets_cipher()); + ByteVec key(keylen); + // Generate random bytes using OpenSSL. + if (RAND_bytes(key.data(), keylen) != 1) + { + auto errornum = ERR_get_error(); + printf("OpenSSL RAND_bytes() failed. %s.\n", ERR_error_string(errornum, nullptr)); + key.clear(); + } + return key; +} diff --git a/tools/passwd/cspasswd.cpp b/tools/passwd/cspasswd.cpp new file mode 100644 index 000000000..d7af80d98 --- /dev/null +++ b/tools/passwd/cspasswd.cpp @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2016 MariaDB Corporation Ab + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file and at www.mariadb.com/bsl11. + * + * Change Date: 2025-05-25 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2 or later of the General + * Public License. + */ + +/** + * @file cspasswd.cpp - Implementation of pasword encoding + * Modified from MariaDB internal implementations + */ + +#include +#include +#include +#include + +#include + +#include "secrets.h" +#include "mcsconfig.h" + +using std::cin; +using std::cout; +using std::endl; +using std::flush; +using std::string; + +struct option options[] = +{ + {"help", no_argument, nullptr, 'h'}, + {"decrypt", no_argument, nullptr, 'd'}, + {"interactive", no_argument, nullptr, 'i'}, + {nullptr, 0, nullptr, 0 } +}; + +void print_usage(const char* executable, const char* directory) +{ + const char msg[] = + R"(Usage: %s [-h|--help] [-i|--interactive] [-d|--decrypt] [path] password +Encrypt a Columnstore plaintext password using the encryption key in the key file +'%s'. The key file may be generated using the 'cskeys'-utility. + -h, --help Display this help. + -d, --decrypt Decrypt an encrypted password instead. + -i, --interactive - If cspasswd is reading from a pipe, it will read a line and + use that as the password. + - If cspasswd is connected to a terminal console, it will prompt + for the password. + If '-i' is specified, a single argument is assumed to be the path + and two arguments is treated like an error. + path The key file directory (default: '%s') + password The password to encrypt or decrypt +)"; + + printf(msg, executable, SECRETS_FILENAME, directory); +} + +bool read_password(string* pPassword) +{ + bool rv = false; + string password; + + if (isatty(STDIN_FILENO)) + { + struct termios tty; + tcgetattr(STDIN_FILENO, &tty); + + bool echo = (tty.c_lflag & ECHO); + if (echo) + { + tty.c_lflag &= ~ECHO; + tcsetattr(STDIN_FILENO, TCSANOW, &tty); + } + + cout << "Enter password : " << flush; + string s1; + std::getline(std::cin, s1); + cout << endl; + + cout << "Repeat password: " << flush; + string s2; + std::getline(std::cin, s2); + cout << endl; + + if (echo) + { + tty.c_lflag |= ECHO; + tcsetattr(STDIN_FILENO, TCSANOW, &tty); + } + + if (s1 == s2) + { + password = s1; + rv = true; + } + else + { + cout << "Passwords are not identical." << endl; + } + } + else + { + std::getline(std::cin, password); + rv = true; + } + + if (rv) + { + *pPassword = password; + } + + return rv; +} + + +int main(int argc, char** argv) +{ + std::ios::sync_with_stdio(); + + CSPasswdLogging* passwdLog = CSPasswdLogging::get(); + const string default_directory = string(MCSDATADIR); + + enum class Mode + { + ENCRYPT, + DECRYPT + }; + + auto mode = Mode::ENCRYPT; + bool interactive = false; + + int c; + while ((c = getopt_long(argc, argv, "hdi", options, NULL)) != -1) + { + switch (c) + { + case 'h': + print_usage(argv[0], default_directory.c_str()); + return EXIT_SUCCESS; + + case 'd': + mode = Mode::DECRYPT; + break; + + case 'i': + interactive = true; + break; + + default: + print_usage(argv[0], default_directory.c_str()); + return EXIT_FAILURE; + } + } + + string input; + string path = default_directory; + + switch (argc - optind) + { + case 2: + // Two args provided. + path = argv[optind]; + if (!interactive) + { + input = argv[optind + 1]; + } + else + { + print_usage(argv[0], default_directory.c_str()); + return EXIT_FAILURE; + } + break; + + case 1: + // One arg provided. + if (!interactive) + { + input = argv[optind]; + } + else + { + path = argv[optind]; + } + break; + + case 0: + if (!interactive) + { + print_usage(argv[0], default_directory.c_str()); + return EXIT_FAILURE; + } + break; + + default: + print_usage(argv[0], default_directory.c_str()); + return EXIT_FAILURE; + } + + if (interactive) + { + if (!read_password(&input)) + { + return EXIT_FAILURE; + } + } + + int rval = EXIT_FAILURE; + + string filepath = path; + filepath.append("/").append(SECRETS_FILENAME); + + auto keydata = secrets_readkeys(filepath); + if (keydata.ok) + { + bool encrypting = (mode == Mode::ENCRYPT); + bool new_mode = keydata.iv.empty(); // false -> constant IV from file + if (keydata.key.empty()) + { + printf("Password encryption key file '%s' not found, cannot %s password.\n", + filepath.c_str(), encrypting ? "encrypt" : "decrypt"); + } + else if (encrypting) + { + string encrypted = new_mode ? encrypt_password(keydata.key, input) : + encrypt_password_old(keydata.key, keydata.iv, input); + if (!encrypted.empty()) + { + printf("%s\n", encrypted.c_str()); + rval = EXIT_SUCCESS; + } + else + { + printf("Password encryption failed.\n"); + } + } + else + { + auto is_hex = std::all_of(input.begin(), input.end(), isxdigit); + if (is_hex && input.length() % 2 == 0) + { + string decrypted = new_mode ? decrypt_password(keydata.key, input) : + decrypt_password_old(keydata.key, keydata.iv, input); + if (!decrypted.empty()) + { + printf("%s\n", decrypted.c_str()); + rval = EXIT_SUCCESS; + } + else + { + printf("Password decryption failed.\n"); + } + } + else + { + printf("Input is not a valid hex-encoded encrypted password.\n"); + } + } + } + else + { + printf("Could not read encryption key file '%s'.\n", filepath.c_str()); + } + return rval; +} diff --git a/tools/passwd/secrets.cpp b/tools/passwd/secrets.cpp new file mode 100644 index 000000000..58aae7df9 --- /dev/null +++ b/tools/passwd/secrets.cpp @@ -0,0 +1,693 @@ +/* + * Copyright (c) 2016 MariaDB Corporation Ab + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file and at www.mariadb.com/bsl11. + * + * Change Date: 2025-05-25 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2 or later of the General + * Public License. + */ +#include "secrets.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BOOST_SPIRIT_THREADSAFE +#include +#include +#include "idberrorinfo.h" +#include "logger.h" +#include "mcsconfig.h" +#include "exceptclasses.h" +#include "columnstoreversion.h" + +using std::string; + +const char* const SECRETS_FILENAME = ".secrets"; + + +namespace +{ +CSPasswdLogging *passwdLog = NULL; +boost::mutex m; +} + +CSPasswdLogging::CSPasswdLogging() +{ + //TODO: make this configurable + setlogmask (LOG_UPTO (LOG_DEBUG)); + openlog ("CSPasswd", LOG_PID | LOG_NDELAY | LOG_PERROR | LOG_CONS, LOG_LOCAL1); +} + +CSPasswdLogging::~CSPasswdLogging() +{ + syslog(LOG_INFO, "CloseLog"); + closelog(); +} + +CSPasswdLogging * CSPasswdLogging::get() +{ + if (passwdLog) + return passwdLog; + boost::mutex::scoped_lock s(m); + if (passwdLog) + return passwdLog; + passwdLog = new CSPasswdLogging(); + return passwdLog; +} + +void CSPasswdLogging::log(int priority,const char *format, ...) +{ + va_list args; + va_start(args, format); + + #ifdef DEBUG + va_list args2; + va_copy(args2, args); + vfprintf(stderr, format, args2); + fprintf(stderr, "\n"); + va_end(args2); + #endif + vsyslog(priority, format, args); + + va_end(args); +} + +namespace +{ + +using HexLookupTable = std::array; +HexLookupTable init_hex_lookup_table() noexcept; + +// Hex char -> byte val lookup table. +const HexLookupTable hex_lookup_table = init_hex_lookup_table(); + +/* used in the bin2hex function */ +const char hex_upper[] = "0123456789ABCDEF"; +const char hex_lower[] = "0123456789abcdef"; + +HexLookupTable init_hex_lookup_table() noexcept +{ + auto char_val = [](char c) -> uint8_t { + if (c >= '0' && c <= '9') + { + return c - '0'; + } + else if (c >= 'A' && c <= 'F') + { + return c - 'A' + 10; + } + else if (c >= 'a' && c <= 'f') + { + return c - 'a' + 10; + } + else + { + return '\177'; + } + }; + + HexLookupTable rval; + for (size_t i = 0; i < rval.size(); i++) + { + rval[i] = char_val(i); + } + return rval; +} + +bool hex2bin(const char* in, unsigned int in_len, uint8_t* out) +{ + // Input length must be multiple of two. + if (!in || in_len == 0 || in_len % 2 != 0) + { + return false; + } + + const char* in_end = in + in_len; + while (in < in_end) + { + // One byte is formed from two hex chars, with the first char forming the high bits. + uint8_t high_half = hex_lookup_table[*in++]; + uint8_t low_half = hex_lookup_table[*in++]; + uint8_t total = (high_half << 4) | low_half; + *out++ = total; + } + return true; +} + +char* bin2hex(const uint8_t* in, unsigned int len, char* out) +{ + const uint8_t* in_end = in + len; + if (len == 0 || in == NULL) + { + return NULL; + } + + for (; in != in_end; ++in) + { + *out++ = hex_upper[((uint8_t) * in) >> 4]; + *out++ = hex_upper[((uint8_t) * in) & 0x0F]; + } + *out = '\0'; + + return out; +} + +struct ThisUnit +{ + ByteVec key; /**< Password decryption key, assigned at startup */ + ByteVec iv; /**< Decryption init vector, assigned at startup. Only used with old-format keys */ +}; +ThisUnit this_unit; + +enum class ProcessingMode +{ + ENCRYPT, + DECRYPT, + DECRYPT_IGNORE_ERRORS +}; + +const char field_desc[] = "description"; +const char field_version[] = "columnstore_version"; +const char field_cipher[] = "encryption_cipher"; +const char field_key[] = "encryption_key"; +const char desc[] = "Columnstore encryption/decryption key"; + +#define SECRETS_CIPHER EVP_aes_256_cbc +#define STRINGIFY(X) #X +#define STRINGIFY2(X) STRINGIFY(X) +const char CIPHER_NAME[] = STRINGIFY2(SECRETS_CIPHER); + +void print_openSSL_errors(const char* operation); + +/** + * Encrypt or decrypt the input buffer to output buffer. + * + * @param key Encryption key + * @param mode Encrypting or decrypting + * @param input Input buffer + * @param input_len Input length + * @param output Output buffer + * @param output_len Produced output length is written here + * @return True on success + */ +bool encrypt_or_decrypt(const uint8_t* key, const uint8_t* iv, ProcessingMode mode, + const uint8_t* input, int input_len, uint8_t* output, int* output_len) +{ + auto ctx = EVP_CIPHER_CTX_new(); + int enc = (mode == ProcessingMode::ENCRYPT) ? AES_ENCRYPT : AES_DECRYPT; + bool ignore_errors = (mode == ProcessingMode::DECRYPT_IGNORE_ERRORS); + bool ok = false; + + if (EVP_CipherInit_ex(ctx, secrets_cipher(), nullptr, key, iv, enc) == 1 || ignore_errors) + { + int output_written = 0; + if (EVP_CipherUpdate(ctx, output, &output_written, input, input_len) == 1 || ignore_errors) + { + int total_output_len = output_written; + if (EVP_CipherFinal_ex(ctx, output + total_output_len, &output_written) == 1 || ignore_errors) + { + total_output_len += output_written; + *output_len = total_output_len; + ok = true; + } + } + } + + EVP_CIPHER_CTX_free(ctx); + if (!ok) + { + const char* operation = (mode == ProcessingMode::ENCRYPT) ? "when encrypting password" : + "when decrypting password"; + print_openSSL_errors(operation); + } + return ok; +} + +void print_openSSL_errors(const char* operation) +{ + // It's unclear how thread(unsafe) OpenSSL error functions are. Minimize such possibilities by + // using a local buffer. + constexpr size_t bufsize = 256; // Should be enough according to some googling. + char buf[bufsize]; + buf[0] = '\0'; + + auto errornum = ERR_get_error(); + auto errornum2 = ERR_get_error(); + ERR_error_string_n(errornum, buf, bufsize); + + if (errornum2 == 0) + { + // One error. + CSPasswdLogging::get()->log(LOG_ERR,"OpenSSL error %s. %s", operation, buf); + } + else + { + // Multiple errors, print all as separate messages. + CSPasswdLogging::get()->log(LOG_ERR,"Multiple OpenSSL errors %s. Detailed messages below.", operation); + CSPasswdLogging::get()->log(LOG_ERR,"%s", buf); + while (errornum2 != 0) + { + ERR_error_string_n(errornum2, buf, bufsize); + CSPasswdLogging::get()->log(LOG_ERR,"%s", buf); + errornum2 = ERR_get_error(); + } + } +} +} + +const EVP_CIPHER* secrets_cipher() +{ + return SECRETS_CIPHER(); +} + +int secrets_keylen() +{ + return EVP_CIPHER_key_length(secrets_cipher()); +} + +int secrets_ivlen() +{ + return EVP_CIPHER_iv_length(secrets_cipher()); +} + +/** + * Reads binary or text data from file and extracts the encryption key and, if old key format is used, + * the init vector. The source file needs to have expected permissions. If the source file does not exist, + * returns empty results. + * + * @param filepath Path to key file. + * @return Result structure. Ok if file was loaded successfully or if file did not exist. False on error. + */ +ReadKeyResult secrets_readkeys(const string& filepath) +{ + ReadKeyResult rval; + auto filepathc = filepath.c_str(); + + const int binary_key_len = secrets_keylen(); + const int binary_iv_len = secrets_ivlen(); + const int binary_total_len = binary_key_len + binary_iv_len; + + // Before opening the file, check its size and permissions. + struct stat filestats { 0 }; + bool stat_error = false; + bool old_format = false; + errno = 0; + if (stat(filepathc, &filestats) == 0) + { + auto filesize = filestats.st_size; + if (filesize == binary_total_len) + { + old_format = true; + CSPasswdLogging::get()->log(LOG_WARNING,"File format of '%s' is deprecated. Please generate a new encryption key ('maxkeys') " + "and re-encrypt passwords ('maxpasswd').", filepathc); + } + + auto filemode = filestats.st_mode; + if (!S_ISREG(filemode)) + { + CSPasswdLogging::get()->log(LOG_ERR,"Secrets file '%s' is not a regular file.", filepathc); + stat_error = true; + } + else if ((filemode & (S_IRWXU | S_IRWXG | S_IRWXO)) != S_IRUSR) + { + CSPasswdLogging::get()->log(LOG_ERR,"Secrets file '%s' permissions are wrong. The only permission on the file should be " + "owner:read.", filepathc); + stat_error = true; + } + } + else if (errno == ENOENT) + { + // The file does not exist. This is ok. Return empty result. + rval.ok = true; + return rval; + } + else + { + CSPasswdLogging::get()->log(LOG_ERR,"stat() for secrets file '%s' failed. Error %d, %s.", + filepathc, errno, strerror(errno)); + stat_error = true; + } + + if (stat_error) + { + return rval; + } + + if (old_format) + { + errno = 0; + std::ifstream file(filepath, std::ios_base::binary); + if (file.is_open()) + { + // Read all data from file. + char readbuf[binary_total_len]; + file.read(readbuf, binary_total_len); + if (file.good()) + { + // Success, copy contents to key structure. + rval.key.assign(readbuf, readbuf + binary_key_len); + rval.iv.assign(readbuf + binary_key_len, readbuf + binary_total_len); + rval.ok = true; + } + else + { + CSPasswdLogging::get()->log(LOG_ERR,"Read from secrets file %s failed. Read %li, expected %i bytes. Error %d, %s.", + filepathc, file.gcount(), binary_total_len, errno, strerror(errno)); + } + } + else + { + CSPasswdLogging::get()->log(LOG_ERR,"Could not open secrets file '%s'. Error %d, %s.", + filepathc, errno, strerror(errno)); + } + } + else + { + // File contents should be json. + //json_error_t err; + boost::property_tree::ptree jsontree; + try + { + boost::property_tree::read_json(filepath, jsontree); + } + catch(boost::property_tree::json_parser::json_parser_error &je) + { + std::cout << "Error reading JSON from secrets file: " << je.filename() << " on line: " << je.line() << std::endl; + std::cout << je.message() << std::endl; + } + catch(...) + { + printf("Error reading JSON from secrets file '%s' failed. Error %d, %s.\n", + filepathc, errno, strerror(errno)); + } + //json_t* obj = json_load_file(filepathc, 0, &err); + string enc_cipher = jsontree.get(field_cipher); + string enc_key = jsontree.get(field_key); + //const char* enc_cipher = json_string_value(json_object_get(obj, field_cipher)); + //const char* enc_key = json_string_value(json_object_get(obj, field_key)); + bool cipher_ok = !enc_cipher.empty() && (enc_cipher == CIPHER_NAME); + if (cipher_ok && !enc_key.empty()) + { + int read_hex_key_len = enc_key.length(); + int expected_hex_key_len = 2 * binary_key_len; + if (read_hex_key_len == expected_hex_key_len) + { + rval.key.resize(binary_key_len); + hex2bin(enc_key.c_str(), read_hex_key_len, rval.key.data()); + rval.ok = true; + } + else + { + CSPasswdLogging::get()->log(LOG_ERR,"Wrong encryption key length in secrets file '%s': found %i, expected %i.", + filepathc, read_hex_key_len, expected_hex_key_len); + } + } + else + { + CSPasswdLogging::get()->log(LOG_ERR,"Secrets file '%s' does not contain expected string fields '%s' and '%s', " + "or '%s' is not '%s'.", + filepathc, field_cipher, field_key, field_cipher, CIPHER_NAME); + } + jsontree.clear(); + } + return rval; +} + +string decrypt_password(const string& input) +{ + const auto& key = this_unit.key; + string rval; + if (key.empty()) + { + // Password encryption is not used, return original. + rval = input; + } + else + { + // If the input is not a HEX string, return the input as is. + auto is_hex = std::all_of(input.begin(), input.end(), isxdigit); + if (is_hex) + { + const auto& iv = this_unit.iv; + rval = iv.empty() ? ::decrypt_password(key, input) : decrypt_password_old(key, iv, input); + } + else + { + rval = input; + } + } + return rval; +} + +/** + * Decrypt passwords encrypted with an old (pre 2.5) .secrets-file. The decryption also depends on whether + * the password was encrypted using maxpasswd 2.4 or 2.5. + * + * @param key Encryption key + * @param iv Init vector + * @param input Encrypted password in hex form + * @return Decrypted password or empty on error + */ +string decrypt_password_old(const ByteVec& key, const ByteVec& iv, const std::string& input) +{ + string rval; + // Convert to binary. + size_t hex_len = input.length(); + auto bin_len = hex_len / 2; + unsigned char encrypted_bin[bin_len]; + hex2bin(input.c_str(), hex_len, encrypted_bin); + + unsigned char plain[bin_len]; // Decryption output cannot be longer than input data. + int decrypted_len = 0; + if (encrypt_or_decrypt(key.data(), iv.data(), ProcessingMode::DECRYPT_IGNORE_ERRORS, encrypted_bin, + bin_len, plain, &decrypted_len)) + { + if (decrypted_len > 0) + { + // Success, password was encrypted using 2.5. Decrypted data should be text. + auto output_data = reinterpret_cast(plain); + rval.assign(output_data, decrypted_len); + } + else + { + // Failure, password was likely encrypted in 2.4. Try to decrypt using 2.4 code. + AES_KEY aeskey; + AES_set_decrypt_key(key.data(), 8 * key.size(), &aeskey); + auto iv_copy = iv; + memset(plain, '\0', bin_len); + AES_cbc_encrypt(encrypted_bin, plain, bin_len, &aeskey, iv_copy.data(), AES_DECRYPT); + rval = reinterpret_cast(plain); + } + } + return rval; +} + +string decrypt_password(const ByteVec& key, const std::string& input) +{ + int total_hex_len = input.length(); + string rval; + + // Extract IV. + auto ptr = input.data(); + int iv_bin_len = secrets_ivlen(); + int iv_hex_len = 2 * iv_bin_len; + uint8_t iv_bin[iv_bin_len]; + if (total_hex_len >= iv_hex_len) + { + hex2bin(ptr, iv_hex_len, iv_bin); + + int encrypted_hex_len = total_hex_len - iv_hex_len; + int encrypted_bin_len = encrypted_hex_len / 2; + unsigned char encrypted_bin[encrypted_bin_len]; + hex2bin(ptr + iv_hex_len, encrypted_hex_len, encrypted_bin); + + uint8_t decrypted[encrypted_bin_len]; // Decryption output cannot be longer than input data. + int decrypted_len = 0; + if (encrypt_or_decrypt(key.data(), iv_bin, ProcessingMode::DECRYPT, encrypted_bin, encrypted_bin_len, + decrypted, &decrypted_len)) + { + // Decrypted data should be text. + auto output_data = reinterpret_cast(decrypted); + rval.assign(output_data, decrypted_len); + } + } + + return rval; +} + +/** + * Encrypt a password that can be stored in the Columnstore configuration file. + * + * @param key Encryption key and init vector + * @param input The plaintext password to encrypt. + * @return The encrypted password, or empty on failure. + */ +string encrypt_password_old(const ByteVec& key, const ByteVec& iv, const string& input) +{ + string rval; + // Output can be a block length longer than input. + auto input_len = input.length(); + unsigned char encrypted_bin[input_len + AES_BLOCK_SIZE]; + + // Although input is text, interpret as binary. + auto input_data = reinterpret_cast(input.c_str()); + int encrypted_len = 0; + if (encrypt_or_decrypt(key.data(), iv.data(), ProcessingMode::ENCRYPT, + input_data, input_len, encrypted_bin, &encrypted_len)) + { + int hex_len = 2 * encrypted_len; + char hex_output[hex_len + 1]; + bin2hex(encrypted_bin, encrypted_len, hex_output); + rval.assign(hex_output, hex_len); + } + return rval; +} + +string encrypt_password(const ByteVec& key, const string& input) +{ + string rval; + // Generate random IV. + auto ivlen = secrets_ivlen(); + unsigned char iv_bin[ivlen]; + if (RAND_bytes(iv_bin, ivlen) != 1) + { + printf("OpenSSL RAND_bytes() failed. %s.\n", ERR_error_string(ERR_get_error(), nullptr)); + return rval; + } + + // Output can be a block length longer than input. + auto input_len = input.length(); + unsigned char encrypted_bin[input_len + EVP_CIPHER_block_size(secrets_cipher())]; + + // Although input is text, interpret as binary. + auto input_data = reinterpret_cast(input.c_str()); + int encrypted_len = 0; + if (encrypt_or_decrypt(key.data(), iv_bin, ProcessingMode::ENCRYPT, + input_data, input_len, encrypted_bin, &encrypted_len)) + { + // Form one string with IV in front. + int iv_hex_len = 2 * ivlen; + int encrypted_hex_len = 2 * encrypted_len; + int total_hex_len = iv_hex_len + encrypted_hex_len; + char hex_output[total_hex_len + 1]; + bin2hex(iv_bin, ivlen, hex_output); + bin2hex(encrypted_bin, encrypted_len, hex_output + iv_hex_len); + rval.assign(hex_output, total_hex_len); + } + return rval; +} + +bool load_encryption_keys() +{ + if(this_unit.key.empty() || this_unit.iv.empty()) + { + string path(MCSDATADIR); + path.append("/").append(SECRETS_FILENAME); + auto ret = secrets_readkeys(path); + if (ret.ok) + { + if (!ret.key.empty()) + { + CSPasswdLogging::get()->log(LOG_INFO,"Using encrypted passwords. Encryption key read from '%s'.", path.c_str()); + this_unit.key = move(ret.key); + this_unit.iv = move(ret.iv); + } + else + { + CSPasswdLogging::get()->log(LOG_INFO,"Password encryption key file '%s' not found, using configured passwords as " + "plaintext.", path.c_str()); + } + return ret.ok; + } + } + return true; +} + +/** + * Write encryption key to JSON-file. Also sets file permissions and owner. + * + * @param key Encryption key in binary form + * @param filepath The full path to the file to write to + * @param owner The final owner of the file. Changing the owner does not always succeed. + * @return True on total success. Even if false is returned, the file may have been written. + */ +bool secrets_write_keys(const ByteVec& key, const string& filepath, const string& owner) +{ + auto keylen = key.size(); + char key_hex[2 * keylen + 1]; + bin2hex(key.data(), keylen, key_hex); + + boost::property_tree::ptree jsontree; + jsontree.put(field_desc,desc); + jsontree.put(field_version,columnstore_version.c_str()); + jsontree.put(field_cipher,CIPHER_NAME); + jsontree.put(field_key,(const char*)key_hex); + + auto filepathc = filepath.c_str(); + bool write_ok = false; + errno = 0; + try + { + write_json(filepathc, jsontree); + } + catch(boost::property_tree::json_parser::json_parser_error &je) + { + std::cout << "Write to secrets file: " << je.filename() << " on line: " << je.line() << std::endl; + std::cout << je.message() << std::endl; + } + catch(...) + { + printf("Write to secrets file '%s' failed. Error %d, %s.\n", + filepathc, errno, strerror(errno)); + } + write_ok = true; + + jsontree.clear(); + bool rval = false; + if (write_ok) + { + // Change file permissions to prevent modifications. + errno = 0; + if (chmod(filepathc, S_IRUSR) == 0) + { + printf("Permissions of '%s' set to owner:read.\n", filepathc); + auto ownerz = owner.c_str(); + auto userinfo = getpwnam(ownerz); + if (userinfo) + { + if (chown(filepathc, userinfo->pw_uid, userinfo->pw_gid) == 0) + { + printf("Ownership of '%s' given to %s.\n", filepathc, ownerz); + rval = true; + } + else + { + printf("Failed to give '%s' ownership of '%s': %d, %s.\n", + ownerz, filepathc, errno, strerror(errno)); + } + } + else + { + printf("Could not find user '%s' when attempting to change ownership of '%s': %d, %s.\n", + ownerz, filepathc, errno, strerror(errno)); + } + } + else + { + printf("Failed to change the permissions of the secrets file '%s'. Error %d, %s.\n", + filepathc, errno, strerror(errno)); + } + } + return rval; +} diff --git a/tools/passwd/secrets.h b/tools/passwd/secrets.h new file mode 100644 index 000000000..781583ece --- /dev/null +++ b/tools/passwd/secrets.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2018 MariaDB Corporation Ab + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file and at www.mariadb.com/bsl11. + * + * Change Date: 2025-05-25 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2 or later of the General + * Public License. + */ +#include +#include +#include "idberrorinfo.h" +#include "logger.h" + +class CSPasswdLogging +{ + public: + static CSPasswdLogging *get(); + ~CSPasswdLogging(); + + void log(int priority, const char *format, ...); + private: + CSPasswdLogging(); +}; + +/** + * Decrypt an encrypted password using the key loaded at startup. If the password is not encrypted, + * ie is not a HEX string, return the original. + * + * @param input The encrypted password + * @return The decrypted password. + */ +std::string decrypt_password(const std::string& input); + +#include + +using ByteVec = std::vector; + +struct evp_cipher_st; +extern const char* const SECRETS_FILENAME; + +/** + * Returns the cipher used for password encryption. + * + * @return Cipher + */ +const evp_cipher_st* secrets_cipher(); + +/** + * Returns encryption key length. + * + * @return Encryption key length + */ +int secrets_keylen(); + +/** + * Returns initialization vector length. + * + * @return initialization vector length + */ +int secrets_ivlen(); + +bool load_encryption_keys(); +std::string encrypt_password_old(const ByteVec& key, const ByteVec& iv, const std::string& input); +std::string encrypt_password(const ByteVec& key, const std::string& input); + +std::string decrypt_password_old(const ByteVec& key, const ByteVec& iv, const std::string& input); +std::string decrypt_password(const ByteVec& key, const std::string& input); + +struct ReadKeyResult +{ + bool ok {false}; + ByteVec key; + ByteVec iv; +}; + +ReadKeyResult secrets_readkeys(const std::string& filepath); +bool secrets_write_keys(const ByteVec& key, const std::string& filepath, const std::string& owner);