From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from localhost (localhost [127.0.0.1]) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTP id DA3882D2FC for ; Thu, 25 Apr 2019 17:05:33 -0400 (EDT) Received: from turing.freelists.org ([127.0.0.1]) by localhost (turing.freelists.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id ytnUbKg7fuau for ; Thu, 25 Apr 2019 17:05:33 -0400 (EDT) Received: from smtpng3.m.smailru.net (smtpng3.m.smailru.net [94.100.177.149]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTPS id 65DB42CE84 for ; Thu, 25 Apr 2019 17:05:33 -0400 (EDT) From: Vladislav Shpilevoy Subject: [tarantool-patches] [PATCH 3/3] crypto: implement crypto codec API and AES 128 encryption Date: Fri, 26 Apr 2019 00:05:29 +0300 Message-Id: <0e95f1eff6e2caaab4a2ed344bfdf9e67a9f23b5.1556226152.git.v.shpilevoy@tarantool.org> In-Reply-To: References: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Sender: tarantool-patches-bounce@freelists.org Errors-to: tarantool-patches-bounce@freelists.org Reply-To: tarantool-patches@freelists.org List-Help: List-Unsubscribe: List-software: Ecartis version 1.0.0 List-Id: tarantool-patches List-Subscribe: List-Owner: List-post: List-Archive: To: tarantool-patches@freelists.org Cc: kostja@tarantool.org OpenSSL API is quite complex and hard to follow, additionally it is very unstable. Encoding/decoding via OpenSSL methods usually consists of multiple calls of a lot of functions. This patch wraps OpenSSL API with one more easy to use and conforming Tarantool code style in scope of crypto library. The API provides struct crypto_codec which encapsulates all the encryption logic and exposes quite simple API like this: crypto_codec_encode/decode(in, in_size, out, out_size) A caller can create a needed codec via crypto_codec_new, which now supports only AES 128 algorithm. The reason behind such a choice is simple - it is needed in SWIM now. The API is flexible in terms of extending and adding new algorithms in future. Needed for #3234 --- src/lib/core/diag.h | 2 + src/lib/core/exception.cc | 25 ++++++ src/lib/core/exception.h | 7 ++ src/lib/crypto/CMakeLists.txt | 2 +- src/lib/crypto/crypto.c | 133 +++++++++++++++++++++++++++++ src/lib/crypto/crypto.h | 87 +++++++++++++++++++ test/unit/CMakeLists.txt | 3 + test/unit/crypto.c | 153 ++++++++++++++++++++++++++++++++++ test/unit/crypto.result | 32 +++++++ 9 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 test/unit/crypto.c create mode 100644 test/unit/crypto.result diff --git a/src/lib/core/diag.h b/src/lib/core/diag.h index 78f0cdbdf..fd3831e66 100644 --- a/src/lib/core/diag.h +++ b/src/lib/core/diag.h @@ -251,6 +251,8 @@ struct error * BuildCollationError(const char *file, unsigned line, const char *format, ...); struct error * BuildSwimError(const char *file, unsigned line, const char *format, ...); +struct error * +BuildCryptoError(const char *file, unsigned line, const char *format, ...); struct index_def; diff --git a/src/lib/core/exception.cc b/src/lib/core/exception.cc index 6124c70d0..a6999af43 100644 --- a/src/lib/core/exception.cc +++ b/src/lib/core/exception.cc @@ -280,6 +280,19 @@ SwimError::SwimError(const char *file, unsigned line, const char *format, ...) va_end(ap); } +const struct type_info type_CryptoError = + make_type("CryptoError", &type_Exception); + +CryptoError::CryptoError(const char *file, unsigned line, + const char *format, ...) + : Exception(&type_CryptoError, file, line) +{ + va_list ap; + va_start(ap, format); + error_vformat_msg(this, format, ap); + va_end(ap); +} + #define BuildAlloc(type) \ void *p = malloc(sizeof(type)); \ if (p == NULL) \ @@ -371,6 +384,18 @@ BuildSwimError(const char *file, unsigned line, const char *format, ...) return e; } +struct error * +BuildCryptoError(const char *file, unsigned line, const char *format, ...) +{ + BuildAlloc(CryptoError); + CryptoError *e = new (p) CryptoError(file, line, ""); + va_list ap; + va_start(ap, format); + error_vformat_msg(e, format, ap); + va_end(ap); + return e; +} + struct error * BuildSocketError(const char *file, unsigned line, const char *socketname, const char *format, ...) diff --git a/src/lib/core/exception.h b/src/lib/core/exception.h index 4f5b66f2e..a29281427 100644 --- a/src/lib/core/exception.h +++ b/src/lib/core/exception.h @@ -51,6 +51,7 @@ extern const struct type_info type_IllegalParams; extern const struct type_info type_SystemError; extern const struct type_info type_CollationError; extern const struct type_info type_SwimError; +extern const struct type_info type_CryptoError; const char * exception_get_string(struct error *e, const struct method_info *method); @@ -166,6 +167,12 @@ public: virtual void raise() { throw this; } }; +class CryptoError: public Exception { +public: + CryptoError(const char *file, unsigned line, const char *format, ...); + virtual void raise() { throw this; } +}; + /** * Initialize the exception subsystem. */ diff --git a/src/lib/crypto/CMakeLists.txt b/src/lib/crypto/CMakeLists.txt index 7e2c6e1d3..4e2e5e403 100644 --- a/src/lib/crypto/CMakeLists.txt +++ b/src/lib/crypto/CMakeLists.txt @@ -2,4 +2,4 @@ set(lib_sources crypto.c) set_source_files_compile_flags(${lib_sources}) add_library(crypto STATIC ${lib_sources}) -target_link_libraries(crypto ${OPENSSL_LIBRARIES}) +target_link_libraries(crypto ${OPENSSL_LIBRARIES} core) diff --git a/src/lib/crypto/crypto.c b/src/lib/crypto/crypto.c index 4b192ba2d..35e53e238 100644 --- a/src/lib/crypto/crypto.c +++ b/src/lib/crypto/crypto.c @@ -29,12 +29,145 @@ * SUCH DAMAGE. */ #include "crypto.h" +#include "diag.h" +#include "exception.h" +#include "core/random.h" #include #include #include #include #include +/** Set a diag error with the latest OpenSSL error message. */ +static inline void +diag_set_OpenSSL(void) +{ + diag_set(CryptoError, "OpenSSL error: %s", + ERR_error_string(ERR_get_error(), NULL)); +} + +/** + * OpenSSL codec. It contains only key, initial vector, and a + * constant cipher type, because OpenSSL does not allow + * long-living EVP_CIPHER_CTX contexts. + */ +struct crypto_codec { + /** + * Secret key, usually is unchanged among multiple data + * packets. Only AES 128 is supported now, so the buffer + * is of 16 byte size. + */ + unsigned char key[CRYPTO_AES128_KEY_SIZE]; + /** + * Initial vector, and in fact a public key. It should be + * regenerated randomly for each data packet. + */ + unsigned char iv[CRYPTO_AES_IV_SIZE]; + /** Cipher type. Depends on algorithm and key size. */ + const EVP_CIPHER *type; +}; + +struct crypto_codec * +crypto_codec_new(enum crypto_algo algo, const char *key) +{ + if (algo != CRYPTO_AES128) { + diag_set(CryptoError, "Only AES 128 is supported"); + return NULL; + } + struct crypto_codec *c = (struct crypto_codec *) malloc(sizeof(*c)); + if (c == NULL) { + diag_set(OutOfMemory, sizeof(*c), "malloc", "c"); + return NULL; + } + memcmp(c->key, key, sizeof(c->key)); + memset(c->iv, 0, sizeof(c->iv)); + c->type = EVP_aes_128_cbc(); + return c; +} + +const char * +crypto_codec_gen_iv(struct crypto_codec *c) +{ + random_bytes((char *) c->iv, sizeof(c->iv)); + return (char *) c->iv; +} + +int +crypto_codec_encode(struct crypto_codec *c, const char *in, int in_size, + char *out, int out_size) +{ + const unsigned char *uin = (const unsigned char *) in; + unsigned char *uout = (unsigned char *) out; + /* + * Note, that even if in_size is already multiple of block + * size, additional block is still needed. Result is + * always bigger than input. + */ + int len, result, need = in_size + CRYPTO_AES_BLOCK_SIZE - + (in_size % CRYPTO_AES_BLOCK_SIZE); + if (need > out_size) + return need; + + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) + goto error; + if (EVP_EncryptInit_ex(ctx, c->type, NULL, c->key, c->iv) != 1) + goto error; + if (EVP_EncryptUpdate(ctx, uout, &len, uin, in_size) != 1) + goto error; + result = len; + assert(result <= need); + if (EVP_EncryptFinal_ex(ctx, uout + result, &len) != 1) + goto error; + result += len; + assert(result == need); + EVP_CIPHER_CTX_free(ctx); + return result; + +error: + diag_set_OpenSSL(); + if (ctx != NULL) + EVP_CIPHER_CTX_free(ctx); + return -1; +} + +int +crypto_codec_decode(struct crypto_codec *c, const char *iv, + const char *in, int in_size, char *out, int out_size) +{ + if (in_size > out_size) + return in_size; + const unsigned char *uin = (const unsigned char *) in; + const unsigned char *uiv = (const unsigned char *) iv; + unsigned char *uout = (unsigned char *) out; + int len, result; + + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) + goto error; + if (EVP_DecryptInit_ex(ctx, c->type, NULL, c->key, uiv) != 1) + goto error; + if (EVP_DecryptUpdate(ctx, uout, &len, uin, in_size) != 1) + goto error; + result = len; + if (EVP_DecryptFinal_ex(ctx, uout + result, &len) != 1) + goto error; + EVP_CIPHER_CTX_free(ctx); + return result + len; + +error: + diag_set_OpenSSL(); + if (ctx != NULL) + EVP_CIPHER_CTX_free(ctx); + return -1; +} + +void +crypto_codec_delete(struct crypto_codec *c) +{ + free(c); +} + void crypto_init(void) { diff --git a/src/lib/crypto/crypto.h b/src/lib/crypto/crypto.h index bc807e1a2..2c803dbf8 100644 --- a/src/lib/crypto/crypto.h +++ b/src/lib/crypto/crypto.h @@ -35,6 +35,93 @@ extern "C" { #endif +enum crypto_algo { + /** + * AES - Advanced Encryption Standard. It is a symmetric + * key block cipher algorithm. AES encodes data in blocks + * of 128 bits, for what it uses a key and an initial + * vector. The key is a secret information which needs to + * be shared among communicating nodes. Key size can be + * 128, 192 and 256 bits. Initial vector is an additional + * entropy factor to prevent an attack when an attacker + * somehow learns a purpose or content of one data packet + * and immediately learns purpose of all the same looking + * packets. Initial vector should be generated randomly + * for each data packet, nonetheless it is not private and + * can be sent without encryption. + */ + CRYPTO_AES128, +}; + +enum { + /** + * Block size of AES operation. Encrypted data size is + * always divisible by this value. + */ + CRYPTO_AES_BLOCK_SIZE = 16, + /** + * Size of AES initial vector. It does not depend on key + * size. + */ + CRYPTO_AES_IV_SIZE = 16, + /** + * Obviously, 128 bits = 16 bytes always. It is just + * syntax sugar so as not to hardcode 16 everywhere. + */ + CRYPTO_AES128_KEY_SIZE = 16, +}; + +struct crypto_codec; + +/** + * Create a new codec working with a specified @a algo algorithm + * and a secret @a key. Size of @a key depends on the chosen + * algorithm. + */ +struct crypto_codec * +crypto_codec_new(enum crypto_algo algo, const char *key); + +/** Generate a new initial vector and return it. */ +const char * +crypto_codec_gen_iv(struct crypto_codec *c); + +/** + * Encode plain data by codec @a c. + * @param c Codec. + * @param in Plain input data. + * @param in_size Byte size of @a in. + * @param out Output buffer. + * @param out_size Byte size of @a out. + * + * @retval -1 Error. Diag is set. + * @return Number of written bytes. If > @a out_size then nothing + * is written, needed number of bytes is returned. + */ +int +crypto_codec_encode(struct crypto_codec *c, const char *in, int in_size, + char *out, int out_size); + +/** + * Decode cipher by codec @a c. + * @param c Codec. + * @param iv Initial vector used to encode @a in. + * @param in Cipher to decode. + * @param in_size Byte size of @a in. + * @param out Output buffer. + * @param out_size Byte size of @a out. + * + * @retval -1 Error. Diag is set. + * @return Number of written bytes. If > @a out_size then nothing + * is written, needed number of bytes is returned. + */ +int +crypto_codec_decode(struct crypto_codec *c, const char *iv, + const char *in, int in_size, char *out, int out_size); + +/** Delete codec. */ +void +crypto_codec_delete(struct crypto_codec *c); + void crypto_init(void); diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index bb88b9c9b..368ceb649 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -213,6 +213,9 @@ target_link_libraries(checkpoint_schedule.test m unit) add_executable(sio.test sio.c) target_link_libraries(sio.test unit core) +add_executable(crypto.test crypto.c) +target_link_libraries(crypto.test crypto unit) + add_executable(swim.test swim.c swim_test_transport.c swim_test_ev.c swim_test_utils.c ${PROJECT_SOURCE_DIR}/src/version.c) target_link_libraries(swim.test unit swim) diff --git a/test/unit/crypto.c b/test/unit/crypto.c new file mode 100644 index 000000000..c490e4f12 --- /dev/null +++ b/test/unit/crypto.c @@ -0,0 +1,153 @@ +/* + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#include "crypto/crypto.h" +#include "core/random.h" +#include "unit.h" +#include "trivia/util.h" +#include "memory.h" +#include "fiber.h" + +static void +test_aes128_codec(void) +{ + header(); + plan(20); + + char key[CRYPTO_AES128_KEY_SIZE]; + random_bytes(key, sizeof(key)); + struct crypto_codec *c = crypto_codec_new(-1, key); + is(c, NULL, "crypto does not support non-128 AES"); + c = crypto_codec_new(CRYPTO_AES128, key); + isnt(c, NULL, "128 is supported"); + + int rc = crypto_codec_encode(c, NULL, 10, NULL, 0); + is(rc, 16, "encode returns needed number of bytes"); + rc = crypto_codec_encode(c, NULL, 10, NULL, 15); + is(rc, 16, "encode does not write anything when too small "\ + "buffer"); + rc = crypto_codec_encode(c, NULL, 0, NULL, 0); + is(rc, 16, "encode does not allow 0 sized buffer"); + rc = crypto_codec_encode(c, NULL, 30, NULL, 0); + is(rc, 32, "encode rounds up by block size"); + rc = crypto_codec_encode(c, NULL, 32, NULL, 0); + is(rc, 48, "encode requires additional block when buffer "\ + "size is multiple of block size"); + + const char *plain = "plain text"; + int plain_size = strlen(plain) + 1; + char buffer1[128], buffer2[128]; + int buffer_size = sizeof(buffer1); + const char *iv = crypto_codec_gen_iv(c); + + rc = crypto_codec_encode(c, plain, plain_size, buffer1, buffer_size); + is(rc, 16, "encode works when buffer is big enough"); + rc = crypto_codec_encode(c, plain, plain_size, buffer2, buffer_size); + is(rc, 16, "encode returns the same on second call"); + is(memcmp(buffer1, buffer2, rc), 0, "encrypted data is the same"); + isnt(memcmp(buffer1, plain, plain_size), 0, + "and it is not just copied from the plain text"); + + rc = crypto_codec_decode(c, iv, NULL, 16, NULL, 0); + is(rc, 16, "decode also checks length and returns needed number "\ + "of bytes"); + rc = crypto_codec_decode(c, iv, buffer1, 16, buffer2, buffer_size); + is(rc, plain_size, "decode returns correct number of bytes"); + is(memcmp(buffer2, plain, plain_size), 0, "and correctly decodes data"); + + rc = crypto_codec_decode(c, "false iv not meaning anything", + buffer1, 16, buffer2, buffer_size); + is(rc, -1, "decode can fail with wrong IV"); + ok(! diag_is_empty(diag_get()), "diag error is set"); + + const char *iv2 = crypto_codec_gen_iv(c); + rc = crypto_codec_encode(c, plain, plain_size, buffer2, buffer_size); + is(rc, 16, "encode with different IV and the same number of written "\ + "bytes returned") + isnt(memcmp(buffer2, buffer1, rc), 0, + "the encoded data looks different"); + rc = crypto_codec_decode(c, iv2, buffer2, 16, buffer1, buffer_size); + is(rc, plain_size, "decode works with correct but another IV"); + is(memcmp(buffer1, plain, plain_size), 0, "data is the same"); + + crypto_codec_delete(c); + + check_plan(); + footer(); +} + +static void +test_aes128_stress(void) +{ + header(); + plan(1); + char key[CRYPTO_AES128_KEY_SIZE]; + random_bytes(key, sizeof(key)); + struct crypto_codec *c = crypto_codec_new(CRYPTO_AES128, key); + + char plain[515], cipher[1024], result[1024]; + int rc, size = 10; + for (int size = 10; size < (int) sizeof(plain); size += 10) { + random_bytes(plain, size); + const char *iv = crypto_codec_gen_iv(c); + rc = crypto_codec_encode(c, plain, size, + cipher, sizeof(cipher)); + rc = crypto_codec_decode(c, iv, cipher, rc, + result, sizeof(result)); + fail_if(memcmp(result, plain, rc) != 0); + } + ok(true, "try encode/decode on a variety of sizes, keys, and ivs"); + + check_plan(); + crypto_codec_delete(c); + footer(); +} + +int +main(void) +{ + header(); + plan(2); + random_init(); + crypto_init(); + memory_init(); + fiber_init(fiber_c_invoke); + + test_aes128_codec(); + test_aes128_stress(); + + fiber_free(); + memory_free(); + crypto_free(); + random_free(); + int rc = check_plan(); + footer(); + return rc; +} diff --git a/test/unit/crypto.result b/test/unit/crypto.result new file mode 100644 index 000000000..a3f479be8 --- /dev/null +++ b/test/unit/crypto.result @@ -0,0 +1,32 @@ + *** main *** +1..2 + *** test_aes128_codec *** + 1..20 + ok 1 - crypto does not support non-128 AES + ok 2 - 128 is supported + ok 3 - encode returns needed number of bytes + ok 4 - encode does not write anything when too small buffer + ok 5 - encode does not allow 0 sized buffer + ok 6 - encode rounds up by block size + ok 7 - encode requires additional block when buffer size is multiple of block size + ok 8 - encode works when buffer is big enough + ok 9 - encode returns the same on second call + ok 10 - encrypted data is the same + ok 11 - and it is not just copied from the plain text + ok 12 - decode also checks length and returns needed number of bytes + ok 13 - decode returns correct number of bytes + ok 14 - and correctly decodes data + ok 15 - decode can fail with wrong IV + ok 16 - diag error is set + ok 17 - encode with different IV and the same number of written bytes returned + ok 18 - the encoded data looks different + ok 19 - decode works with correct but another IV + ok 20 - data is the same +ok 1 - subtests + *** test_aes128_codec: done *** + *** test_aes128_stress *** + 1..1 + ok 1 - try encode/decode on a variety of sizes, keys, and ivs +ok 2 - subtests + *** test_aes128_stress: done *** + *** main: done *** -- 2.20.1 (Apple Git-117)