diff options
Diffstat (limited to 'plugin/aws_key_management')
-rw-r--r-- | plugin/aws_key_management/CMakeLists.txt | 17 | ||||
-rw-r--r-- | plugin/aws_key_management/aws_key_management_plugin.cc | 768 |
2 files changed, 785 insertions, 0 deletions
diff --git a/plugin/aws_key_management/CMakeLists.txt b/plugin/aws_key_management/CMakeLists.txt new file mode 100644 index 00000000..3c6ca018 --- /dev/null +++ b/plugin/aws_key_management/CMakeLists.txt @@ -0,0 +1,17 @@ +INCLUDE(aws_sdk) +CHECK_AWS_SDK(HAVE_AWS_SDK REASON) +IF(NOT HAVE_AWS_SDK) + MESSAGE_ONCE(AWS_KEY_MANAGEMENT_NO_AWS_SDK "Can't build aws_key_management - AWS SDK not available (${REASON})") + ADD_FEATURE_INFO(AWS_KEY_MANAGEMENT "OFF" "AWS Encryption Key Management Plugin") + RETURN() +ENDIF() + +MYSQL_ADD_PLUGIN(aws_key_management + aws_key_management_plugin.cc + COMPONENT aws-key-management) + +IF(TARGET aws_key_management) + USE_AWS_SDK_LIBS(aws_key_management kms) +ENDIF() + +ADD_FEATURE_INFO(AWS_KEY_MANAGEMENT "ON" "AWS Encryption Key Management Plugin") diff --git a/plugin/aws_key_management/aws_key_management_plugin.cc b/plugin/aws_key_management/aws_key_management_plugin.cc new file mode 100644 index 00000000..7740c2ea --- /dev/null +++ b/plugin/aws_key_management/aws_key_management_plugin.cc @@ -0,0 +1,768 @@ +/* + Copyright (c) 2016 MariaDB Corporation + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + + +#include <my_global.h> +#include <typelib.h> +#include <mysql/plugin_encryption.h> +#include <my_crypt.h> +#include <string.h> +#include <stdio.h> +#include <stdlib.h> +#include <mysqld_error.h> +#include <my_sys.h> +#include <map> +#include <algorithm> +#include <string> +#include <vector> +#include <iterator> +#include <sstream> +#include <fstream> + +#ifndef _WIN32 +#include <dirent.h> +#endif + +#include <aws/core/Aws.h> +#include <aws/core/client/AWSError.h> +#include <aws/core/utils/logging/AWSLogging.h> +#include <aws/core/utils/logging/ConsoleLogSystem.h> +#include <aws/kms/KMSClient.h> +#include <aws/kms/model/DecryptRequest.h> +#include <aws/kms/model/DecryptResult.h> +#include <aws/kms/model/GenerateDataKeyWithoutPlaintextRequest.h> +#include <aws/kms/model/GenerateDataKeyWithoutPlaintextResult.h> +#include <aws/core/utils/Outcome.h> + +using namespace std; +using namespace Aws::KMS; +using namespace Aws::KMS::Model; +using namespace Aws::Utils::Logging; + + +/* Plaintext key info struct */ +struct KEY_INFO +{ + unsigned int key_id; + unsigned int key_version; + unsigned int length; + unsigned char data[MY_AES_MAX_KEY_LENGTH]; + bool load_failed; /* if true, do not attempt to reload?*/ +public: + KEY_INFO() : key_id(0), key_version(0), length(0), load_failed(false){}; +}; +#define KEY_ID_AND_VERSION(key_id,version) ((longlong)key_id << 32 | version) + +/* Cache for the latest version, per key id */ +static std::map<uint, uint> latest_version_cache; + +/* Cache for plaintext keys */ +static std::map<ulonglong, KEY_INFO> key_info_cache; + +static const char *key_spec_names[]={ "AES_128", "AES_256", 0 }; + +/* Plugin variables */ +static char* master_key_id; +static char* region; +static unsigned long key_spec; +static unsigned long log_level; +static int rotate_key; +static int request_timeout; +static char* endpoint_url; + +#ifndef DBUG_OFF +#define WITH_AWS_MOCK 1 +#else +#define WITH_AWS_MOCK 0 +#endif + +#if WITH_AWS_MOCK +static char mock; +#endif + + +/* AWS functionality*/ +static int read_and_decrypt_key(const char *path, KEY_INFO *info); +static int generate_and_save_datakey(uint key_id, uint version); + +static int extract_id_and_version(const char *name, uint *id, uint *ver); +static unsigned int get_latest_key_version(unsigned int key_id); +static unsigned int get_latest_key_version_nolock(unsigned int key_id); +static int load_key(KEY_INFO *info); +static std::mutex mtx; + + +static Aws::KMS::KMSClient *client; + +static void print_kms_error(const char *func, const Aws::Client::AWSError<Aws::KMS::KMSErrors>& err) +{ + my_printf_error(ER_UNKNOWN_ERROR, + "AWS KMS plugin : KMS Client API '%s' failed : %s - %s", + ME_ERROR_LOG_ONLY, + func, err.GetExceptionName().c_str(), err.GetMessage().c_str()); +} + +#if WITH_AWS_MOCK +/* + Mock routines to test plugin without actual AWS KMS interaction + we only need to mock 2 functions - generating encrypted key, and decrypt + + This mock functions do no-op encryption, i.e encrypt and decrypt of + a buffer return the buffer itself. +*/ + +/* + Generate random "encrypted" key. We do not encrypt anything in mock mode. +*/ +static int mock_generate_encrypted_key(Aws::Utils::ByteBuffer *result) +{ + size_t len = key_spec == 0?16 : 32; + *result = Aws::Utils::ByteBuffer(len); + my_random_bytes(result->GetUnderlyingData(), (int)len); + return 0; +} + + +static int mock_decrypt(Aws::Utils::ByteBuffer input, Aws::Utils::ByteBuffer* output) +{ + /* We do not encrypt or decrypt in mock mode.*/ + *output = input; + return 0; +} +#endif + +/* Redirect AWS trace to error log */ +class MySQLLogSystem : public Aws::Utils::Logging::FormattedLogSystem +{ +public: + + using Base = FormattedLogSystem; + MySQLLogSystem(LogLevel logLevel) : + Base(logLevel) + { + } + virtual LogLevel GetLogLevel(void) const override + { + return (LogLevel)log_level; + } + virtual ~MySQLLogSystem() + { + } + + virtual void Flush(void) override + { + } + +protected: + virtual void ProcessFormattedStatement(Aws::String&& statement) override + { +#ifdef _WIN32 + /* + On Windows, we can't use C runtime functions to write to stdout, + because we compile with static C runtime, so plugin has a stdout + different from server. Thus we're using WriteFile(). + */ + DWORD nSize= (DWORD)statement.size(); + DWORD nWritten; + const char *s= statement.c_str(); + HANDLE h= GetStdHandle(STD_OUTPUT_HANDLE); + + WriteFile(h, s, nSize, &nWritten, NULL); +#else + printf("%s", statement.c_str()); +#endif + } +}; + +/* Get list of files in current directory */ +static vector<string> traverse_current_directory() +{ + vector<string> v; +#ifdef _WIN32 + WIN32_FIND_DATA find_data; + HANDLE h= FindFirstFile("*.*", &find_data); + if (h == INVALID_HANDLE_VALUE) + return v; + do + { + v.push_back(find_data.cFileName); + } + while (FindNextFile(h, &find_data)); + FindClose(h); +#else + DIR *dir = opendir("."); + if (!dir) + return v; + struct dirent *e; + while ((e= readdir(dir))) + v.push_back(e->d_name); + closedir(dir); +#endif + return v; +} + +Aws::SDKOptions sdkOptions; + +static int aws_init() +{ + +#ifdef HAVE_WOLFSSL + sdkOptions.cryptoOptions.initAndCleanupOpenSSL = true; +#else + /* Server initialized OpenSSL already, thus AWS must skip it */ + sdkOptions.cryptoOptions.initAndCleanupOpenSSL = false; +#endif + + Aws::InitAPI(sdkOptions); + InitializeAWSLogging(Aws::MakeShared<MySQLLogSystem>("aws_key_management_plugin", (Aws::Utils::Logging::LogLevel) log_level)); + + Aws::Client::ClientConfiguration clientConfiguration; + if (region && region[0]) + { + clientConfiguration.region = region; + } + if (endpoint_url && endpoint_url[0]) + { + clientConfiguration.endpointOverride = endpoint_url; + } + if (request_timeout) + { + clientConfiguration.requestTimeoutMs= request_timeout; + clientConfiguration.connectTimeoutMs= request_timeout; + } + client = new KMSClient(clientConfiguration); + if (!client) + { + my_printf_error(ER_UNKNOWN_ERROR, "Can't initialize KMS client", ME_ERROR_LOG_ONLY | ME_WARNING); + return -1; + } + return 0; +} + +static int init() +{ +#if WITH_AWS_MOCK + if(mock) + return 0; +#endif + return aws_init(); +} + +/* + Plugin initialization. + + Create KMS client and scan datadir to find out which keys and versions + are present. +*/ +static int plugin_init(void *p) +{ + if (init()) + return -1; + + vector<string> files= traverse_current_directory(); + for (size_t i=0; i < files.size(); i++) + { + + KEY_INFO info; + if (extract_id_and_version(files[i].c_str(), &info.key_id, &info.key_version) == 0) + { + key_info_cache[KEY_ID_AND_VERSION(info.key_id, info.key_version)]= info; + latest_version_cache[info.key_id]= max(info.key_version, latest_version_cache[info.key_id]); + } + } + return 0; +} + + +static void aws_shutdown() +{ + delete client; + ShutdownAWSLogging(); + Aws::ShutdownAPI(sdkOptions); +} + + +static void shutdown() +{ +#if WITH_AWS_MOCK + if(mock) + return; +#endif + aws_shutdown(); +} + + +static int plugin_deinit(void *p) +{ + latest_version_cache.clear(); + key_info_cache.clear(); + shutdown(); + return 0; +} + +/* Generate filename to store the ciphered key */ +static void format_keyfile_name(char *buf, size_t size, uint key_id, uint version) +{ + snprintf(buf, size, "aws-kms-key.%u.%u", key_id, version); +} + +/* Extract key id and version from file name */ +static int extract_id_and_version(const char *name, uint *id, uint *ver) +{ + int len; + int n= sscanf(name, "aws-kms-key.%u.%u%n", id, ver, &len); + if (n == 2 && *id > 0 && *ver > 0 && len == (int)strlen(name)) + return 0; + return 1; +} + +/* + Decrypt key stored in aws-kms-key.<id>.<version> + Cache the decrypted key. +*/ +static int load_key(KEY_INFO *info) +{ + int ret; + char path[256]; + + format_keyfile_name(path, sizeof(path), info->key_id, info->key_version); + ret= read_and_decrypt_key(path, info); + if (ret) + info->load_failed= true; + + latest_version_cache[info->key_id]= max(latest_version_cache[info->key_id], info->key_version); + key_info_cache[KEY_ID_AND_VERSION(info->key_id, info->key_version)]= *info; + + if (!ret) + { + my_printf_error(ER_UNKNOWN_ERROR, "AWS KMS plugin: loaded key %u, version %u, key length %u bit", ME_ERROR_LOG_ONLY | ME_NOTE, + info->key_id, info->key_version,(uint)info->length*8); + } + else + { + my_printf_error(ER_UNKNOWN_ERROR, "AWS KMS plugin: key %u, version %u could not be decrypted", ME_ERROR_LOG_ONLY | ME_WARNING, + info->key_id, info->key_version); + } + return ret; +} + + +/* + Get latest version for the key. + + If key is not decrypted yet, this function also decrypt the key + and error will be returned if decryption fails. + + The reason for that is that Innodb crashes + in case errors are returned by get_key(), + + A new key will be created if it does not exist, provided there is + valid master_key_id. +*/ +static unsigned int get_latest_key_version(unsigned int key_id) +{ + unsigned int ret; + mtx.lock(); + ret= get_latest_key_version_nolock(key_id); + mtx.unlock(); + return ret; +} + +static unsigned int get_latest_key_version_nolock(unsigned int key_id) +{ + KEY_INFO info; + uint ver; + + ver= latest_version_cache[key_id]; + if (ver > 0) + { + info= key_info_cache[KEY_ID_AND_VERSION(key_id, ver)]; + } + if (info.load_failed) + { + /* Decryption failed previously, don't retry */ + return(ENCRYPTION_KEY_VERSION_INVALID); + } + else if (ver > 0) + { + /* Key exists already, return it*/ + if (info.length > 0) + return(ver); + } + else // (ver == 0) + { + /* Generate a new key, version 1 */ + if (generate_and_save_datakey(key_id, 1) != 0) + return(ENCRYPTION_KEY_VERSION_INVALID); + info.key_id= key_id; + info.key_version= 1; + info.length= 0; + } + + if (load_key(&info)) + return(ENCRYPTION_KEY_VERSION_INVALID); + return(info.key_version); +} + +/* Decrypt Byte buffer with AWS. */ +static int aws_decrypt(Aws::Utils::ByteBuffer input, Aws::Utils::ByteBuffer* output) +{ + DecryptRequest request; + request.SetCiphertextBlob(input); + DecryptOutcome outcome = client->Decrypt(request); + if (!outcome.IsSuccess()) + { + print_kms_error("Decrypt", outcome.GetError()); + return -1; + } + *output= outcome.GetResult().GetPlaintext(); + return 0; +} + + +static int decrypt(Aws::Utils::ByteBuffer input, Aws::Utils::ByteBuffer* output) +{ +#if WITH_AWS_MOCK + if(mock) + return mock_decrypt(input,output); +#endif + return aws_decrypt(input, output); +} + +/* + Decrypt a file with KMS +*/ +static int read_and_decrypt_key(const char *path, KEY_INFO *info) +{ + + /* Read file content into memory */ + ifstream ifs(path, ios::binary | ios::ate); + if (!ifs.good()) + { + my_printf_error(ER_UNKNOWN_ERROR, "can't open file %s", ME_ERROR_LOG_ONLY, path); + return(-1); + } + size_t pos = (size_t)ifs.tellg(); + if (!pos || pos == SIZE_T_MAX) + { + my_printf_error(ER_UNKNOWN_ERROR, "invalid key file %s", ME_ERROR_LOG_ONLY, path); + return(-1); + } + std::vector<char> contents(pos); + ifs.seekg(0, ios::beg); + ifs.read(&contents[0], pos); + + /* Decrypt data the with AWS */ + + Aws::Utils::ByteBuffer input((unsigned char *)contents.data(), pos); + Aws::Utils::ByteBuffer plaintext; + + if (decrypt(input, &plaintext)) + { + return -1; + } + + size_t len = plaintext.GetLength(); + + if (len > sizeof(info->data)) + { + my_printf_error(ER_UNKNOWN_ERROR, "AWS KMS plugin: encoding key too large for %s", ME_ERROR_LOG_ONLY, path); + return(ENCRYPTION_KEY_BUFFER_TOO_SMALL); + } + memcpy(info->data, plaintext.GetUnderlyingData(), len); + info->length= (unsigned int)len; + return(0); +} + + +int aws_generate_encrypted_key(Aws::Utils::ByteBuffer *result) +{ + if (!master_key_id[0]) + { + my_printf_error(ER_UNKNOWN_ERROR, + "Can't generate encryption key, because 'aws_key_management_master_key_id' parameter is not set", + MYF(0)); + return(-1); + } + GenerateDataKeyWithoutPlaintextRequest request; + request.SetKeyId(master_key_id); + request.SetKeySpec(DataKeySpecMapper::GetDataKeySpecForName(key_spec_names[key_spec])); + + GenerateDataKeyWithoutPlaintextOutcome outcome; + outcome= client->GenerateDataKeyWithoutPlaintext(request); + if (!outcome.IsSuccess()) + { + print_kms_error("GenerateDataKeyWithoutPlaintext", outcome.GetError()); + return(-1); + } + *result = outcome.GetResult().GetCiphertextBlob(); + return 0; +} + + +static int generate_encrypted_key(Aws::Utils::ByteBuffer *output) +{ +#if WITH_AWS_MOCK + if(mock) + return mock_generate_encrypted_key(output); +#endif + return aws_generate_encrypted_key(output); +} + +/* Generate a new datakey and store it a file */ +static int generate_and_save_datakey(uint keyid, uint version) +{ + Aws::Utils::ByteBuffer byteBuffer; + + if (generate_encrypted_key(&byteBuffer)) + return -1; + + string out; + char filename[20]; + format_keyfile_name(filename, sizeof(filename), keyid, version); + int fd= open(filename, O_WRONLY |O_CREAT|O_BINARY, IF_WIN(_S_IREAD, S_IRUSR| S_IRGRP| S_IROTH)); + if (fd < 0) + { + my_printf_error(ER_UNKNOWN_ERROR, "AWS KMS plugin: Can't create file %s", ME_ERROR_LOG_ONLY, filename); + return(-1); + } + unsigned int len= (unsigned int)byteBuffer.GetLength(); + if (write(fd, byteBuffer.GetUnderlyingData(), len) != len) + { + my_printf_error(ER_UNKNOWN_ERROR, "AWS KMS plugin: can't write to %s", ME_ERROR_LOG_ONLY, filename); + close(fd); + unlink(filename); + return(-1); + } + close(fd); + my_printf_error(ER_UNKNOWN_ERROR, "AWS KMS plugin: generated encrypted datakey for key id=%u, version=%u", ME_ERROR_LOG_ONLY | ME_NOTE, + keyid, version); + return(0); +} + +/* Key rotation for a single key */ +static int rotate_single_key(uint key_id) +{ + uint ver; + ver= latest_version_cache[key_id]; + + if (!ver) + { + my_printf_error(ER_UNKNOWN_ERROR, "key %u does not exist", MYF(ME_WARNING), key_id); + return -1; + } + else if (generate_and_save_datakey(key_id, ver + 1)) + { + my_printf_error(ER_UNKNOWN_ERROR, "Could not generate datakey for key id= %u, ver= %u", + MYF(ME_WARNING), key_id, ver); + return -1; + } + else + { + KEY_INFO info; + info.key_id= key_id; + info.key_version = ver + 1; + if (load_key(&info)) + { + my_printf_error(ER_UNKNOWN_ERROR, "Could not load datakey for key id= %u, ver= %u", + MYF(ME_WARNING), key_id, ver); + return -1; + } + } + return 0; +} + +/* Key rotation for all key ids */ +static int rotate_all_keys() +{ + int ret= 0; + for (map<uint, uint>::iterator it= latest_version_cache.begin(); it != latest_version_cache.end(); it++) + { + ret= rotate_single_key(it->first); + if (ret) + break; + } + return ret; +} + +static void update_rotate(MYSQL_THD, struct st_mysql_sys_var *, void *, const void *val) +{ + if (!master_key_id[0]) + { + my_printf_error(ER_UNKNOWN_ERROR, + "aws_key_management_master_key_id must be set to generate new data keys", MYF(ME_WARNING)); + return; + } + mtx.lock(); + rotate_key= *(int *)val; + switch (rotate_key) + { + case 0: + break; + case -1: + rotate_all_keys(); + break; + default: + rotate_single_key(rotate_key); + break; + } + rotate_key= 0; + mtx.unlock(); +} + +static unsigned int get_key( + unsigned int key_id, + unsigned int version, + unsigned char* dstbuf, + unsigned int* buflen) +{ + KEY_INFO info; + + mtx.lock(); + info= key_info_cache[KEY_ID_AND_VERSION(key_id, version)]; + if (info.length == 0 && !info.load_failed) + { + info.key_id= key_id; + info.key_version= version; + load_key(&info); + } + mtx.unlock(); + if (info.load_failed) + return(ENCRYPTION_KEY_VERSION_INVALID); + if (*buflen < info.length) + { + *buflen= info.length; + return(ENCRYPTION_KEY_BUFFER_TOO_SMALL); + } + *buflen= info.length; + memcpy(dstbuf, info.data, info.length); + return(0); +} + + +/* Plugin defs */ +struct st_mariadb_encryption aws_key_management_plugin= { + MariaDB_ENCRYPTION_INTERFACE_VERSION, + get_latest_key_version, + get_key, + // use default encrypt/decrypt functions + 0, 0, 0, 0, 0 +}; + + +static TYPELIB key_spec_typelib = +{ + array_elements(key_spec_names) - 1, "", + key_spec_names, NULL +}; + +const char *log_level_names[] = +{ + "Off", + "Fatal", + "Error", + "Warn", + "Info", + "Debug", + "Trace", + 0 +}; + +static TYPELIB log_level_typelib = +{ + array_elements(log_level_names) - 1, "", + log_level_names, NULL +}; + +static MYSQL_SYSVAR_STR(master_key_id, master_key_id, + PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_MEMALLOC, + "Key id for master encryption key. Used to create new datakeys. If not set, no new keys will be created", + NULL, NULL, ""); + +static MYSQL_SYSVAR_ENUM(key_spec, key_spec, + PLUGIN_VAR_RQCMDARG, + "Encryption algorithm used to create new keys.", + NULL, NULL, 0, &key_spec_typelib); + + +static MYSQL_SYSVAR_ENUM(log_level, log_level, + PLUGIN_VAR_RQCMDARG, + "Logging for AWS API", + NULL, NULL, 0, &log_level_typelib); + + +static MYSQL_SYSVAR_INT(rotate_key, rotate_key, + PLUGIN_VAR_RQCMDARG, + "Set this variable to key id to perform rotation of the key. Specify -1 to rotate all keys", + NULL, update_rotate, 0, -1, INT_MAX, 1); + + +static MYSQL_SYSVAR_INT(request_timeout, request_timeout, + PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY, + "Timeout in milliseconds for create HTTPS connection or execute AWS request. Specify 0 to use SDK default.", + NULL, NULL, 0, 0, INT_MAX, 1); + +static MYSQL_SYSVAR_STR(region, region, + PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY, + "AWS region. For example us-east-1, or eu-central-1. If no value provided, SDK default is used.", + NULL, NULL, ""); + +static MYSQL_SYSVAR_STR(endpoint_url, endpoint_url, + PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY, + "Used to override the default AWS API endpoint. If not set, the default will be used", + NULL, NULL, ""); + +#if WITH_AWS_MOCK +static MYSQL_SYSVAR_BOOL(mock, mock, + PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY, + "Mock AWS KMS calls (for testing).", + NULL, NULL, 0); +#endif + +static struct st_mysql_sys_var* settings[]= { + MYSQL_SYSVAR(master_key_id), + MYSQL_SYSVAR(key_spec), + MYSQL_SYSVAR(rotate_key), + MYSQL_SYSVAR(log_level), + MYSQL_SYSVAR(request_timeout), + MYSQL_SYSVAR(region), + MYSQL_SYSVAR(endpoint_url), +#if WITH_AWS_MOCK + MYSQL_SYSVAR(mock), +#endif + NULL +}; + +/* + Plugin library descriptor +*/ +maria_declare_plugin(aws_key_management) +{ + MariaDB_ENCRYPTION_PLUGIN, + &aws_key_management_plugin, + "aws_key_management", + "MariaDB Corporation", + "AWS key management plugin", + PLUGIN_LICENSE_GPL, + plugin_init, + plugin_deinit, + 0x0100, + NULL, + settings, + "1.0", + MariaDB_PLUGIN_MATURITY_STABLE +} +maria_declare_plugin_end; |