diff --git a/CMakeLists.txt b/CMakeLists.txt index 693ecb915ab7..100cfdc54477 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1368,6 +1368,7 @@ if(WITH_TESTS) db/write_batch_test.cc db/write_callback_test.cc db/write_controller_test.cc + encryption/encryption_test.cc env/env_test.cc env/io_posix_test.cc env/mock_env_test.cc diff --git a/Makefile b/Makefile index 07d36bebcc91..4a9a191c679b 100644 --- a/Makefile +++ b/Makefile @@ -703,6 +703,7 @@ TESTS_PLATFORM_DEPENDENT := \ crc32c_test \ coding_test \ inlineskiplist_test \ + encryption_test \ env_basic_test \ env_test \ env_logger_test \ @@ -1979,6 +1980,8 @@ cache_reservation_manager_test: $(OBJ_DIR)/cache/cache_reservation_manager_test. $(AM_LINK) wide_column_serialization_test: $(OBJ_DIR)/db/wide/wide_column_serialization_test.o $(TEST_LIBRARY) $(LIBRARY) + +encryption_test: encryption/encryption_test.o $(LIBOBJECTS) $(TESTHARNESS) $(AM_LINK) #------------------------------------------------- diff --git a/encryption/encryption.cc b/encryption/encryption.cc index 6a5d1645e329..c0048f3f43d8 100644 --- a/encryption/encryption.cc +++ b/encryption/encryption.cc @@ -1,64 +1,222 @@ -// Copyright (c) 2011-present, Facebook, Inc. All rights reserved. -// This source code is licensed under both the GPLv2 (found in the -// COPYING file in the root directory) and Apache 2.0 License -// (found in the LICENSE.Apache file in the root directory). +// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. #ifndef ROCKSDB_LITE #ifdef OPENSSL #include "encryption/encryption.h" -#include "util/string_util.h" +#include +#include + +#include + +#include "port/port.h" namespace ROCKSDB_NAMESPACE { namespace encryption { -Status AESBlockCipher::InitKey(const std::string& key) { -// AES_set_encrypt_key and AES_set_decrypt_key are deprecated: Since OpenSSL 3.0 -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - int ret = - AES_set_encrypt_key(reinterpret_cast(key.data()), - static_cast(key.size()) * 8, &encrypt_key_); - if (ret != 0) { - return Status::InvalidArgument("AES set encrypt key error: " + - std::to_string(ret)); - } - ret = AES_set_decrypt_key(reinterpret_cast(key.data()), - static_cast(key.size()) * 8, &decrypt_key_); -#pragma GCC diagnostic pop - if (ret != 0) { - return Status::InvalidArgument("AES set decrypt key error: " + - std::to_string(ret)); +namespace { +uint64_t GetBigEndian64(const unsigned char* buf) { + if (port::kLittleEndian) { + return (static_cast(buf[0]) << 56) + + (static_cast(buf[1]) << 48) + + (static_cast(buf[2]) << 40) + + (static_cast(buf[3]) << 32) + + (static_cast(buf[4]) << 24) + + (static_cast(buf[5]) << 16) + + (static_cast(buf[6]) << 8) + + (static_cast(buf[7])); + } else { + return *(reinterpret_cast(buf)); + } +} + +void PutBigEndian64(uint64_t value, unsigned char* buf) { + if (port::kLittleEndian) { + buf[0] = static_cast((value >> 56) & 0xff); + buf[1] = static_cast((value >> 48) & 0xff); + buf[2] = static_cast((value >> 40) & 0xff); + buf[3] = static_cast((value >> 32) & 0xff); + buf[4] = static_cast((value >> 24) & 0xff); + buf[5] = static_cast((value >> 16) & 0xff); + buf[6] = static_cast((value >> 8) & 0xff); + buf[7] = static_cast(value & 0xff); + } else { + *(reinterpret_cast(buf)) = value; + } +} +} // anonymous namespace + +// AESCTRCipherStream use OpenSSL EVP API with CTR mode to encrypt and decrypt +// data, instead of using the CTR implementation provided by +// BlockAccessCipherStream. Benefits: +// +// 1. The EVP API automatically figure out if AES-NI can be enabled. +// 2. Keep the data format consistent with OpenSSL (e.g. how IV is interpreted +// as block counter). +// +// References for the openssl EVP API: +// * man page: https://www.openssl.org/docs/man1.1.1/man3/EVP_EncryptUpdate.html +// * SO answer for random access: https://stackoverflow.com/a/57147140/11014942 +// * +// https://medium.com/@amit.kulkarni/encrypting-decrypting-a-file-using-openssl-evp-b26e0e4d28d4 +Status AESCTRCipherStream::Cipher(uint64_t file_offset, char* data, + size_t data_size, bool is_encrypt) { +#if OPENSSL_VERSION_NUMBER < 0x01000200f + (void)file_offset; + (void)data; + (void)data_size; + (void)is_encrypt; + return Status::NotSupported("OpenSSL version < 1.0.2"); +#else + int ret = 1; + EVP_CIPHER_CTX* ctx = nullptr; + InitCipherContext(ctx); + if (ctx == nullptr) { + return Status::IOError("Failed to create cipher context."); + } + + uint64_t block_index = file_offset / AES_BLOCK_SIZE; + uint64_t block_offset = file_offset % AES_BLOCK_SIZE; + + // In CTR mode, OpenSSL EVP API treat the IV as a 128-bit big-endien, and + // increase it by 1 for each block. + // + // In case of unsigned integer overflow in c++, the result is moduloed by + // range, means only the lowest bits of the result will be kept. + // http://www.cplusplus.com/articles/DE18T05o/ + uint64_t iv_high = initial_iv_high_; + uint64_t iv_low = initial_iv_low_ + block_index; + if (std::numeric_limits::max() - block_index < initial_iv_low_) { + iv_high++; + } + unsigned char iv[AES_BLOCK_SIZE]; + PutBigEndian64(iv_high, iv); + PutBigEndian64(iv_low, iv + sizeof(uint64_t)); + + ret = EVP_CipherInit(ctx, cipher_, + reinterpret_cast(key_.data()), iv, + (is_encrypt ? 1 : 0)); + if (ret != 1) { + return Status::IOError("Failed to init cipher."); + } + + // Disable padding. After disabling padding, data size should always be + // multiply of block size. + ret = EVP_CIPHER_CTX_set_padding(ctx, 0); + if (ret != 1) { + return Status::IOError("Failed to disable padding for cipher context."); + } + + uint64_t data_offset = 0; + size_t remaining_data_size = data_size; + int output_size = 0; + unsigned char partial_block[AES_BLOCK_SIZE]; + + // In the following we assume EVP_CipherUpdate allow in and out buffer are + // the same, to save one memcpy. This is not specified in official man page. + + // Handle partial block at the beginning. The parital block is copied to + // buffer to fake a full block. + if (block_offset > 0) { + size_t partial_block_size = + std::min(AES_BLOCK_SIZE - block_offset, remaining_data_size); + memcpy(partial_block + block_offset, data, partial_block_size); + ret = EVP_CipherUpdate(ctx, partial_block, &output_size, partial_block, + AES_BLOCK_SIZE); + if (ret != 1) { + return Status::IOError("Crypter failed for first block, offset " + + std::to_string(file_offset)); + } + if (output_size != AES_BLOCK_SIZE) { + return Status::IOError( + "Unexpected crypter output size for first block, expected " + + std::to_string(AES_BLOCK_SIZE) + " vs actual " + std::to_string(output_size)); + } + memcpy(data, partial_block + block_offset, partial_block_size); + data_offset += partial_block_size; + remaining_data_size -= partial_block_size; } + + // Handle full blocks in the middle. + if (remaining_data_size >= AES_BLOCK_SIZE) { + size_t actual_data_size = + remaining_data_size - remaining_data_size % AES_BLOCK_SIZE; + unsigned char* full_blocks = + reinterpret_cast(data) + data_offset; + ret = EVP_CipherUpdate(ctx, full_blocks, &output_size, full_blocks, + static_cast(actual_data_size)); + if (ret != 1) { + return Status::IOError("Crypter failed at offset " + + std::to_string(file_offset + data_offset)); + } + if (output_size != static_cast(actual_data_size)) { + return Status::IOError("Unexpected crypter output size, expected " + + std::to_string(actual_data_size) + " vs actual " + + std::to_string(output_size)); + } + data_offset += actual_data_size; + remaining_data_size -= actual_data_size; + } + + // Handle partial block at the end. The parital block is copied to buffer to + // fake a full block. + if (remaining_data_size > 0) { + assert(remaining_data_size < AES_BLOCK_SIZE); + memcpy(partial_block, data + data_offset, remaining_data_size); + ret = EVP_CipherUpdate(ctx, partial_block, &output_size, partial_block, + AES_BLOCK_SIZE); + if (ret != 1) { + return Status::IOError("Crypter failed for last block, offset " + + std::to_string(file_offset + data_offset)); + } + if (output_size != AES_BLOCK_SIZE) { + return Status::IOError( + "Unexpected crypter output size for last block, expected " + + std::to_string(AES_BLOCK_SIZE) + " vs actual " + std::to_string(output_size)); + } + memcpy(data + data_offset, partial_block, remaining_data_size); + } + FreeCipherContext(ctx); return Status::OK(); +#endif } Status NewAESCTRCipherStream(EncryptionMethod method, const std::string& key, const std::string& iv, std::unique_ptr* result) { assert(result != nullptr); - size_t key_size = KeySize(method); - if (key_size == 0) { - return Status::InvalidArgument("Unsupported encryption method: " + - std::to_string(static_cast(method))); + const EVP_CIPHER* cipher = nullptr; + switch (method) { + case EncryptionMethod::kAES128_CTR: + cipher = EVP_aes_128_ctr(); + break; + case EncryptionMethod::kAES192_CTR: + cipher = EVP_aes_192_ctr(); + break; + case EncryptionMethod::kAES256_CTR: + cipher = EVP_aes_256_ctr(); + break; + default: + return Status::InvalidArgument("Unsupported encryption method: " + + std::to_string(static_cast(method))); } - if (key.size() != key_size) { + if (key.size() != KeySize(method)) { return Status::InvalidArgument("Encryption key size mismatch. " + std::to_string(key.size()) + "(actual) vs. " + - std::to_string(key_size) + "(expected)."); + std::to_string(KeySize(method)) + "(expected)."); } if (iv.size() != AES_BLOCK_SIZE) { return Status::InvalidArgument( "iv size not equal to block cipher block size: " + std::to_string(iv.size()) + "(actual) vs. " + std::to_string(AES_BLOCK_SIZE) + "(expected)."); } - std::unique_ptr cipher_stream(new AESCTRCipherStream(iv)); - Status s = cipher_stream->InitKey(key); - if (!s.ok()) { - return s; - } - *result = std::move(cipher_stream); + Slice iv_slice(iv); + uint64_t iv_high = + GetBigEndian64(reinterpret_cast(iv.data())); + uint64_t iv_low = GetBigEndian64( + reinterpret_cast(iv.data() + sizeof(uint64_t))); + result->reset(new AESCTRCipherStream(cipher, key, iv_high, iv_low)); return Status::OK(); } diff --git a/encryption/encryption.h b/encryption/encryption.h index 8b5dbfc2ac30..2f0b3e2fbd88 100644 --- a/encryption/encryption.h +++ b/encryption/encryption.h @@ -1,91 +1,93 @@ +// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. + #pragma once #ifndef ROCKSDB_LITE #ifdef OPENSSL #include +#include + +#include #include "rocksdb/encryption.h" #include "rocksdb/env_encryption.h" -#include "util/coding.h" +#include "util/string_util.h" namespace ROCKSDB_NAMESPACE { namespace encryption { -class AESBlockCipher final : public BlockCipher { - public: - virtual ~AESBlockCipher() = default; +#if OPENSSL_VERSION_NUMBER < 0x01010000f - const char* Name() const override { return "AESBlockCipher"; } +#define InitCipherContext(ctx) \ + EVP_CIPHER_CTX ctx##_var; \ + ctx = &ctx##_var; \ + EVP_CIPHER_CTX_init(ctx); - Status InitKey(const std::string& key); +// do nothing +#define FreeCipherContext(ctx) - size_t BlockSize() override { - return AES_BLOCK_SIZE; // 16 - } +#else -// AES_encrypt and AES_decrypt are deprecated: Since OpenSSL 3.0 -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - Status Encrypt(char* data) override { - AES_encrypt(reinterpret_cast(data), - reinterpret_cast(data), &encrypt_key_); - return Status::OK(); +#define InitCipherContext(ctx) \ + ctx = EVP_CIPHER_CTX_new(); \ + if (ctx != nullptr) { \ + if (EVP_CIPHER_CTX_reset(ctx) != 1) { \ + ctx = nullptr; \ + } \ } - Status Decrypt(char* data) override { - AES_decrypt(reinterpret_cast(data), - reinterpret_cast(data), &decrypt_key_); - return Status::OK(); - } -#pragma GCC diagnostic pop +#define FreeCipherContext(ctx) EVP_CIPHER_CTX_free(ctx); - private: - AES_KEY encrypt_key_; - AES_KEY decrypt_key_; -}; +#endif class AESCTRCipherStream : public BlockAccessCipherStream { public: - static constexpr size_t kNonceSize = AES_BLOCK_SIZE - sizeof(uint64_t); // 8 + AESCTRCipherStream(const EVP_CIPHER* cipher, const std::string& key, + uint64_t iv_high, uint64_t iv_low) + : cipher_(cipher), + key_(key), + initial_iv_high_(iv_high), + initial_iv_low_(iv_low) {} - AESCTRCipherStream(const std::string& iv) - : nonce_(iv, 0, kNonceSize), - initial_counter_( - *reinterpret_cast(iv.data() + kNonceSize)) {} + ~AESCTRCipherStream() = default; size_t BlockSize() override { return AES_BLOCK_SIZE; // 16 } - Status InitKey(const std::string& key) { return block_cipher_.InitKey(key); } + Status Encrypt(uint64_t file_offset, char* data, size_t data_size) override { + return Cipher(file_offset, data, data_size, true /*is_encrypt*/); + } + + Status Decrypt(uint64_t file_offset, char* data, size_t data_size) override { + return Cipher(file_offset, data, data_size, false /*is_encrypt*/); + } protected: - void AllocateScratch(std::string& scratch) override { - scratch.reserve(BlockSize()); + // Following methods required by BlockAccessCipherStream is unused. + + void AllocateScratch(std::string& /*scratch*/) override { + // should not be called. + assert(false); } - Status EncryptBlock(uint64_t block_index, char* data, - char* scratch) override { - memcpy(scratch, nonce_.data(), kNonceSize); - EncodeFixed64(scratch + kNonceSize, block_index + initial_counter_); - Status s = block_cipher_.Encrypt(scratch); - if (!s.ok()) { - return s; - } - for (size_t i = 0; i < AES_BLOCK_SIZE; i++) { - data[i] = data[i] ^ scratch[i]; - } - return Status::OK(); + Status EncryptBlock(uint64_t /*block_index*/, char* /*data*/, + char* /*scratch*/) override { + return Status::NotSupported("EncryptBlock should not be called."); } - Status DecryptBlock(uint64_t block_index, char* data, - char* scratch) override { - return EncryptBlock(block_index, data, scratch); + Status DecryptBlock(uint64_t /*block_index*/, char* /*data*/, + char* /*scratch*/) override { + return Status::NotSupported("DecryptBlock should not be called."); } private: - AESBlockCipher block_cipher_; - std::string nonce_; - uint64_t initial_counter_; + Status Cipher(uint64_t file_offset, char* data, size_t data_size, + bool is_encrypt); + + const EVP_CIPHER* cipher_; + const std::string key_; + const uint64_t initial_iv_high_; + const uint64_t initial_iv_low_; }; extern Status NewAESCTRCipherStream( diff --git a/encryption/encryption_test.cc b/encryption/encryption_test.cc new file mode 100644 index 000000000000..ffe74c56842e --- /dev/null +++ b/encryption/encryption_test.cc @@ -0,0 +1,166 @@ +// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. + +#include "encryption/encryption.h" + +#include "port/stack_trace.h" +#include "test_util/testharness.h" +#include "test_util/testutil.h" + +#ifndef ROCKSDB_LITE +#ifdef OPENSSL + +namespace ROCKSDB_NAMESPACE { +namespace encryption { + +const unsigned char KEY[33] = + "\xe4\x3e\x8e\xca\x2a\x83\xe1\x88\xfb\xd8\x02\xdc\xf3\x62\x65\x3e" + "\x00\xee\x31\x39\xe7\xfd\x1d\x92\x20\xb1\x62\xae\xb2\xaf\x0f\x1a"; +const unsigned char IV_RANDOM[17] = + "\x77\x9b\x82\x72\x26\xb5\x76\x50\xf7\x05\xd2\xd6\xb8\xaa\xa9\x2c"; +const unsigned char IV_OVERFLOW_LOW[17] = + "\x77\x9b\x82\x72\x26\xb5\x76\x50\xff\xff\xff\xff\xff\xff\xff\xff"; +const unsigned char IV_OVERFLOW_FULL[17] = + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"; + +constexpr size_t MAX_SIZE = 16 * 10; + +// Test to make sure output of AESCTRCipherStream is the same as output from +// OpenSSL EVP API. +class EncryptionTest + : public testing::TestWithParam> { + public: + unsigned char plaintext[MAX_SIZE]; + // Reserve a bit more room to make sure OpenSSL have enough buffer. + unsigned char ciphertext[MAX_SIZE + 16 * 2]; + + void GenerateCiphertext(const unsigned char* iv) { + Random rnd(666); + std::string random_string = + rnd.HumanReadableString(static_cast(MAX_SIZE)); + memcpy(plaintext, random_string.data(), MAX_SIZE); + + int ret = 1; + EVP_CIPHER_CTX* ctx; + InitCipherContext(ctx); + assert(ctx != nullptr); + + const EVP_CIPHER* cipher = nullptr; + EncryptionMethod method = std::get<1>(GetParam()); + switch (method) { + case EncryptionMethod::kAES128_CTR: + cipher = EVP_aes_128_ctr(); + break; + case EncryptionMethod::kAES192_CTR: + cipher = EVP_aes_192_ctr(); + break; + case EncryptionMethod::kAES256_CTR: + cipher = EVP_aes_256_ctr(); + break; + default: + assert(false); + } + assert(cipher != nullptr); + + ret = EVP_EncryptInit(ctx, cipher, KEY, iv); + assert(ret == 1); + int output_size = 0; + ret = EVP_EncryptUpdate(ctx, ciphertext, &output_size, plaintext, + static_cast(MAX_SIZE)); + assert(ret == 1); + int final_output_size = 0; + ret = EVP_EncryptFinal(ctx, ciphertext + output_size, &final_output_size); + assert(ret == 1); + assert(output_size + final_output_size == MAX_SIZE); + FreeCipherContext(ctx); + } + + void TestEncryptionImpl(size_t start, size_t end, const unsigned char* iv, + bool* success) { + assert(start < end && end <= MAX_SIZE); + GenerateCiphertext(iv); + + EncryptionMethod method = std::get<1>(GetParam()); + std::string key_str(reinterpret_cast(KEY), KeySize(method)); + std::string iv_str(reinterpret_cast(iv), 16); + std::unique_ptr cipher_stream; + ASSERT_OK(NewAESCTRCipherStream(method, key_str, iv_str, &cipher_stream)); + + size_t data_size = end - start; + // Allocate exact size. AESCTRCipherStream should make sure there will be + // no memory corruption. + std::unique_ptr data(new char[data_size]); + + if (std::get<0>(GetParam())) { + // Encrypt + memcpy(data.get(), plaintext + start, data_size); + ASSERT_OK(cipher_stream->Encrypt(start, data.get(), data_size)); + ASSERT_EQ(0, memcmp(ciphertext + start, data.get(), data_size)); + } else { + // Decrypt + memcpy(data.get(), ciphertext + start, data_size); + ASSERT_OK(cipher_stream->Decrypt(start, data.get(), data_size)); + ASSERT_EQ(0, memcmp(plaintext + start, data.get(), data_size)); + } + + *success = true; + } + + bool TestEncryption(size_t start, size_t end, + const unsigned char* iv = IV_RANDOM) { + // Workaround failure of ASSERT_* result in return immediately. + bool success = false; + TestEncryptionImpl(start, end, iv, &success); + return success; + } +}; + +TEST_P(EncryptionTest, EncryptionTest) { + // One full block. + EXPECT_TRUE(TestEncryption(0, 16)); + // One block in the middle. + EXPECT_TRUE(TestEncryption(16 * 5, 16 * 6)); + // Multiple aligned blocks. + EXPECT_TRUE(TestEncryption(16 * 5, 16 * 8)); + + // Random byte at the beginning of a block. + EXPECT_TRUE(TestEncryption(16 * 5, 16 * 5 + 1)); + // Random byte in the middle of a block. + EXPECT_TRUE(TestEncryption(16 * 5 + 4, 16 * 5 + 5)); + // Random byte at the end of a block. + EXPECT_TRUE(TestEncryption(16 * 5 + 15, 16 * 6)); + + // Partial block aligned at the beginning. + EXPECT_TRUE(TestEncryption(16 * 5, 16 * 5 + 15)); + // Partial block aligned at the end. + EXPECT_TRUE(TestEncryption(16 * 5 + 1, 16 * 6)); + // Multiple blocks with a partial block at the end. + EXPECT_TRUE(TestEncryption(16 * 5, 16 * 8 + 15)); + // Multiple blocks with a partial block at the beginning. + EXPECT_TRUE(TestEncryption(16 * 5 + 1, 16 * 8)); + // Partial block at both ends. + EXPECT_TRUE(TestEncryption(16 * 5 + 1, 16 * 8 + 15)); + + // Lower bits of IV overflow. + EXPECT_TRUE(TestEncryption(16, 16 * 2, IV_OVERFLOW_LOW)); + // Full IV overflow. + EXPECT_TRUE(TestEncryption(16, 16 * 2, IV_OVERFLOW_FULL)); +} + +INSTANTIATE_TEST_CASE_P( + EncryptionTestInstance, EncryptionTest, + testing::Combine(testing::Bool(), + testing::Values(EncryptionMethod::kAES128_CTR, + EncryptionMethod::kAES192_CTR, + EncryptionMethod::kAES256_CTR))); + +} // namespace encryption +} // namespace ROCKSDB_NAMESPACE + +#endif // OPENSSL +#endif // !ROCKSDB_LITE + +int main(int argc, char** argv) { + rocksdb::port::InstallStackTraceHandler(); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/include/rocksdb/encryption.h b/include/rocksdb/encryption.h index bcd5a095ccef..253c9c6a5049 100644 --- a/include/rocksdb/encryption.h +++ b/include/rocksdb/encryption.h @@ -1,7 +1,4 @@ -// Copyright (c) 2011-present, Facebook, Inc. All rights reserved. -// This source code is licensed under both the GPLv2 (found in the -// COPYING file in the root directory) and Apache 2.0 License -// (found in the LICENSE.Apache file in the root directory). +// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. #pragma once #ifndef ROCKSDB_LITE