From 7bc2528c077e585cab2be5773fcf659b2d8113ff Mon Sep 17 00:00:00 2001 From: Yingchun Lai Date: Fri, 15 Sep 2023 15:37:11 +0800 Subject: [PATCH] [encryption] Manage file key by the file to encrypt itself (#16) https://github.com/apache/incubator-pegasus/issues/1575 After all encryption related patches been cherry-picked from [tikv](https://github.com/tikv/rocksdb/commits/6.29.tikv) and merged, now we will improve the encrytion, including: - Fix action job `build-linux-encrypted_env-no_compression-no_openssl` to build binaries without openssl and compression libs correctly. - Fix action job `build-linux-encrypted_env-openssl` to export the `ENCRYPTED_ENV` enviroment variable correctly. - Don not skip tests which are skipped by TiKV. - Refactor `AESCTRCipherStream` and `AESEncryptionProvider` to support manage file key by the file itself, according to the design docs in [Data at rest encryption](https://github.com/apache/incubator-pegasus/issues/1575). - Remove all KeyManager related codes. - Replace KeyManager tests by AES encryption tests. - Refactor encryption/encryption_test.cc and add more tests. - Make it possible to construct AESEncryptionProvider object via `EncryptionProvider::CreateFromString()` by registering a factory in "encryption" library. It's possible to construct an object by URI: `AES`, `AES://test` or `AES:,`. - `ldb` tool support to parse `--fs_uri` flags as the URI mentioned above. - Add tests to create AESEncryptionProvider object in `CreateEncryptedEnvTest.CreateEncryptedFileSystem` - `db_bench` support to run benchmark with encryption enabled, by adding new flags for `db_bench`, they are `encryption_method` and `encryption_instance_key`. - Move code from the exported header directory (i.e. include/rocksdb/encryption.h) to rocksdb internal (i.e. encryption/encryption.h), do not expose them to users. - Code format. Review hint: https://github.com/pegasus-kv/rocksdb/pull/17 shows all the code changes from the base branch (i.e. `pegasus-kv:v8.3.2-pegasus`), you can review it together to make sure the request branch `acelyc111:pk_enc_new` doesn't have vice effect on the base. Manual test: ``` // Generate some data. ./db_bench --encryption_method=AES128CTR --encryption_instance_key=test_instance_key --num=10000 // Dump WAL OK ./tools/ldb --fs_uri="provider=AES; id=EncryptedFileSystem" dump_wal --walfile=/tmp/rocksdbtest-1000/dbbench/000004.log ./tools/ldb --fs_uri="provider=AES://test; id=EncryptedFileSystem" dump_wal --walfile=/tmp/rocksdbtest-1000/dbbench/000004.log ./tools/ldb --fs_uri="provider=AES:test_instance_key,AES128CTR; id=EncryptedFileSystem" dump_wal --walfile=/tmp/rocksdbtest-1000/dbbench/000004.log // Dump WAL failed. Pass bad provider parameters to --fs_uri, e.g. ./tools/ldb --fs_uri="provider=AES1:test_instance_key,1AES128CTR; id=EncryptedFileSystem" dump_wal --walfile=/tmp/rocksdbtest-1000/dbbench/000004.log ./tools/ldb --fs_uri="provider=AES:bad_test_instance_key,AES128CTR; id=EncryptedFileSystem" dump_wal --walfile=/tmp/rocksdbtest-1000/dbbench/000004.log ./tools/ldb --fs_uri="provider=AES:test_instance_key,AES192CTR; id=EncryptedFileSystem" dump_wal --walfile=/tmp/rocksdbtest-1000/dbbench/000004.log // The same to other ldb tools. ``` --- .github/workflows/jobs-linux-run-tests.yml | 6 +- db/db_test.cc | 4 - db/db_test_util.cc | 17 +- db/db_test_util.h | 1 - encryption/encryption.cc | 728 +++++++++------------ encryption/encryption.h | 141 ++-- encryption/encryption_test.cc | 204 +++--- encryption/in_memory_key_manager.h | 85 --- env/env_basic_test.cc | 102 ++- env/env_encryption.cc | 58 ++ env/env_encryption_ctr.h | 4 +- env/env_test.cc | 28 +- file/filename.cc | 33 +- file/filename.h | 8 - include/rocksdb/encryption.h | 123 ---- include/rocksdb/env.h | 1 + test_util/testutil.cc | 10 - test_util/testutil.h | 67 -- tools/db_bench_tool.cc | 33 +- 19 files changed, 693 insertions(+), 960 deletions(-) delete mode 100644 encryption/in_memory_key_manager.h delete mode 100644 include/rocksdb/encryption.h diff --git a/.github/workflows/jobs-linux-run-tests.yml b/.github/workflows/jobs-linux-run-tests.yml index 5010b38e45c..5e8863b78d8 100644 --- a/.github/workflows/jobs-linux-run-tests.yml +++ b/.github/workflows/jobs-linux-run-tests.yml @@ -85,7 +85,7 @@ jobs: steps: - uses: actions/checkout@v3.5.0 - uses: "./.github/actions/pre-steps" - - run: mkdir build && cd build && cmake -DWITH_SNAPPY=0 -DWITH_ZLIB=0 -DWITH_BZ2=0 -DWITH_LZ4=0 -DWITH_ZSTD=0 .. && make V=1 -j5 && ctest -j5 -V + - run: mkdir build && cd build && cmake -DWITH_OPENSSL=0 -DWITH_SNAPPY=0 -DWITH_ZLIB=0 -DWITH_BZ2=0 -DWITH_LZ4=0 -DWITH_ZSTD=0 .. && make V=1 -j5 && ctest -j5 -V - run: "cd build/tools && ./sst_dump --help | grep -E -q 'Supported compression types: kNoCompression'" - uses: "./.github/actions/post-steps" build-linux-encrypted_env-openssl: @@ -95,5 +95,7 @@ jobs: steps: - uses: actions/checkout@v3.5.0 - uses: "./.github/actions/pre-steps" - - run: mkdir build && cd build && cmake -DWITH_OPENSSL=1 -DENCRYPTED_ENV=1 .. && make V=1 -j5 && ctest -j5 -V + - run: | + export ENCRYPTED_ENV=AES + mkdir build && cd build && cmake -DWITH_OPENSSL=1 .. && make V=1 -j5 && ctest -j5 -V - uses: "./.github/actions/post-steps" diff --git a/db/db_test.cc b/db/db_test.cc index b0c4134fae0..609f96ea5fc 100644 --- a/db/db_test.cc +++ b/db/db_test.cc @@ -2414,10 +2414,6 @@ TEST_F(DBTest, DestroyDBMetaDatabase) { } TEST_F(DBTest, SnapshotFiles) { - if (getenv("ENCRYPTED_ENV")) { - // File copy does not carry encryption key. - return; - } do { Options options = CurrentOptions(); options.write_buffer_size = 100000000; // Large write buffer diff --git a/db/db_test_util.cc b/db/db_test_util.cc index 582ab28fe82..5a64b2f3f2f 100644 --- a/db/db_test_util.cc +++ b/db/db_test_util.cc @@ -71,14 +71,15 @@ DBTestBase::DBTestBase(const std::string path, bool env_do_fsync) mem_env_ = MockEnv::Create(base_env, base_env->GetSystemClock()); } if (getenv("ENCRYPTED_ENV")) { -#ifdef OPENSSL - std::shared_ptr key_manager( - new test::TestKeyManager); - encrypted_env_ = NewKeyManagedEncryptedEnv(Env::Default(), key_manager); -#else - fprintf(stderr, "EncryptedEnv is not available without OpenSSL."); - assert(false); -#endif + std::shared_ptr provider; + std::string provider_id = getenv("ENCRYPTED_ENV"); + if (provider_id.find("=") == std::string::npos && + !EndsWith(provider_id, "://test")) { + provider_id = provider_id + "://test"; + } + EXPECT_OK(EncryptionProvider::CreateFromString(ConfigOptions(), provider_id, + &provider)); + encrypted_env_ = NewEncryptedEnv(mem_env_ ? mem_env_ : base_env, provider); } env_ = new SpecialEnv(encrypted_env_ ? encrypted_env_ : (mem_env_ ? mem_env_ : base_env)); diff --git a/db/db_test_util.h b/db/db_test_util.h index c4b42c4c40d..52e856cb349 100644 --- a/db/db_test_util.h +++ b/db/db_test_util.h @@ -29,7 +29,6 @@ #include "rocksdb/compaction_filter.h" #include "rocksdb/convenience.h" #include "rocksdb/db.h" -#include "rocksdb/encryption.h" #include "rocksdb/env.h" #include "rocksdb/file_system.h" #include "rocksdb/filter_policy.h" diff --git a/encryption/encryption.cc b/encryption/encryption.cc index 23d203a0c06..fa5c16e2d6b 100644 --- a/encryption/encryption.cc +++ b/encryption/encryption.cc @@ -1,86 +1,224 @@ -// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. +// 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 (c) 2011 The LevelDB Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. See the AUTHORS file for names of contributors. +// +// Slice is a simple structure containing a pointer into some external +// storage and a size. The user of a Slice must ensure that the slice +// is not used after the corresponding external storage has been +// deallocated. +// +// Multiple threads can invoke const methods on a Slice without +// external synchronization, but if any of the threads may call a +// non-const method, all threads accessing the same Slice must use +// external synchronization. #ifndef ROCKSDB_LITE #ifdef OPENSSL #include "encryption/encryption.h" +#include #include +#include #include #include +#include "encryption/encryption.h" #include "file/filename.h" +#include "port/likely.h" #include "port/port.h" +#include "rocksdb/utilities/options_type.h" #include "test_util/sync_point.h" namespace ROCKSDB_NAMESPACE { namespace encryption { +inline size_t KeySize(EncryptionMethod method) { + switch (method) { + case EncryptionMethod::kAES128_CTR: + return 16; + case EncryptionMethod::kAES192_CTR: + return 24; + case EncryptionMethod::kAES256_CTR: + return 32; + case EncryptionMethod::kSM4_CTR: +#if OPENSSL_VERSION_NUMBER >= 0x1010100fL && !defined(OPENSSL_NO_SM4) + return 16; +#else + return 0; +#endif + default: + return 0; + }; +} + +size_t BlockSize(EncryptionMethod method) { + switch (method) { + case EncryptionMethod::kAES128_CTR: + case EncryptionMethod::kAES192_CTR: + case EncryptionMethod::kAES256_CTR: + return AES_BLOCK_SIZE; + case EncryptionMethod::kSM4_CTR: + // TODO: OpenSSL Lib does not export SM4_BLOCK_SIZE by now. + // Need to use the macro exported from OpenSSL once it is available. + // Ref: + // https://github.com/openssl/openssl/blob/OpenSSL_1_1_1-stable/include/crypto/sm4.h#L24 +#if OPENSSL_VERSION_NUMBER >= 0x1010100fL && !defined(OPENSSL_NO_SM4) + return 16; +#else + return 0; +#endif + default: + return 0; + } +} + +EncryptionMethod EncryptionMethodStringToEnum(const std::string& method) { + if (!strcasecmp(method.c_str(), "AES128CTR")) { + return ROCKSDB_NAMESPACE::encryption::EncryptionMethod::kAES128_CTR; + } else if (!strcasecmp(method.c_str(), "AES192CTR")) { + return ROCKSDB_NAMESPACE::encryption::EncryptionMethod::kAES192_CTR; + } else if (!strcasecmp(method.c_str(), "AES256CTR")) { + return ROCKSDB_NAMESPACE::encryption::EncryptionMethod::kAES256_CTR; + } else if (!strcasecmp(method.c_str(), "SM4CTR")) { + return ROCKSDB_NAMESPACE::encryption::EncryptionMethod::kSM4_CTR; + } + return ROCKSDB_NAMESPACE::encryption::EncryptionMethod::kUnknown; +} + +const EVP_CIPHER* GetEVPCipher(EncryptionMethod method) { + switch (method) { + case EncryptionMethod::kAES128_CTR: + return EVP_aes_128_ctr(); + case EncryptionMethod::kAES192_CTR: + return EVP_aes_192_ctr(); + case EncryptionMethod::kAES256_CTR: + return EVP_aes_256_ctr(); + // case EncryptionMethod::AES128ECB: + // return EVP_aes_128_ecb(); + // case EncryptionMethod::AES192ECB: + // return EVP_aes_192_ecb(); + // case EncryptionMethod::AES256ECB: + // return EVP_aes_256_ecb(); + case EncryptionMethod::kSM4_CTR: +#if OPENSSL_VERSION_NUMBER < 0x1010100fL || defined(OPENSSL_NO_SM4) + return Status::InvalidArgument( + "Unsupport SM4 encryption method under OpenSSL version: " + + std::string(OPENSSL_VERSION_TEXT)); +#else + // Openssl support SM4 after 1.1.1 release version. + return EVP_sm4_ctr(); +#endif + default: + return nullptr; + } +} + +std::string GetOpenSSLErrors() { + std::ostringstream serr; + unsigned long l; + const char *file, *data, *func; + int line, flags; + +#if OPENSSL_VERSION_NUMBER >= 0x30000000 + l = ERR_peek_last_error_all(&file, &line, &func, &data, &flags); +#else + l = ERR_peek_last_error_line_data(&file, &line, &data, &flags); + func = ERR_func_error_string(l); +#endif + + if (l != 0) { + serr << l << ":" << ERR_lib_error_string(l) << ":" << func << ":" << file + << ":" << line; + if ((flags & ERR_TXT_STRING) && data && *data) { + serr << ":" << data; + } else { + serr << ":" << ERR_reason_error_string(l); + } + } + return serr.str(); +} + namespace { -uint64_t GetBigEndian64(const unsigned char* buf) { +const char* const kEncryptionHeaderMagic = "encrypt"; +const int kEncryptionHeaderMagicLength = 7; + +uint64_t GetBigEndian64(const unsigned char* src) { 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])); + return (static_cast(src[0]) << 56) + + (static_cast(src[1]) << 48) + + (static_cast(src[2]) << 40) + + (static_cast(src[3]) << 32) + + (static_cast(src[4]) << 24) + + (static_cast(src[5]) << 16) + + (static_cast(src[6]) << 8) + + (static_cast(src[7])); } else { - return *(reinterpret_cast(buf)); + return *(reinterpret_cast(src)); } } -void PutBigEndian64(uint64_t value, unsigned char* buf) { +void PutBigEndian64(uint64_t value, unsigned char* dst) { 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); + dst[0] = static_cast((value >> 56) & 0xff); + dst[1] = static_cast((value >> 48) & 0xff); + dst[2] = static_cast((value >> 40) & 0xff); + dst[3] = static_cast((value >> 32) & 0xff); + dst[4] = static_cast((value >> 24) & 0xff); + dst[5] = static_cast((value >> 16) & 0xff); + dst[6] = static_cast((value >> 8) & 0xff); + dst[7] = static_cast(value & 0xff); } else { - *(reinterpret_cast(buf)) = value; + *(reinterpret_cast(dst)) = value; } } -} // anonymous namespace -// AESCTRCipherStream use OpenSSL EVP API with CTR mode to encrypt and decrypt +Status GenerateFileKey(EncryptionMethod method, char* file_key) { + auto key_bytes = static_cast(KeySize(method) / 8); + OPENSSL_RET_NOT_OK( + RAND_bytes(reinterpret_cast(file_key), key_bytes), + "Failed to generate random key"); + return Status::OK(); +} + +// 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). +// 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) { +Status Cipher(const EncryptionMethod method, const std::string& key, + const uint64_t initial_iv_high, const uint64_t initial_iv_low, + uint64_t file_offset, char* data, size_t data_size, + AESCTRCipherStream::EncryptType encrypt_type) { #if OPENSSL_VERSION_NUMBER < 0x01000200f (void)file_offset; (void)data; (void)data_size; - (void)is_encrypt; + (void)encrypt_type; return Status::NotSupported("OpenSSL version < 1.0.2"); #else - int ret = 1; - EVP_CIPHER_CTX* ctx = nullptr; - InitCipherContext(ctx); - if (ctx == nullptr) { + evp_ctx_unique_ptr ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (UNLIKELY(!ctx)) { return Status::IOError("Failed to create cipher context."); } - const size_t block_size = BlockSize(); + const size_t kBlockSize = BlockSize(method); + assert(kBlockSize > 0); - uint64_t block_index = file_offset / block_size; - uint64_t block_offset = file_offset % block_size; + uint64_t block_index = file_offset / kBlockSize; + uint64_t block_offset = file_offset % kBlockSize; // In CTR mode, OpenSSL EVP API treat the IV as a 128-bit big-endien, and // increase it by 1 for each block. @@ -88,56 +226,49 @@ Status AESCTRCipherStream::Cipher(uint64_t file_offset, char* data, // 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_) { + 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[block_size]; + unsigned char iv[kBlockSize]; 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."); - } + OPENSSL_RET_NOT_OK( + EVP_CipherInit(ctx.get(), GetEVPCipher(method), + reinterpret_cast(key.data()), iv, + static_cast(encrypt_type)), + "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) { - FreeCipherContext(ctx); - return Status::IOError("Failed to disable padding for cipher context."); - } + // multiply of kBlockSize. + OPENSSL_RET_NOT_OK(EVP_CIPHER_CTX_set_padding(ctx.get(), 0), + "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[block_size]; + unsigned char partial_block[kBlockSize]; // 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 + // Handle partial block at the beginning. The partial block is copied to // buffer to fake a full block. if (block_offset > 0) { size_t partial_block_size = - std::min(block_size - block_offset, remaining_data_size); + std::min(kBlockSize - block_offset, remaining_data_size); memcpy(partial_block + block_offset, data, partial_block_size); - ret = EVP_CipherUpdate(ctx, partial_block, &output_size, partial_block, - static_cast(block_size)); - if (ret != 1) { - FreeCipherContext(ctx); - return Status::IOError("Crypter failed for first block, offset " + - std::to_string(file_offset)); - } - if (output_size != static_cast(block_size)) { - FreeCipherContext(ctx); + OPENSSL_RET_NOT_OK( + EVP_CipherUpdate(ctx.get(), partial_block, &output_size, partial_block, + static_cast(kBlockSize)), + "Crypter failed for first block, offset " + + std::to_string(file_offset)); + if (UNLIKELY(output_size != static_cast(kBlockSize))) { return Status::IOError( "Unexpected crypter output size for first block, expected " + - std::to_string(block_size) + " vs actual " + + std::to_string(kBlockSize) + " vs actual " + std::to_string(output_size)); } memcpy(data, partial_block + block_offset, partial_block_size); @@ -146,20 +277,17 @@ Status AESCTRCipherStream::Cipher(uint64_t file_offset, char* data, } // Handle full blocks in the middle. - if (remaining_data_size >= block_size) { + if (remaining_data_size >= kBlockSize) { size_t actual_data_size = - remaining_data_size - remaining_data_size % block_size; + remaining_data_size - remaining_data_size % kBlockSize; 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) { - FreeCipherContext(ctx); - return Status::IOError("Crypter failed at offset " + - std::to_string(file_offset + data_offset)); - } - if (output_size != static_cast(actual_data_size)) { - FreeCipherContext(ctx); + OPENSSL_RET_NOT_OK( + EVP_CipherUpdate(ctx.get(), full_blocks, &output_size, full_blocks, + static_cast(actual_data_size)), + "Crypter failed at offset " + + std::to_string(file_offset + data_offset)); + if (UNLIKELY(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)); @@ -168,23 +296,23 @@ Status AESCTRCipherStream::Cipher(uint64_t file_offset, char* data, remaining_data_size -= actual_data_size; } - // Handle partial block at the end. The parital block is copied to buffer to + // TODO(yingchun): Can we remove the end partial block handling if handling a + // suitable adjusted partial block at the beginning and full blocks in the + // middle? + // Handle partial block at the end. The partial block is copied to buffer to // fake a full block. if (remaining_data_size > 0) { - assert(remaining_data_size < block_size); + assert(remaining_data_size < kBlockSize); memcpy(partial_block, data + data_offset, remaining_data_size); - ret = EVP_CipherUpdate(ctx, partial_block, &output_size, partial_block, - static_cast(block_size)); - if (ret != 1) { - FreeCipherContext(ctx); - return Status::IOError("Crypter failed for last block, offset " + - std::to_string(file_offset + data_offset)); - } - if (output_size != static_cast(block_size)) { - FreeCipherContext(ctx); + OPENSSL_RET_NOT_OK( + EVP_CipherUpdate(ctx.get(), partial_block, &output_size, partial_block, + static_cast(kBlockSize)), + "Crypter failed for last block, offset " + + std::to_string(file_offset + data_offset)); + if (UNLIKELY(output_size != static_cast(kBlockSize))) { return Status::IOError( "Unexpected crypter output size for last block, expected " + - std::to_string(block_size) + " vs actual " + + std::to_string(kBlockSize) + " vs actual " + std::to_string(output_size)); } memcpy(data + data_offset, partial_block, remaining_data_size); @@ -195,366 +323,156 @@ Status AESCTRCipherStream::Cipher(uint64_t file_offset, char* data, // EVP_CipherFinal_ex to finish the last block cipher. // Reference to the implement of EVP_CipherFinal_ex: // https://github.com/openssl/openssl/blob/OpenSSL_1_1_1-stable/crypto/evp/evp_enc.c#L219 - FreeCipherContext(ctx); return Status::OK(); #endif } +} // anonymous namespace -Status NewAESCTRCipherStream(EncryptionMethod method, const std::string& key, - const std::string& iv, - std::unique_ptr* result) { - assert(result != nullptr); - 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; - case EncryptionMethod::kSM4_CTR: -#if OPENSSL_VERSION_NUMBER < 0x1010100fL || defined(OPENSSL_NO_SM4) - return Status::InvalidArgument( - "Unsupport SM4 encryption method under OpenSSL version: " + - std::string(OPENSSL_VERSION_TEXT)); -#else - // Openssl support SM4 after 1.1.1 release version. - cipher = EVP_sm4_ctr(); - break; -#endif - default: - return Status::InvalidArgument("Unsupported encryption method: " + - std::to_string(static_cast(method))); - } - if (key.size() != KeySize(method)) { - return Status::InvalidArgument( - "Encryption key size mismatch. " + std::to_string(key.size()) + - "(actual) vs. " + 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)."); - } - 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(); +size_t AESCTRCipherStream::BlockSize() { + return ROCKSDB_NAMESPACE::encryption::BlockSize(method_); } -Status AESEncryptionProvider::CreateCipherStream( - const std::string& fname, const EnvOptions& /*options*/, Slice& /*prefix*/, - std::unique_ptr* result) { - assert(result != nullptr); - FileEncryptionInfo file_info; - Status s = key_manager_->GetFile(fname, &file_info); - if (!s.ok()) { - return s; - } - std::unique_ptr cipher_stream; - s = NewAESCTRCipherStream(file_info.method, file_info.key, file_info.iv, - &cipher_stream); - if (!s.ok()) { - return s; - } - *result = std::move(cipher_stream); - return Status::OK(); +Status AESCTRCipherStream::Cipher(uint64_t file_offset, char* data, + size_t data_size, EncryptType encrypt_type) { + return ROCKSDB_NAMESPACE::encryption::Cipher( + method_, file_key_, initial_iv_high_, initial_iv_low_, file_offset, data, + data_size, encrypt_type); } -KeyManagedEncryptedEnv::KeyManagedEncryptedEnv( - Env* base_env, std::shared_ptr& key_manager, - std::shared_ptr& provider, - std::unique_ptr&& encrypted_env) - : EnvWrapper(base_env), - key_manager_(key_manager), - provider_(provider), - encrypted_env_(std::move(encrypted_env)) {} - -KeyManagedEncryptedEnv::~KeyManagedEncryptedEnv() = default; +bool AESEncryptionProvider::IsInstanceOf(const std::string& name) const { + return EncryptionProvider::IsInstanceOf(name); +} -Status KeyManagedEncryptedEnv::NewSequentialFile( - const std::string& fname, std::unique_ptr* result, - const EnvOptions& options) { - FileEncryptionInfo file_info; - Status s = key_manager_->GetFile(fname, &file_info); - if (!s.ok()) { - return s; +Status AESEncryptionProvider::ReadEncryptionHeader( + Slice prefix, FileEncryptionInfo* file_info) const { + // 1. Check the encryption header magic. + if (UNLIKELY(!prefix.starts_with(kEncryptionHeaderMagic))) { + return Status::Corruption("Invalid encryption header"); } - switch (file_info.method) { - case EncryptionMethod::kPlaintext: - s = target()->NewSequentialFile(fname, result, options); - break; - case EncryptionMethod::kAES128_CTR: - case EncryptionMethod::kAES192_CTR: - case EncryptionMethod::kAES256_CTR: - case EncryptionMethod::kSM4_CTR: - s = encrypted_env_->NewSequentialFile(fname, result, options); - // Hack: when upgrading from TiKV <= v5.0.0-rc, the old current - // file is encrypted but it could be replaced with a plaintext - // current file. The operation below guarantee that the current - // file is read correctly. - if (s.ok() && IsCurrentFile(fname)) { - if (!IsValidCurrentFile(std::move(*result))) { - s = target()->NewSequentialFile(fname, result, options); - } else { - s = encrypted_env_->NewSequentialFile(fname, result, options); - } - } - break; - default: - s = Status::InvalidArgument( - "Unsupported encryption method: " + - std::to_string(static_cast(file_info.method))); + + // 2. Read the encryption method. + auto method = EncryptionMethod(prefix[kEncryptionHeaderMagicLength]); + size_t key_size = KeySize(method); + if (UNLIKELY(key_size == 0)) { + return Status::Corruption("Unknown encryption algorithm " + + std::to_string(static_cast(method))); } - return s; -} -Status KeyManagedEncryptedEnv::NewRandomAccessFile( - const std::string& fname, std::unique_ptr* result, - const EnvOptions& options) { - FileEncryptionInfo file_info; - Status s = key_manager_->GetFile(fname, &file_info); - if (!s.ok()) { + // 3. Read the encrypted file key and decrypt it. + char encrypted_file_key[key_size]; + memcpy(encrypted_file_key, prefix.data() + kEncryptionHeaderMagicLength + 1, + key_size); + Status s = Cipher(method_, instance_key_, 0, 0, 0, encrypted_file_key, + key_size, AESCTRCipherStream::EncryptType::kDecrypt); + if (UNLIKELY(!s.ok())) { return s; } - switch (file_info.method) { - case EncryptionMethod::kPlaintext: - s = target()->NewRandomAccessFile(fname, result, options); - break; - case EncryptionMethod::kAES128_CTR: - case EncryptionMethod::kAES192_CTR: - case EncryptionMethod::kAES256_CTR: - case EncryptionMethod::kSM4_CTR: - s = encrypted_env_->NewRandomAccessFile(fname, result, options); - break; - default: - s = Status::InvalidArgument( - "Unsupported encryption method: " + - std::to_string(static_cast(file_info.method))); - } - return s; + + // 4. Fill the FileEncryptionInfo. + file_info->method = method; + file_info->key.assign(encrypted_file_key, key_size); + // TODO(yingchun): write a real IV to header_buf. + static std::string fake_iv(AES_BLOCK_SIZE, '0'); + file_info->iv = fake_iv; + return Status::OK(); } -Status KeyManagedEncryptedEnv::NewWritableFile( - const std::string& fname, std::unique_ptr* result, - const EnvOptions& options) { - FileEncryptionInfo file_info; - Status s; - bool skipped = IsCurrentFile(fname); - TEST_SYNC_POINT_CALLBACK("KeyManagedEncryptedEnv::NewWritableFile", &skipped); - if (!skipped) { - s = key_manager_->NewFile(fname, &file_info); - if (!s.ok()) { - return s; - } - } else { - file_info.method = EncryptionMethod::kPlaintext; - } +Status AESEncryptionProvider::WriteEncryptionHeader(char* header_buf) const { + auto method = EncryptionMethod(method_); + size_t key_size = KeySize(method_); + assert(key_size != 0); - switch (file_info.method) { - case EncryptionMethod::kPlaintext: - s = target()->NewWritableFile(fname, result, options); - break; - case EncryptionMethod::kAES128_CTR: - case EncryptionMethod::kAES192_CTR: - case EncryptionMethod::kAES256_CTR: - case EncryptionMethod::kSM4_CTR: - s = encrypted_env_->NewWritableFile(fname, result, options); - break; - default: - s = Status::InvalidArgument( - "Unsupported encryption method: " + - std::to_string(static_cast(file_info.method))); - } - if (!s.ok() && !skipped) { - // Ignore error - key_manager_->DeleteFile(fname); - } - return s; -} + // 1. Write the encryption header magic. + size_t offset = 0; + memcpy(header_buf, kEncryptionHeaderMagic, kEncryptionHeaderMagicLength); + offset += kEncryptionHeaderMagicLength; -Status KeyManagedEncryptedEnv::ReopenWritableFile( - const std::string& fname, std::unique_ptr* result, - const EnvOptions& options) { - FileEncryptionInfo file_info; - Status s = key_manager_->GetFile(fname, &file_info); - if (!s.ok()) { - return s; - } - switch (file_info.method) { - case EncryptionMethod::kPlaintext: - s = target()->ReopenWritableFile(fname, result, options); - break; - case EncryptionMethod::kAES128_CTR: - case EncryptionMethod::kAES192_CTR: - case EncryptionMethod::kAES256_CTR: - case EncryptionMethod::kSM4_CTR: - s = encrypted_env_->ReopenWritableFile(fname, result, options); - break; - default: - s = Status::InvalidArgument( - "Unsupported encryption method: " + - std::to_string(static_cast(file_info.method))); - } - return s; -} + // 2. Write the encryption method. + header_buf[offset] = static_cast(method_); + offset += 1; -Status KeyManagedEncryptedEnv::ReuseWritableFile( - const std::string& fname, const std::string& old_fname, - std::unique_ptr* result, const EnvOptions& options) { - FileEncryptionInfo file_info; - // ReuseWritableFile is only used in the context of rotating WAL file and - // reuse them. Old content is discardable and new WAL records are to - // overwrite the file. So NewFile() should be called. - Status s = key_manager_->NewFile(fname, &file_info); - if (!s.ok()) { - return s; - } - switch (file_info.method) { - case EncryptionMethod::kPlaintext: - s = target()->ReuseWritableFile(fname, old_fname, result, options); - break; - case EncryptionMethod::kAES128_CTR: - case EncryptionMethod::kAES192_CTR: - case EncryptionMethod::kAES256_CTR: - case EncryptionMethod::kSM4_CTR: - s = encrypted_env_->ReuseWritableFile(fname, old_fname, result, options); - break; - default: - s = Status::InvalidArgument( - "Unsupported encryption method: " + - std::to_string(static_cast(file_info.method))); - } - if (!s.ok()) { + // 3. Generate a file key, encrypt and write it. + char file_key[key_size]; + Status s = GenerateFileKey(method, file_key); + if (UNLIKELY(!s.ok())) { return s; } - s = key_manager_->LinkFile(old_fname, fname); - if (!s.ok()) { + s = Cipher(method_, instance_key_, 0, 0, 0, file_key, key_size, + AESCTRCipherStream::EncryptType::kEncrypt); + if (UNLIKELY(!s.ok())) { return s; } - s = key_manager_->DeleteFile(old_fname); - return s; -} + memcpy(header_buf + offset, file_key, key_size); + offset += key_size; -Status KeyManagedEncryptedEnv::NewRandomRWFile( - const std::string& fname, std::unique_ptr* result, - const EnvOptions& options) { - FileEncryptionInfo file_info; - // NewRandomRWFile is only used in the context of external file ingestion, - // for rewriting global seqno. So it should call GetFile() instead of - // NewFile(). - Status s = key_manager_->GetFile(fname, &file_info); - if (!s.ok()) { - return s; - } - switch (file_info.method) { - case EncryptionMethod::kPlaintext: - s = target()->NewRandomRWFile(fname, result, options); - break; - case EncryptionMethod::kAES128_CTR: - case EncryptionMethod::kAES192_CTR: - case EncryptionMethod::kAES256_CTR: - case EncryptionMethod::kSM4_CTR: - s = encrypted_env_->NewRandomRWFile(fname, result, options); - break; - default: - s = Status::InvalidArgument( - "Unsupported encryption method: " + - std::to_string(static_cast(file_info.method))); - } - if (!s.ok()) { - // Ignore error - key_manager_->DeleteFile(fname); - } - return s; -} + // 4. Pad with 0. + memset(header_buf + offset, 0, (64 - offset)); -Status KeyManagedEncryptedEnv::DeleteFile(const std::string& fname) { - // Try deleting the file from file system before updating key_manager. - Status s = target()->DeleteFile(fname); - if (!s.ok()) { - return s; - } - return key_manager_->DeleteFile(fname); + // TODO(yingchun): write IV to header_buf. + return Status::OK(); } -Status KeyManagedEncryptedEnv::LinkFile(const std::string& src_fname, - const std::string& dst_fname) { - if (IsCurrentFile(dst_fname)) { - assert(IsCurrentFile(src_fname)); - Status s = target()->LinkFile(src_fname, dst_fname); - return s; - } else { - assert(!IsCurrentFile(src_fname)); +Status AESEncryptionProvider::CreateNewPrefix(const std::string& fname, + char* prefix, + size_t prefix_length) const { + if (prefix_length != GetPrefixLength()) { + return IOStatus::Corruption("CreateNewPrefix with invalid prefix length: " + + std::to_string(prefix_length) + " for " + + fname); } - Status s = key_manager_->LinkFile(src_fname, dst_fname); + auto s = WriteEncryptionHeader(prefix); if (!s.ok()) { + s = Status::CopyAppendMessage(s, " in ", fname); return s; } - s = target()->LinkFile(src_fname, dst_fname); - if (!s.ok()) { - Status delete_status __attribute__((__unused__)) = - key_manager_->DeleteFile(dst_fname); - assert(delete_status.ok()); - } - return s; + return Status::OK(); } -Status KeyManagedEncryptedEnv::RenameFile(const std::string& src_fname, - const std::string& dst_fname) { - if (IsCurrentFile(dst_fname)) { - assert(IsCurrentFile(src_fname)); - Status s = target()->RenameFile(src_fname, dst_fname); - // Replacing with plaintext requires deleting the info in the key manager. - // The stale current file info exists when upgrading from TiKV <= v5.0.0-rc. - Status delete_status __attribute__((__unused__)) = - key_manager_->DeleteFile(dst_fname); - assert(delete_status.ok()); - return s; - } else { - assert(!IsCurrentFile(src_fname)); - } - // Link(copy)File instead of RenameFile to avoid losing src_fname info when - // failed to rename the src_fname in the file system. - Status s = key_manager_->LinkFile(src_fname, dst_fname); +Status AESEncryptionProvider::CreateCipherStream( + const std::string& fname, const EnvOptions& /*options*/, Slice& prefix, + std::unique_ptr* result) { + assert(result != nullptr); + FileEncryptionInfo file_info; + Status s = ReadEncryptionHeader(prefix, &file_info); if (!s.ok()) { + s = Status::CopyAppendMessage(s, " in ", fname); return s; } - s = target()->RenameFile(src_fname, dst_fname); - if (s.ok()) { - s = key_manager_->DeleteFileExt(src_fname, dst_fname); - } else { - Status delete_status __attribute__((__unused__)) = - key_manager_->DeleteFileExt(dst_fname, src_fname); - assert(delete_status.ok()); - } - return s; -} - -Status KeyManagedEncryptedEnv::DeleteDir(const std::string& dname) { - // We don't guarantee atomicity. Delete keys first. - Status s = key_manager_->DeleteFile(dname); + std::unique_ptr cipher_stream; + s = NewAESCTRCipherStream(file_info.method, file_info.key, file_info.iv, + &cipher_stream); if (!s.ok()) { + s = Status::CopyAppendMessage(s, " in ", fname); return s; } - return target()->DeleteDir(dname); + *result = std::move(cipher_stream); + return Status::OK(); } -Env* NewKeyManagedEncryptedEnv(Env* base_env, - std::shared_ptr& key_manager) { - std::shared_ptr provider( - new AESEncryptionProvider(key_manager.get())); - std::unique_ptr encrypted_env(NewEncryptedEnv(base_env, provider)); - return new KeyManagedEncryptedEnv(base_env, key_manager, provider, - std::move(encrypted_env)); +Status NewAESCTRCipherStream(EncryptionMethod method, + const std::string& file_key, + const std::string& file_key_iv, + std::unique_ptr* result) { + assert(result != nullptr); + if (file_key.size() != KeySize(method)) { + return Status::InvalidArgument( + "Encryption file_key size mismatch. " + + std::to_string(file_key.size()) + "(actual) vs. " + + std::to_string(KeySize(method)) + "(expected)."); + } + // TODO(yingchun): check the correction of the fixed length block size + if (file_key_iv.size() != AES_BLOCK_SIZE) { + return Status::InvalidArgument( + "file_key_iv size not equal to block cipher block size: " + + std::to_string(file_key_iv.size()) + "(actual) vs. " + + std::to_string(AES_BLOCK_SIZE) + "(expected)."); + } + uint64_t iv_high = GetBigEndian64( + reinterpret_cast(file_key_iv.data())); + uint64_t iv_low = GetBigEndian64(reinterpret_cast( + file_key_iv.data() + sizeof(uint64_t))); + result->reset(new AESCTRCipherStream(method, file_key, iv_high, iv_low)); + return Status::OK(); } } // namespace encryption diff --git a/encryption/encryption.h b/encryption/encryption.h index 6d0ac220221..22c9da7359d 100644 --- a/encryption/encryption.h +++ b/encryption/encryption.h @@ -1,4 +1,20 @@ -// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. +// 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 (c) 2011 The LevelDB Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. See the AUTHORS file for names of contributors. +// +// Slice is a simple structure containing a pointer into some external +// storage and a size. The user of a Slice must ensure that the slice +// is not used after the corresponding external storage has been +// deallocated. +// +// Multiple threads can invoke const methods on a Slice without +// external synchronization, but if any of the threads may call a +// non-const method, all threads accessing the same Slice must use +// external synchronization. #pragma once #ifndef ROCKSDB_LITE @@ -8,71 +24,76 @@ #include -#include "rocksdb/encryption.h" +#include "encryption/encryption.h" #include "rocksdb/env_encryption.h" #include "util/string_util.h" namespace ROCKSDB_NAMESPACE { namespace encryption { +class AESCTRCipherStream; + +using evp_ctx_unique_ptr = + std::unique_ptr; + +// The encryption method supported. +enum class EncryptionMethod : int { + kUnknown = 0, + kPlaintext = 1, + kAES128_CTR = 2, + kAES192_CTR = 3, + kAES256_CTR = 4, + // OpenSSL support SM4 after 1.1.1 release version. + kSM4_CTR = 5, +}; -#if OPENSSL_VERSION_NUMBER < 0x01010000f - -#define InitCipherContext(ctx) \ - EVP_CIPHER_CTX ctx##_var; \ - ctx = &ctx##_var; \ - EVP_CIPHER_CTX_init(ctx); +// Get the key size of the encryption method. +size_t KeySize(EncryptionMethod method); -// do nothing -#define FreeCipherContext(ctx) +// Get the block size of the encryption method. +size_t BlockSize(EncryptionMethod method); -#else +// Get the encryption method from string, case-insensitive. +EncryptionMethod EncryptionMethodStringToEnum(const std::string& method); -#define InitCipherContext(ctx) \ - ctx = EVP_CIPHER_CTX_new(); \ - if (ctx != nullptr) { \ - if (EVP_CIPHER_CTX_reset(ctx) != 1) { \ - ctx = nullptr; \ - } \ - } +// Get the OpenSSL EVP_CIPHER according to the encryption method. +const EVP_CIPHER* GetEVPCipher(EncryptionMethod method); -#define FreeCipherContext(ctx) EVP_CIPHER_CTX_free(ctx); +// Get the last OpenSSL error message. +std::string GetOpenSSLErrors(); -#endif +// Convert an OpenSSL error to an IOError Status. +#define OPENSSL_RET_NOT_OK(call, msg) \ + if (UNLIKELY((call) <= 0)) { \ + return Status::IOError((msg), GetOpenSSLErrors()); \ + } -// TODO: OpenSSL Lib does not export SM4_BLOCK_SIZE by now. -// Need to remove SM4_BLOCK_Size once Openssl lib support the definition. -// SM4 uses 128-bit block size as AES. -// Ref: -// https://github.com/openssl/openssl/blob/OpenSSL_1_1_1-stable/include/crypto/sm4.h#L24 -#define SM4_BLOCK_SIZE 16 +Status NewAESCTRCipherStream(EncryptionMethod method, + const std::string& file_key, + const std::string& file_key_iv, + std::unique_ptr* result); +// The cipher stream for AES-CTR encryption. class AESCTRCipherStream : public BlockAccessCipherStream { public: - AESCTRCipherStream(const EVP_CIPHER* cipher, const std::string& key, + AESCTRCipherStream(const EncryptionMethod method, const std::string& file_key, uint64_t iv_high, uint64_t iv_low) - : cipher_(cipher), - key_(key), + : method_(method), + file_key_(file_key), initial_iv_high_(iv_high), initial_iv_low_(iv_low) {} ~AESCTRCipherStream() = default; - size_t BlockSize() override { - // Openssl support SM4 after 1.1.1 release version. -#if OPENSSL_VERSION_NUMBER >= 0x1010100fL && !defined(OPENSSL_NO_SM4) - if (EVP_CIPHER_nid(cipher_) == NID_sm4_ctr) { - return SM4_BLOCK_SIZE; - } -#endif - return AES_BLOCK_SIZE; // 16 - } + size_t BlockSize() override; + + enum class EncryptType : int { kDecrypt = 0, kEncrypt = 1 }; Status Encrypt(uint64_t file_offset, char* data, size_t data_size) override { - return Cipher(file_offset, data, data_size, true /*is_encrypt*/); + return Cipher(file_offset, data, data_size, EncryptType::kEncrypt); } Status Decrypt(uint64_t file_offset, char* data, size_t data_size) override { - return Cipher(file_offset, data, data_size, false /*is_encrypt*/); + return Cipher(file_offset, data, data_size, EncryptType::kDecrypt); } protected: @@ -95,31 +116,30 @@ class AESCTRCipherStream : public BlockAccessCipherStream { private: Status Cipher(uint64_t file_offset, char* data, size_t data_size, - bool is_encrypt); + EncryptType encrypt_type); - const EVP_CIPHER* cipher_; - const std::string key_; + const EncryptionMethod method_; + const std::string file_key_; const uint64_t initial_iv_high_; const uint64_t initial_iv_low_; }; -extern Status NewAESCTRCipherStream( - EncryptionMethod method, const std::string& key, const std::string& iv, - std::unique_ptr* result); - +// TODO(yingchun): Is it possible to derive from CTREncryptionProvider? +// The encryption provider for AES-CTR encryption. class AESEncryptionProvider : public EncryptionProvider { public: - AESEncryptionProvider(KeyManager* key_manager) : key_manager_(key_manager) {} + AESEncryptionProvider(std::string instance_key, EncryptionMethod method) + : instance_key_(std::move(instance_key)), method_(method) {} virtual ~AESEncryptionProvider() = default; - const char* Name() const override { return "AESEncryptionProvider"; } + static const char* kClassName() { return "AES"; } + const char* Name() const override { return kClassName(); } + bool IsInstanceOf(const std::string& name) const override; - size_t GetPrefixLength() const override { return 0; } + size_t GetPrefixLength() const override { return kDefaultPageSize; } - Status CreateNewPrefix(const std::string& /*fname*/, char* /*prefix*/, - size_t /*prefix_length*/) const override { - return Status::OK(); - } + Status CreateNewPrefix(const std::string& /*fname*/, char* prefix, + size_t prefix_length) const override; Status AddCipher(const std::string& /*descriptor*/, const char* /*cipher*/, size_t /*len*/, bool /*for_write*/) override { @@ -131,7 +151,18 @@ class AESEncryptionProvider : public EncryptionProvider { std::unique_ptr* result) override; private: - KeyManager* key_manager_; + struct FileEncryptionInfo { + EncryptionMethod method = EncryptionMethod::kUnknown; + std::string key; + std::string iv; // TODO(yingchun): not used yet + }; + + Status WriteEncryptionHeader(char* header_buf) const; + Status ReadEncryptionHeader(Slice prefix, + FileEncryptionInfo* file_info) const; + + const std::string instance_key_; + const EncryptionMethod method_; }; } // namespace encryption diff --git a/encryption/encryption_test.cc b/encryption/encryption_test.cc index 26513ccba56..903585e2bd7 100644 --- a/encryption/encryption_test.cc +++ b/encryption/encryption_test.cc @@ -1,7 +1,26 @@ -// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. +// 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 (c) 2011 The LevelDB Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. See the AUTHORS file for names of contributors. +// +// Slice is a simple structure containing a pointer into some external +// storage and a size. The user of a Slice must ensure that the slice +// is not used after the corresponding external storage has been +// deallocated. +// +// Multiple threads can invoke const methods on a Slice without +// external synchronization, but if any of the threads may call a +// non-const method, all threads accessing the same Slice must use +// external synchronization. #include "encryption/encryption.h" +#include + +#include "fuzz/util.h" #include "port/stack_trace.h" #include "test_util/testharness.h" #include "test_util/testutil.h" @@ -12,9 +31,13 @@ namespace ROCKSDB_NAMESPACE { namespace encryption { +// Make sure the length of KEY is larger than the max KeySize(EncryptionMethod). 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"; + +// Make sure the length of IV_RANDOM, IV_OVERFLOW_LOW and IV_OVERFLOW_FULL is +// larger than the max BlockSize(EncryptionMethod). 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] = @@ -22,134 +45,152 @@ const unsigned char IV_OVERFLOW_LOW[17] = 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(EncryptionTest, KeySize) { + ASSERT_EQ(16, KeySize(EncryptionMethod::kAES128_CTR)); + ASSERT_EQ(24, KeySize(EncryptionMethod::kAES192_CTR)); + ASSERT_EQ(32, KeySize(EncryptionMethod::kAES256_CTR)); + ASSERT_EQ(16, KeySize(EncryptionMethod::kSM4_CTR)); + ASSERT_EQ(0, KeySize(EncryptionMethod::kUnknown)); +} + +TEST(EncryptionTest, BlockSize) { + ASSERT_EQ(16, BlockSize(EncryptionMethod::kAES128_CTR)); + ASSERT_EQ(16, BlockSize(EncryptionMethod::kAES192_CTR)); + ASSERT_EQ(16, BlockSize(EncryptionMethod::kAES256_CTR)); + ASSERT_EQ(16, BlockSize(EncryptionMethod::kSM4_CTR)); + ASSERT_EQ(0, BlockSize(EncryptionMethod::kUnknown)); +} + +TEST(EncryptionTest, EncryptionMethodStringToEnum) { + ASSERT_EQ(EncryptionMethod::kAES128_CTR, + EncryptionMethodStringToEnum("AES128CTR")); + ASSERT_EQ(EncryptionMethod::kAES192_CTR, + EncryptionMethodStringToEnum("AES192CTR")); + ASSERT_EQ(EncryptionMethod::kAES256_CTR, + EncryptionMethodStringToEnum("AES256CTR")); + ASSERT_EQ(EncryptionMethod::kSM4_CTR, EncryptionMethodStringToEnum("SM4CTR")); + ASSERT_EQ(EncryptionMethod::kUnknown, + EncryptionMethodStringToEnum("unknown")); +} // Test to make sure output of AESCTRCipherStream is the same as output from // OpenSSL EVP API. class EncryptionTest - : public testing::TestWithParam> { + : public testing::TestWithParam< + std::tuple> { 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]; + int kMaxSize; + std::unique_ptr plaintext; + std::unique_ptr ciphertext; + const unsigned char* current_iv = nullptr; + + EncryptionTest() : kMaxSize(10 * BlockSize(std::get<1>(GetParam()))) { + CHECK_OK(ReGenerateCiphertext(IV_RANDOM)); + } - void GenerateCiphertext(const unsigned char* iv) { - Random rnd(666); + Status ReGenerateCiphertext(const unsigned char* iv) { + current_iv = iv; + + Random rnd(test::RandomSeed()); std::string random_string = - rnd.HumanReadableString(static_cast(MAX_SIZE)); - memcpy(plaintext, random_string.data(), MAX_SIZE); + rnd.HumanReadableString(static_cast(kMaxSize)); + plaintext.reset(new unsigned char[kMaxSize]); + memcpy(plaintext.get(), random_string.data(), kMaxSize); - int ret = 1; - EVP_CIPHER_CTX* ctx; - InitCipherContext(ctx); - assert(ctx != nullptr); + evp_ctx_unique_ptr ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + CHECK_TRUE(ctx); - 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; -#if OPENSSL_VERSION_NUMBER >= 0x1010100fL && !defined(OPENSSL_NO_SM4) - // Openssl support SM4 after 1.1.1 release version. - case EncryptionMethod::kSM4_CTR: - cipher = EVP_sm4_ctr(); - break; -#endif - default: - assert(false); - } - assert(cipher != nullptr); + const EVP_CIPHER* cipher = GetEVPCipher(method); + CHECK_TRUE(cipher != nullptr); - ret = EVP_EncryptInit(ctx, cipher, KEY, iv); - assert(ret == 1); + OPENSSL_RET_NOT_OK(EVP_EncryptInit(ctx.get(), cipher, KEY, current_iv), + "EVP_EncryptInit failed."); int output_size = 0; - ret = EVP_EncryptUpdate(ctx, ciphertext, &output_size, plaintext, - static_cast(MAX_SIZE)); - assert(ret == 1); + ciphertext.reset(new unsigned char[kMaxSize]); + OPENSSL_RET_NOT_OK( + EVP_EncryptUpdate(ctx.get(), ciphertext.get(), &output_size, + plaintext.get(), static_cast(kMaxSize)), + "EVP_EncryptUpdate failed."); 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); + OPENSSL_RET_NOT_OK( + EVP_EncryptFinal(ctx.get(), ciphertext.get() + output_size, + &final_output_size), + "EVP_EncryptFinal failed."); + CHECK_EQ(kMaxSize, output_size + final_output_size); + return Status::OK(); } - void TestEncryptionImpl(size_t start, size_t end, const unsigned char* iv, - bool* success) { - assert(start < end && end <= MAX_SIZE); - GenerateCiphertext(iv); + void TestEncryption(size_t start, size_t end) { + ASSERT_LT(start, end); + ASSERT_LE(end, kMaxSize); EncryptionMethod method = std::get<1>(GetParam()); std::string key_str(reinterpret_cast(KEY), KeySize(method)); - std::string iv_str(reinterpret_cast(iv), 16); + std::string iv_str(reinterpret_cast(current_iv), + BlockSize(method)); std::unique_ptr cipher_stream; ASSERT_OK(NewAESCTRCipherStream(method, key_str, iv_str, &cipher_stream)); + ASSERT_TRUE(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); + if (std::get<0>(GetParam()) == AESCTRCipherStream::EncryptType::kEncrypt) { + memcpy(data.get(), plaintext.get() + start, data_size); ASSERT_OK(cipher_stream->Encrypt(start, data.get(), data_size)); - ASSERT_EQ(0, memcmp(ciphertext + start, data.get(), data_size)); + ASSERT_EQ(0, memcmp(ciphertext.get() + start, data.get(), data_size)); } else { - // Decrypt - memcpy(data.get(), ciphertext + start, data_size); + ASSERT_EQ(AESCTRCipherStream::EncryptType::kDecrypt, + std::get<0>(GetParam())); + memcpy(data.get(), ciphertext.get() + start, data_size); ASSERT_OK(cipher_stream->Decrypt(start, data.get(), data_size)); - ASSERT_EQ(0, memcmp(plaintext + start, data.get(), data_size)); + ASSERT_EQ(0, memcmp(plaintext.get() + 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) { +TEST_P(EncryptionTest, AESCTRCipherStreamTest) { + const size_t kBlockSize = BlockSize(std::get<1>(GetParam())); + // TODO(yingchun): The following tests are based on the fact that the + // kBlockSize is 16, make sure they work if adding new encryption methods. + ASSERT_EQ(kBlockSize, 16); + // One full block. - EXPECT_TRUE(TestEncryption(0, 16)); + ASSERT_NO_FATAL_FAILURE(TestEncryption(0, kBlockSize)); // One block in the middle. - EXPECT_TRUE(TestEncryption(16 * 5, 16 * 6)); + ASSERT_NO_FATAL_FAILURE(TestEncryption(kBlockSize * 5, kBlockSize * 6)); // Multiple aligned blocks. - EXPECT_TRUE(TestEncryption(16 * 5, 16 * 8)); + ASSERT_NO_FATAL_FAILURE(TestEncryption(kBlockSize * 5, kBlockSize * 8)); // Random byte at the beginning of a block. - EXPECT_TRUE(TestEncryption(16 * 5, 16 * 5 + 1)); + ASSERT_NO_FATAL_FAILURE(TestEncryption(kBlockSize * 5, kBlockSize * 5 + 1)); // Random byte in the middle of a block. - EXPECT_TRUE(TestEncryption(16 * 5 + 4, 16 * 5 + 5)); + ASSERT_NO_FATAL_FAILURE( + TestEncryption(kBlockSize * 5 + 4, kBlockSize * 5 + 5)); // Random byte at the end of a block. - EXPECT_TRUE(TestEncryption(16 * 5 + 15, 16 * 6)); + ASSERT_NO_FATAL_FAILURE(TestEncryption(kBlockSize * 5 + 15, kBlockSize * 6)); // Partial block aligned at the beginning. - EXPECT_TRUE(TestEncryption(16 * 5, 16 * 5 + 15)); + ASSERT_NO_FATAL_FAILURE(TestEncryption(kBlockSize * 5, kBlockSize * 5 + 15)); // Partial block aligned at the end. - EXPECT_TRUE(TestEncryption(16 * 5 + 1, 16 * 6)); + ASSERT_NO_FATAL_FAILURE(TestEncryption(kBlockSize * 5 + 1, kBlockSize * 6)); // Multiple blocks with a partial block at the end. - EXPECT_TRUE(TestEncryption(16 * 5, 16 * 8 + 15)); + ASSERT_NO_FATAL_FAILURE(TestEncryption(kBlockSize * 5, kBlockSize * 8 + 15)); // Multiple blocks with a partial block at the beginning. - EXPECT_TRUE(TestEncryption(16 * 5 + 1, 16 * 8)); + ASSERT_NO_FATAL_FAILURE(TestEncryption(kBlockSize * 5 + 1, kBlockSize * 8)); // Partial block at both ends. - EXPECT_TRUE(TestEncryption(16 * 5 + 1, 16 * 8 + 15)); + ASSERT_NO_FATAL_FAILURE( + TestEncryption(kBlockSize * 5 + 1, kBlockSize * 8 + 15)); // Lower bits of IV overflow. - EXPECT_TRUE(TestEncryption(16, 16 * 2, IV_OVERFLOW_LOW)); + ASSERT_OK(ReGenerateCiphertext(IV_OVERFLOW_LOW)); + ASSERT_NO_FATAL_FAILURE(TestEncryption(kBlockSize, kBlockSize * 2)); // Full IV overflow. - EXPECT_TRUE(TestEncryption(16, 16 * 2, IV_OVERFLOW_FULL)); + ASSERT_OK(ReGenerateCiphertext(IV_OVERFLOW_FULL)); + ASSERT_NO_FATAL_FAILURE(TestEncryption(kBlockSize, kBlockSize * 2)); } // Openssl support SM4 after 1.1.1 release version. @@ -163,7 +204,8 @@ INSTANTIATE_TEST_CASE_P( #else INSTANTIATE_TEST_CASE_P( EncryptionTestInstance, EncryptionTest, - testing::Combine(testing::Bool(), + testing::Combine(testing::Values(AESCTRCipherStream::EncryptType::kEncrypt, + AESCTRCipherStream::EncryptType::kDecrypt), testing::Values(EncryptionMethod::kAES128_CTR, EncryptionMethod::kAES192_CTR, EncryptionMethod::kAES256_CTR, diff --git a/encryption/in_memory_key_manager.h b/encryption/in_memory_key_manager.h deleted file mode 100644 index 6f7e216c3c3..00000000000 --- a/encryption/in_memory_key_manager.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once -#ifndef ROCKSDB_LITE -#ifdef OPENSSL -#include - -#include -#include - -#include "encryption/encryption.h" -#include "port/port.h" -#include "test_util/testutil.h" -#include "util/mutexlock.h" - -namespace ROCKSDB_NAMESPACE { -namespace encryption { - -// KeyManager store metadata in memory. It is used in tests and db_bench only. -class InMemoryKeyManager final : public KeyManager { - public: - InMemoryKeyManager(EncryptionMethod method) - : rnd_(42), - method_(method), - key_(rnd_.HumanReadableString(static_cast(KeySize(method)))) { - assert(method != EncryptionMethod::kUnknown); - } - - virtual ~InMemoryKeyManager() = default; - - Status GetFile(const std::string& fname, - FileEncryptionInfo* file_info) override { - assert(file_info != nullptr); - MutexLock l(&mu_); - if (files_.count(fname) == 0) { - return Status::Corruption("File not found: " + fname); - } - file_info->method = method_; - file_info->key = key_; - file_info->iv = files_[fname]; - return Status::OK(); - } - - Status NewFile(const std::string& fname, - FileEncryptionInfo* file_info) override { - assert(file_info != nullptr); - MutexLock l(&mu_); - std::string iv = rnd_.HumanReadableString(AES_BLOCK_SIZE); - files_[fname] = iv; - file_info->method = method_; - file_info->key = key_; - file_info->iv = iv; - return Status::OK(); - } - - Status DeleteFile(const std::string& fname) override { - MutexLock l(&mu_); - if (files_.count(fname) == 0) { - return Status::Corruption("File not found: " + fname); - } - files_.erase(fname); - return Status::OK(); - } - - Status LinkFile(const std::string& src_fname, - const std::string& dst_fname) override { - MutexLock l(&mu_); - if (files_.count(src_fname) == 0) { - return Status::Corruption("File not found: " + src_fname); - } - files_[dst_fname] = files_[src_fname]; - return Status::OK(); - } - - private: - mutable port::Mutex mu_; - Random rnd_; - const EncryptionMethod method_; - const std::string key_; - std::unordered_map files_; -}; - -} // namespace encryption -} // namespace ROCKSDB_NAMESPACE - -#endif // OPENSSL -#endif // !ROCKSDB_LITE diff --git a/env/env_basic_test.cc b/env/env_basic_test.cc index fc31b6b22f1..4f20940164f 100644 --- a/env/env_basic_test.cc +++ b/env/env_basic_test.cc @@ -10,6 +10,7 @@ #include #include "db/db_test_util.h" +#include "encryption/encryption.h" #include "env/mock_env.h" #include "file/file_util.h" #include "rocksdb/convenience.h" @@ -83,12 +84,47 @@ static Env* GetTestFS() { } #ifdef OPENSSL -static Env* GetKeyManagedEncryptedEnv() { - static std::shared_ptr key_manager( - new test::TestKeyManager); - static std::unique_ptr key_managed_encrypted_env( - NewKeyManagedEncryptedEnv(Env::Default(), key_manager)); - return key_managed_encrypted_env.get(); +const std::string kTestInstanceKey = "test_instance_key"; + +static Env* GetAes128EncryptedEnv() { + static Env* env = nullptr; + auto provider = std::make_shared( + kTestInstanceKey, encryption::EncryptionMethod::kAES128_CTR); + env = NewEncryptedEnv(Env::Default(), provider); + return env; +} + +static Env* GetAes192EncryptedEnv() { + static Env* env = nullptr; + auto provider = std::make_shared( + kTestInstanceKey, encryption::EncryptionMethod::kAES192_CTR); + env = NewEncryptedEnv(Env::Default(), provider); + return env; +} + +static Env* GetAes256EncryptedEnv() { + static Env* env = nullptr; + auto provider = std::make_shared( + kTestInstanceKey, encryption::EncryptionMethod::kAES256_CTR); + env = NewEncryptedEnv(Env::Default(), provider); + return env; +} + +static Env* GetSm4EncryptedEnv() { + static Env* env = nullptr; + auto provider = std::make_shared( + kTestInstanceKey, encryption::EncryptionMethod::kSM4_CTR); + env = NewEncryptedEnv(Env::Default(), provider); + return env; +} + +std::vector GetEncryptedEnvs() { + std::vector res; + res.push_back(&GetAes128EncryptedEnv); + res.push_back(&GetAes192EncryptedEnv); + res.push_back(&GetAes256EncryptedEnv); + res.push_back(&GetSm4EncryptedEnv); + return res; } #endif // OPENSSL @@ -130,8 +166,8 @@ INSTANTIATE_TEST_CASE_P(MemEnv, EnvBasicTestWithParam, ::testing::Values(&GetMemoryEnv)); #ifdef OPENSSL -INSTANTIATE_TEST_CASE_P(KeyManagedEncryptedEnv, EnvBasicTestWithParam, - ::testing::Values(&GetKeyManagedEncryptedEnv)); +INSTANTIATE_TEST_CASE_P(AesEncryptedEnv, EnvBasicTestWithParam, + ::testing::ValuesIn(GetEncryptedEnvs())); #endif // OPENSSL namespace { @@ -160,52 +196,10 @@ INSTANTIATE_TEST_CASE_P(CustomEnv, EnvBasicTestWithParam, INSTANTIATE_TEST_CASE_P(CustomEnv, EnvMoreTestWithParam, ::testing::ValuesIn(GetCustomEnvs())); - -TEST_P(EnvBasicTestWithParam, RenameCurrent) { - if (!getenv("ENCRYPTED_ENV")) { - return; - } - Slice result; - char scratch[100]; - std::unique_ptr seq_file; - std::unique_ptr writable_file; - std::vector children; - - // Create an encrypted `CURRENT` file so it shouldn't be skipped . - SyncPoint::GetInstance()->SetCallBack( - "KeyManagedEncryptedEnv::NewWritableFile", [&](void* arg) { - bool* skip = static_cast(arg); - *skip = false; - }); - SyncPoint::GetInstance()->EnableProcessing(); - ASSERT_OK( - env_->NewWritableFile(test_dir_ + "/CURRENT", &writable_file, soptions_)); - SyncPoint::GetInstance()->ClearAllCallBacks(); - SyncPoint::GetInstance()->DisableProcessing(); - ASSERT_OK(writable_file->Append("MANIFEST-0")); - ASSERT_OK(writable_file->Close()); - writable_file.reset(); - - ASSERT_OK( - env_->NewSequentialFile(test_dir_ + "/CURRENT", &seq_file, soptions_)); - ASSERT_OK(seq_file->Read(100, &result, scratch)); - ASSERT_EQ(0, result.compare("MANIFEST-0")); - - // Create a plaintext `CURRENT` temp file. - ASSERT_OK(env_->NewWritableFile(test_dir_ + "/current.dbtmp.plain", - &writable_file, soptions_)); - ASSERT_OK(writable_file->Append("MANIFEST-1")); - ASSERT_OK(writable_file->Close()); - writable_file.reset(); - - ASSERT_OK(env_->RenameFile(test_dir_ + "/current.dbtmp.plain", - test_dir_ + "/CURRENT")); - - ASSERT_OK( - env_->NewSequentialFile(test_dir_ + "/CURRENT", &seq_file, soptions_)); - ASSERT_OK(seq_file->Read(100, &result, scratch)); - ASSERT_EQ(0, result.compare("MANIFEST-1")); -} +#ifdef OPENSSL +INSTANTIATE_TEST_CASE_P(AesEncryptedEnv, EnvMoreTestWithParam, + ::testing::ValuesIn(GetEncryptedEnvs())); +#endif // OPENSSL TEST_P(EnvBasicTestWithParam, Basics) { uint64_t file_size; diff --git a/env/env_encryption.cc b/env/env_encryption.cc index 7b2a531c424..0fa21bb7eaf 100644 --- a/env/env_encryption.cc +++ b/env/env_encryption.cc @@ -11,6 +11,7 @@ #include #include +#include "encryption/encryption.h" #include "env/composite_env_wrapper.h" #include "env/env_encryption_ctr.h" #include "monitoring/perf_context_imp.h" @@ -1170,6 +1171,63 @@ static void RegisterEncryptionBuiltins() { return guard->get(); }); + +#ifdef OPENSSL + // Match "AES" and "AES://test" + auto func = lib->AddFactory( + ObjectLibrary::PatternEntry( + encryption::AESEncryptionProvider::kClassName(), true) + .AddSuffix("://test"), + [](const std::string& uri, std::unique_ptr* guard, + std::string* errmsg) { + errmsg->clear(); + std::string instance_key = "test_instance_key"; + encryption::EncryptionMethod method = + encryption::EncryptionMethod::kAES128_CTR; + encryption::AESEncryptionProvider* provider = nullptr; + + // Parse the uri to arguments to construct an AESEncryptionProvider. + do { + if (uri == encryption::AESEncryptionProvider::kClassName()) { + break; + } + if (EndsWith(uri, "://test")) { + break; + } + auto type_args = StringSplit(uri, ':'); + if (type_args.size() != 2) { + *errmsg = "Invalid EncryptionProvider URI: " + uri; + break; + } + auto args = StringSplit(type_args[1], ','); + if (args.size() != 2) { + *errmsg = "Invalid EncryptionProvider URI: " + uri; + break; + } + instance_key = args[0]; + method = encryption::EncryptionMethodStringToEnum(args[1]); + } while (false); + + // Construct the provider if no error occurs. + if (errmsg->empty()) { + // TODO(yingchun): check instance_key + assert(method != encryption::EncryptionMethod::kUnknown); + provider = + new encryption::AESEncryptionProvider(instance_key, method); + } + + guard->reset(provider); + return guard->get(); + }); + + // Match "AES:," + lib->AddFactory( + ObjectLibrary::PatternEntry( + encryption::AESEncryptionProvider::kClassName(), false) + .AddSeparator(":") + .AddSeparator(","), + func); +#endif }); } } // namespace diff --git a/env/env_encryption_ctr.h b/env/env_encryption_ctr.h index b4342f7012d..31e537f5a18 100644 --- a/env/env_encryption_ctr.h +++ b/env/env_encryption_ctr.h @@ -9,8 +9,8 @@ #include "rocksdb/env_encryption.h" namespace ROCKSDB_NAMESPACE { -// CTRCipherStream implements BlockAccessCipherStream using an -// Counter operations mode. +// CTRCipherStream implements BlockAccessCipherStream using a +// Counter operation mode. // See https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation // // Note: This is a possible implementation of BlockAccessCipherStream, diff --git a/env/env_test.cc b/env/env_test.cc index 4462b95b81f..ceab961d8f8 100644 --- a/env/env_test.cc +++ b/env/env_test.cc @@ -36,6 +36,7 @@ #endif #include "db/db_impl/db_impl.h" +#include "encryption/encryption.h" #include "env/emulated_clock.h" #include "env/env_chroot.h" #include "env/env_encryption_ctr.h" @@ -2901,11 +2902,31 @@ TEST_F(CreateEnvTest, CreateChrootFileSystem) { } #endif // OS_WIN -TEST_F(CreateEnvTest, CreateEncryptedFileSystem) { +class CreateEncryptedEnvTest : public CreateEnvTest, + public testing::WithParamInterface { +}; + +#ifdef OPENSSL +INSTANTIATE_TEST_CASE_P(CreateEnvTest, CreateEncryptedEnvTest, + ::testing::Values( + // CTREncryptionProvider + std::string("provider=1://test; id=") + + EncryptedFileSystem::kClassName(), + // AESEncryptionProvider + std::string("provider=AES://test; id=") + + EncryptedFileSystem::kClassName())); +#else +INSTANTIATE_TEST_CASE_P(CreateEnvTest, CreateEncryptedEnvTest, + ::testing::Values( + // CTREncryptionProvider + std::string("provider=1://test; id=") + + EncryptedFileSystem::kClassName())); +#endif // OPENSSL + +TEST_P(CreateEncryptedEnvTest, CreateEncryptedFileSystem) { std::shared_ptr fs, copy; - std::string base_opts = - std::string("provider=1://test; id=") + EncryptedFileSystem::kClassName(); + std::string base_opts = GetParam(); // The EncryptedFileSystem requires a "provider" option. ASSERT_NOK(FileSystem::CreateFromString( config_options_, EncryptedFileSystem::kClassName(), &fs)); @@ -2932,7 +2953,6 @@ TEST_F(CreateEnvTest, CreateEncryptedFileSystem) { ASSERT_TRUE(fs->AreEquivalent(config_options_, copy.get(), &mismatch)); } - namespace { constexpr size_t kThreads = 8; diff --git a/file/filename.cc b/file/filename.cc index b911ac6272b..1e04c73395e 100644 --- a/file/filename.cc +++ b/file/filename.cc @@ -30,33 +30,6 @@ static const std::string kRocksDbTFileExt = "sst"; static const std::string kLevelDbTFileExt = "ldb"; static const std::string kRocksDBBlobFileExt = "blob"; static const std::string kArchivalDirName = "archive"; -static const std::string kUnencryptedTempFileNameSuffix = "dbtmp.plain"; - -bool IsCurrentFile(const std::string& fname) { - // skip CURRENT file. - size_t current_length = strlen("CURRENT"); - if (fname.length() >= current_length && - !fname.compare(fname.length() - current_length, current_length, - "CURRENT")) { - return true; - } - // skip temporary file for CURRENT file. - size_t temp_length = kUnencryptedTempFileNameSuffix.length(); - if (fname.length() >= temp_length && - !fname.compare(fname.length() - temp_length, temp_length, - kUnencryptedTempFileNameSuffix)) { - return true; - } - return false; -} - -bool IsValidCurrentFile( - std::unique_ptr seq_file) { - Slice result; - char scratch[64]; - seq_file->Read(8, &result, scratch); - return result.compare("MANIFEST") == 0; -} // Given a path, flatten the path name by replacing all chars not in // {[0-9,a-z,A-Z,-,_,.]} with _. And append '_LOG\0' at the end. @@ -210,10 +183,6 @@ std::string TempFileName(const std::string& dbname, uint64_t number) { return MakeFileName(dbname, number, kTempFileNameSuffix.c_str()); } -std::string TempPlainFileName(const std::string& dbname, uint64_t number) { - return MakeFileName(dbname, number, kUnencryptedTempFileNameSuffix.c_str()); -} - InfoLogPrefix::InfoLogPrefix(bool has_log_dir, const std::string& db_absolute_path) { if (!has_log_dir) { @@ -424,7 +393,7 @@ IOStatus SetCurrentFile(FileSystem* fs, const std::string& dbname, Slice contents = manifest; assert(contents.starts_with(dbname + "/")); contents.remove_prefix(dbname.size() + 1); - std::string tmp = TempPlainFileName(dbname, descriptor_number); + std::string tmp = TempFileName(dbname, descriptor_number); IOStatus s = WriteStringToFile(fs, contents.ToString() + "\n", tmp, true); TEST_SYNC_POINT_CALLBACK("SetCurrentFile:BeforeRename", &s); if (s.ok()) { diff --git a/file/filename.h b/file/filename.h index c85293ba79a..2eb125b6a17 100644 --- a/file/filename.h +++ b/file/filename.h @@ -37,14 +37,6 @@ constexpr char kFilePathSeparator = '\\'; constexpr char kFilePathSeparator = '/'; #endif -// Some non-sensitive files are not encrypted to preserve atomicity of file -// operations. -extern bool IsCurrentFile(const std::string& fname); - -// Determine if the content is read from the valid current file. -extern bool IsValidCurrentFile( - std::unique_ptr seq_file); - // Return the name of the log file with the specified number // in the db named by "dbname". The result will be prefixed with // "dbname". diff --git a/include/rocksdb/encryption.h b/include/rocksdb/encryption.h deleted file mode 100644 index c9c54760e25..00000000000 --- a/include/rocksdb/encryption.h +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. - -#pragma once -#ifndef ROCKSDB_LITE -#ifdef OPENSSL - -#include -#include - -#include "rocksdb/env.h" - -namespace ROCKSDB_NAMESPACE { -namespace encryption { - -class AESEncryptionProvider; - -enum class EncryptionMethod : int { - kUnknown = 0, - kPlaintext = 1, - kAES128_CTR = 2, - kAES192_CTR = 3, - kAES256_CTR = 4, - kSM4_CTR = 5, -}; - -inline size_t KeySize(EncryptionMethod method) { - switch (method) { - case EncryptionMethod::kAES128_CTR: - return 16; - case EncryptionMethod::kAES192_CTR: - return 24; - case EncryptionMethod::kAES256_CTR: - return 32; - case EncryptionMethod::kSM4_CTR: - return 16; - default: - return 0; - }; -} - -struct FileEncryptionInfo { - EncryptionMethod method = EncryptionMethod::kUnknown; - std::string key; - std::string iv; -}; - -// Interface to manage encryption keys for files. KeyManagedEncryptedEnv -// will query KeyManager for the key being used for each file, and update -// KeyManager when it creates a new file or moving files around. -class KeyManager { - public: - virtual ~KeyManager() = default; - - virtual Status GetFile(const std::string& fname, - FileEncryptionInfo* file_info) = 0; - virtual Status NewFile(const std::string& fname, - FileEncryptionInfo* file_info) = 0; - // Used with both file and directory. - virtual Status DeleteFile(const std::string& fname) = 0; - virtual Status LinkFile(const std::string& src_fname, - const std::string& dst_fname) = 0; - // Provide additional hint of physical file when the key name doesn't map to - // one. A typical use case of this is atomically deleting a directory by - // renaming it first. - virtual Status DeleteFileExt(const std::string& fname, - const std::string& /*physical_fname*/) { - return DeleteFile(fname); - } -}; - -// An Env with underlying files being encrypted. It holds a reference to an -// external KeyManager for encryption key management. -class KeyManagedEncryptedEnv : public EnvWrapper { - public: - KeyManagedEncryptedEnv(Env* base_env, - std::shared_ptr& key_manager, - std::shared_ptr& provider, - std::unique_ptr&& encrypted_env); - - virtual ~KeyManagedEncryptedEnv(); - - Status NewSequentialFile(const std::string& fname, - std::unique_ptr* result, - const EnvOptions& options) override; - Status NewRandomAccessFile(const std::string& fname, - std::unique_ptr* result, - const EnvOptions& options) override; - Status NewWritableFile(const std::string& fname, - std::unique_ptr* result, - const EnvOptions& options) override; - Status ReopenWritableFile(const std::string& fname, - std::unique_ptr* result, - const EnvOptions& options) override; - Status ReuseWritableFile(const std::string& fname, - const std::string& old_fname, - std::unique_ptr* result, - const EnvOptions& options) override; - Status NewRandomRWFile(const std::string& fname, - std::unique_ptr* result, - const EnvOptions& options) override; - - Status DeleteFile(const std::string& fname) override; - Status LinkFile(const std::string& src_fname, - const std::string& dst_fname) override; - Status RenameFile(const std::string& src_fname, - const std::string& dst_fname) override; - - Status DeleteDir(const std::string& dname) override; - - private: - const std::shared_ptr key_manager_; - const std::shared_ptr provider_; - const std::unique_ptr encrypted_env_; -}; - -extern Env* NewKeyManagedEncryptedEnv(Env* base_env, - std::shared_ptr& key_manager); - -} // namespace encryption -} // namespace ROCKSDB_NAMESPACE - -#endif // OPENSSL -#endif // !ROCKSDB_LITE diff --git a/include/rocksdb/env.h b/include/rocksdb/env.h index ea99a7a9e44..b894591bb62 100644 --- a/include/rocksdb/env.h +++ b/include/rocksdb/env.h @@ -279,6 +279,7 @@ class Env : public Customizable { std::unique_ptr* result, const EnvOptions& options); + // TODO(yingchun): it's not true, it will not create a file when not exists. // Open `fname` for random read and write, if file doesn't exist the file // will be created. On success, stores a pointer to the new file in // *result and returns OK. On failure returns non-OK. diff --git a/test_util/testutil.cc b/test_util/testutil.cc index 20d32c77fde..d958cb0cd3e 100644 --- a/test_util/testutil.cc +++ b/test_util/testutil.cc @@ -37,16 +37,6 @@ void RegisterCustomObjects(int /*argc*/, char** /*argv*/) {} namespace ROCKSDB_NAMESPACE { namespace test { -#ifdef OPENSSL -#ifndef ROCKSDB_LITE -const std::string TestKeyManager::default_key = - "\x12\x34\x56\x78\x12\x34\x56\x78\x12\x34\x56\x78\x12\x34\x56\x78\x12\x34" - "\x56\x78\x12\x34\x56\x78"; -const std::string TestKeyManager::default_iv = - "\xaa\xbb\xcc\xdd\xaa\xbb\xcc\xdd\xaa\xbb\xcc\xdd\xaa\xbb\xcc\xdd"; -#endif -#endif - const uint32_t kDefaultFormatVersion = BlockBasedTableOptions().format_version; const std::set kFooterFormatVersionsToTest{ 5U, diff --git a/test_util/testutil.h b/test_util/testutil.h index 279830d84e0..c40fcdcb0d9 100644 --- a/test_util/testutil.h +++ b/test_util/testutil.h @@ -10,17 +10,12 @@ #pragma once #include #include -#include -#include #include #include #include "env/composite_env_wrapper.h" -#include "file/filename.h" #include "file/writable_file_writer.h" #include "rocksdb/compaction_filter.h" -#include "rocksdb/db.h" -#include "rocksdb/encryption.h" #include "rocksdb/env.h" #include "rocksdb/iterator.h" #include "rocksdb/merge_operator.h" @@ -48,68 +43,6 @@ class SequentialFileReader; namespace test { -// TODO(yiwu): Use InMemoryKeyManager instead for tests. -#ifdef OPENSSL -#ifndef ROCKSDB_LITE -class TestKeyManager : public encryption::KeyManager { - public: - virtual ~TestKeyManager() = default; - - static const std::string default_key; - static const std::string default_iv; - std::mutex mutex; - std::set file_set; - - Status GetFile(const std::string& fname, - encryption::FileEncryptionInfo* file_info) override { - std::lock_guard l(mutex); - if (file_set.find(fname) == file_set.end()) { - file_info->method = encryption::EncryptionMethod::kPlaintext; - } else { - file_info->method = encryption::EncryptionMethod::kAES192_CTR; - } - file_info->key = default_key; - file_info->iv = default_iv; - return Status::OK(); - } - - Status NewFile(const std::string& fname, - encryption::FileEncryptionInfo* file_info) override { - std::lock_guard l(mutex); - file_info->method = encryption::EncryptionMethod::kAES192_CTR; - file_info->key = default_key; - file_info->iv = default_iv; - file_set.insert(fname); - return Status::OK(); - } - - Status DeleteFile(const std::string& fname) override { - std::lock_guard l(mutex); - file_set.erase(fname); - if (!fname.empty()) { - std::string copy = fname; - if (copy.back() != '/') { - copy.push_back('/'); - } - auto begin = file_set.lower_bound(copy); - auto end = begin; - while (end != file_set.end() && end->compare(0, copy.size(), copy) == 0) { - end++; - } - file_set.erase(begin, end); - } - return Status::OK(); - } - - Status LinkFile(const std::string& /*src*/, const std::string& dst) override { - std::lock_guard l(mutex); - file_set.insert(dst); - return Status::OK(); - } -}; -#endif -#endif - extern const uint32_t kDefaultFormatVersion; extern const std::set kFooterFormatVersionsToTest; diff --git a/tools/db_bench_tool.cc b/tools/db_bench_tool.cc index f75e23f3359..a0ed3bb10da 100644 --- a/tools/db_bench_tool.cc +++ b/tools/db_bench_tool.cc @@ -41,7 +41,7 @@ #include "db/db_impl/db_impl.h" #include "db/malloc_stats.h" #include "db/version_set.h" -#include "encryption/in_memory_key_manager.h" +#include "encryption/encryption.h" #include "monitoring/histogram.h" #include "monitoring/statistics_impl.h" #include "options/cf_options.h" @@ -50,7 +50,6 @@ #include "rocksdb/cache.h" #include "rocksdb/convenience.h" #include "rocksdb/db.h" -#include "rocksdb/encryption.h" #include "rocksdb/env.h" #include "rocksdb/filter_policy.h" #include "rocksdb/memtablerep.h" @@ -1744,9 +1743,15 @@ DEFINE_bool(build_info, false, DEFINE_bool(track_and_verify_wals_in_manifest, false, "If true, enable WAL tracking in the MANIFEST"); +#ifdef OPENSSL DEFINE_string( encryption_method, "", - "If non-empty, enable encryption with the specific encryption method."); + "If non-empty, enable encryption with the specific encryption method. Now " + "supports AES128CTR, AES192CTR, AES256CTR and SM4CTR."); + +DEFINE_string(encryption_instance_key, "", + "Instance key in plain-text to create the encrypted Env."); +#endif namespace ROCKSDB_NAMESPACE { namespace { @@ -8572,26 +8577,16 @@ int db_bench_tool(int argc, char** argv) { #ifdef OPENSSL if (!FLAGS_encryption_method.empty()) { - ROCKSDB_NAMESPACE::encryption::EncryptionMethod method = - ROCKSDB_NAMESPACE::encryption::EncryptionMethod::kUnknown; - if (!strcasecmp(FLAGS_encryption_method.c_str(), "AES128CTR")) { - method = ROCKSDB_NAMESPACE::encryption::EncryptionMethod::kAES128_CTR; - } else if (!strcasecmp(FLAGS_encryption_method.c_str(), "AES192CTR")) { - method = ROCKSDB_NAMESPACE::encryption::EncryptionMethod::kAES192_CTR; - } else if (!strcasecmp(FLAGS_encryption_method.c_str(), "AES256CTR")) { - method = ROCKSDB_NAMESPACE::encryption::EncryptionMethod::kAES256_CTR; - } else if (!strcasecmp(FLAGS_encryption_method.c_str(), "SM4CTR")) { - method = ROCKSDB_NAMESPACE::encryption::EncryptionMethod::kSM4_CTR; - } - if (method == ROCKSDB_NAMESPACE::encryption::EncryptionMethod::kUnknown) { + encryption::EncryptionMethod method = + encryption::EncryptionMethodStringToEnum(FLAGS_encryption_method); + if (method == encryption::EncryptionMethod::kUnknown) { fprintf(stderr, "Unknown encryption method %s\n", FLAGS_encryption_method.c_str()); exit(1); } - std::shared_ptr key_manager( - new ROCKSDB_NAMESPACE::encryption::InMemoryKeyManager(method)); - FLAGS_env = ROCKSDB_NAMESPACE::encryption::NewKeyManagedEncryptedEnv( - FLAGS_env, key_manager); + auto provider = std::make_shared( + FLAGS_encryption_instance_key, method); + FLAGS_env = NewEncryptedEnv(FLAGS_env, provider); } #endif // OPENSSL