blob: ed6d15dbb6ac83b50a669207f309427e8557ffff [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include <gtest/gtest.h>
#include <iostream>
#include <string>
#include <thread>
#include <unordered_map>
#include "arrow/filesystem/filesystem.h"
#include "arrow/filesystem/localfs.h"
#include "arrow/io/file.h"
#include "arrow/status.h"
#include "arrow/testing/gtest_util.h"
#include "arrow/testing/util.h"
#include "arrow/util/config.h"
#include "arrow/util/logging.h"
#include "parquet/encryption/crypto_factory.h"
#include "parquet/encryption/key_toolkit.h"
#include "parquet/encryption/test_encryption_util.h"
#include "parquet/encryption/test_in_memory_kms.h"
#include "parquet/file_reader.h"
#include "parquet/test_util.h"
namespace parquet::encryption::test {
class TestEncryptionKeyManagement : public ::testing::Test {
protected:
std::unique_ptr<TemporaryDir> temp_dir_;
FileEncryptor encryptor_;
FileDecryptor decryptor_;
std::unordered_map<std::string, SecureString> key_list_;
std::unordered_map<std::string, SecureString> new_key_list_;
std::string column_key_mapping_;
KmsConnectionConfig kms_connection_config_;
CryptoFactory crypto_factory_;
bool wrap_locally_;
void SetUp() {
#ifndef ARROW_WITH_SNAPPY
GTEST_SKIP() << "Test requires Snappy compression";
#endif
key_list_ = BuildKeyMap(kColumnMasterKeyIds, kColumnMasterKeys, kFooterMasterKeyId,
kFooterMasterKey);
new_key_list_ = BuildKeyMap(kColumnMasterKeyIds, kNewColumnMasterKeys,
kFooterMasterKeyId, kNewFooterMasterKey);
column_key_mapping_ = BuildColumnKeyMapping();
temp_dir_ = temp_data_dir().ValueOrDie();
}
void SetupCryptoFactory(bool wrap_locally) {
wrap_locally_ = wrap_locally;
std::shared_ptr<KmsClientFactory> kms_client_factory =
std::make_shared<TestOnlyInMemoryKmsClientFactory>(wrap_locally, key_list_);
crypto_factory_.RegisterKmsClientFactory(std::move(kms_client_factory));
}
std::string GetFileName(bool double_wrapping, bool wrap_locally,
bool internal_key_material, int encryption_no) {
std::string file_name;
file_name += double_wrapping ? "double_wrapping" : "no_double_wrapping";
file_name += wrap_locally ? "-wrap_locally" : "-wrap_on_server";
file_name +=
internal_key_material ? "-internal_key_material" : "-external_key_material";
switch (encryption_no) {
case 0:
file_name += "-encrypt_columns_and_footer_diff_keys";
break;
case 1:
file_name += "-encrypt_columns_not_footer";
break;
case 2:
file_name += "-encrypt_columns_and_footer_same_keys";
break;
case 3:
file_name += "-encrypt_columns_and_footer_ctr";
break;
default:
file_name += "-no_encrypt";
break;
}
file_name += encryption_no == 4 ? ".parquet" : ".parquet.encrypted";
return file_name;
}
EncryptionConfiguration GetEncryptionConfiguration(bool double_wrapping,
bool internal_key_material,
int encryption_no) {
EncryptionConfiguration encryption(kFooterMasterKeyId);
encryption.double_wrapping = double_wrapping;
switch (encryption_no) {
case 0:
// encrypt some columns and footer, different keys
encryption.column_keys = column_key_mapping_;
break;
case 1:
// encrypt columns, plaintext footer, different keys
encryption.column_keys = column_key_mapping_;
encryption.plaintext_footer = true;
break;
case 2:
// encrypt some columns and footer, same key
encryption.uniform_encryption = true;
break;
case 3:
// Encrypt two columns and the footer, with different keys.
// Use AES_GCM_CTR_V1 algorithm.
encryption.column_keys = column_key_mapping_;
encryption.encryption_algorithm = ParquetCipher::AES_GCM_CTR_V1;
break;
default:
// no encryption
ARROW_LOG(FATAL) << "Invalid encryption_no";
}
encryption.internal_key_material = internal_key_material;
return encryption;
}
DecryptionConfiguration GetDecryptionConfiguration() {
return DecryptionConfiguration();
}
void WriteEncryptedParquetFile(bool double_wrapping, bool internal_key_material,
int encryption_no) {
std::string file_name =
GetFileName(double_wrapping, wrap_locally_, internal_key_material, encryption_no);
auto encryption_config =
GetEncryptionConfiguration(double_wrapping, internal_key_material, encryption_no);
std::string file_path = temp_dir_->path().ToString() + file_name;
if (internal_key_material) {
auto file_encryption_properties = crypto_factory_.GetFileEncryptionProperties(
kms_connection_config_, encryption_config);
encryptor_.EncryptFile(file_path, file_encryption_properties);
} else {
auto file_system = std::make_shared<::arrow::fs::LocalFileSystem>();
auto file_encryption_properties = crypto_factory_.GetFileEncryptionProperties(
kms_connection_config_, encryption_config, file_path, file_system);
encryptor_.EncryptFile(file_path, file_encryption_properties);
}
}
void ReadEncryptedParquetFile(bool double_wrapping, bool internal_key_material,
int encryption_no) {
auto decryption_config = GetDecryptionConfiguration();
std::string file_name =
GetFileName(double_wrapping, wrap_locally_, internal_key_material, encryption_no);
std::string file_path = temp_dir_->path().ToString() + file_name;
if (internal_key_material) {
auto file_decryption_properties = crypto_factory_.GetFileDecryptionProperties(
kms_connection_config_, decryption_config);
decryptor_.DecryptFile(file_path, file_decryption_properties);
} else {
auto file_system = std::make_shared<::arrow::fs::LocalFileSystem>();
auto file_decryption_properties = crypto_factory_.GetFileDecryptionProperties(
kms_connection_config_, decryption_config, file_path, file_system);
decryptor_.DecryptFile(file_path, file_decryption_properties);
}
}
void RotateKeys(bool double_wrapping, int encryption_no) {
auto file_system = std::make_shared<::arrow::fs::LocalFileSystem>();
std::string file_name =
GetFileName(double_wrapping, wrap_locally_, false, encryption_no);
std::string file = temp_dir_->path().ToString() + file_name;
crypto_factory_.RemoveCacheEntriesForAllTokens();
TestOnlyInServerWrapKms::StartKeyRotation(new_key_list_);
crypto_factory_.RotateMasterKeys(kms_connection_config_, file, file_system,
double_wrapping);
TestOnlyInServerWrapKms::FinishKeyRotation();
crypto_factory_.RemoveCacheEntriesForAllTokens();
}
// Create encryption properties without keeping the creating CryptoFactory alive
std::shared_ptr<FileEncryptionProperties> GetOrphanedFileEncryptionProperties(
std::shared_ptr<KmsClientFactory> kms_client_factory,
EncryptionConfiguration encryption_config) {
std::shared_ptr<CryptoFactory> crypto_factory = std::make_shared<CryptoFactory>();
crypto_factory->RegisterKmsClientFactory(kms_client_factory);
return crypto_factory->GetFileEncryptionProperties(kms_connection_config_,
encryption_config);
}
// Create decryption properties without keeping the creating CryptoFactory alive
std::shared_ptr<FileDecryptionProperties> GetOrphanedFileDecryptionProperties(
std::shared_ptr<KmsClientFactory> kms_client_factory,
DecryptionConfiguration decryption_config) {
std::shared_ptr<CryptoFactory> crypto_factory = std::make_shared<CryptoFactory>();
crypto_factory->RegisterKmsClientFactory(kms_client_factory);
return crypto_factory->GetFileDecryptionProperties(kms_connection_config_,
decryption_config);
}
};
class TestEncryptionKeyManagementMultiThread : public TestEncryptionKeyManagement {
protected:
void WriteEncryptedParquetFiles() {
std::vector<std::thread> write_threads;
for (const bool internal_key_material : {false, true}) {
for (const bool double_wrapping : {false, true}) {
for (int encryption_no = 0; encryption_no < 4; encryption_no++) {
write_threads.push_back(std::thread(
[this, double_wrapping, internal_key_material, encryption_no]() {
this->WriteEncryptedParquetFile(double_wrapping, internal_key_material,
encryption_no);
}));
}
}
}
for (auto& th : write_threads) {
th.join();
}
}
void ReadEncryptedParquetFiles() {
std::vector<std::thread> read_threads;
for (const bool internal_key_material : {false, true}) {
for (const bool double_wrapping : {false, true}) {
for (int encryption_no = 0; encryption_no < 4; encryption_no++) {
read_threads.push_back(std::thread(
[this, double_wrapping, internal_key_material, encryption_no]() {
this->ReadEncryptedParquetFile(double_wrapping, internal_key_material,
encryption_no);
}));
}
}
}
for (auto& th : read_threads) {
th.join();
}
}
};
TEST_F(TestEncryptionKeyManagement, WrapLocally) {
this->SetupCryptoFactory(true);
for (const bool internal_key_material : {false, true}) {
for (const bool double_wrapping : {false, true}) {
for (int encryption_no = 0; encryption_no < 4; encryption_no++) {
this->WriteEncryptedParquetFile(double_wrapping, internal_key_material,
encryption_no);
this->ReadEncryptedParquetFile(double_wrapping, internal_key_material,
encryption_no);
}
}
}
}
TEST_F(TestEncryptionKeyManagement, WrapOnServer) {
this->SetupCryptoFactory(false);
for (const bool internal_key_material : {false, true}) {
for (const bool double_wrapping : {false, true}) {
for (int encryption_no = 0; encryption_no < 4; encryption_no++) {
this->WriteEncryptedParquetFile(double_wrapping, internal_key_material,
encryption_no);
this->ReadEncryptedParquetFile(double_wrapping, internal_key_material,
encryption_no);
}
}
}
}
TEST_F(TestEncryptionKeyManagement, CheckExternalKeyStoreWithNullFilePath) {
bool internal_key_material = false;
bool double_wrapping = true;
int encryption_no = 0;
this->SetupCryptoFactory(false);
auto encryption_config =
GetEncryptionConfiguration(double_wrapping, internal_key_material, encryption_no);
EXPECT_THROW(crypto_factory_.GetFileEncryptionProperties(kms_connection_config_,
encryption_config),
ParquetException);
}
TEST_F(TestEncryptionKeyManagement, CheckKeyRotationDoubleWrapping) {
bool internal_key_material =
false; // Key rotation is not supported for internal key material
bool double_wrapping = true;
this->SetupCryptoFactory(
false); // key rotation is not supported with local key wrapping
for (int encryption_no = 0; encryption_no < 4; encryption_no++) {
TestOnlyInServerWrapKms::InitializeMasterKeys(key_list_);
this->WriteEncryptedParquetFile(double_wrapping, internal_key_material,
encryption_no);
this->ReadEncryptedParquetFile(double_wrapping, internal_key_material, encryption_no);
this->RotateKeys(double_wrapping, encryption_no);
this->ReadEncryptedParquetFile(double_wrapping, internal_key_material, encryption_no);
}
}
TEST_F(TestEncryptionKeyManagement, CheckKeyRotationSingleWrapping) {
bool internal_key_material =
false; // Key rotation is not supported for internal key material
bool double_wrapping = false;
this->SetupCryptoFactory(
false); // key rotation is not supported with local key wrapping
for (int encryption_no = 0; encryption_no < 4; encryption_no++) {
TestOnlyInServerWrapKms::InitializeMasterKeys(key_list_);
this->WriteEncryptedParquetFile(double_wrapping, internal_key_material,
encryption_no);
this->ReadEncryptedParquetFile(double_wrapping, internal_key_material, encryption_no);
this->RotateKeys(double_wrapping, encryption_no);
this->ReadEncryptedParquetFile(double_wrapping, internal_key_material, encryption_no);
}
}
TEST_F(TestEncryptionKeyManagement, KeyRotationWithInternalMaterial) {
bool internal_key_material = true;
bool double_wrapping = false;
this->SetupCryptoFactory(false);
int encryption_no = 0;
TestOnlyInServerWrapKms::InitializeMasterKeys(key_list_);
this->WriteEncryptedParquetFile(double_wrapping, internal_key_material, encryption_no);
// Key rotation requires external key material so this should throw an exception
EXPECT_THROW(this->RotateKeys(double_wrapping, encryption_no), ParquetException);
}
TEST_F(TestEncryptionKeyManagement, UsePropertiesAfterCrytoFactoryDestroyed) {
constexpr bool wrap_locally = true;
std::shared_ptr<KmsClientFactory> kms_client_factory =
std::make_shared<TestOnlyInMemoryKmsClientFactory>(wrap_locally, key_list_);
constexpr bool double_wrapping = true;
constexpr bool internal_key_material = true;
constexpr int encryption_no = 0;
std::string file_name =
GetFileName(double_wrapping, wrap_locally, internal_key_material, encryption_no);
std::string file_path = temp_dir_->path().ToString() + file_name;
// Encrypt file using encryption properties without holding a reference to the
// CryptoFactory that created them
auto encryption_config =
GetEncryptionConfiguration(double_wrapping, internal_key_material, encryption_no);
auto file_encryption_properties =
GetOrphanedFileEncryptionProperties(kms_client_factory, encryption_config);
encryptor_.EncryptFile(file_path, file_encryption_properties);
// Decrypt file using decryption properties without holding a reference to the
// CryptoFactory that created them
auto decryption_config = GetDecryptionConfiguration();
auto file_decryption_properties =
GetOrphanedFileDecryptionProperties(kms_client_factory, decryption_config);
decryptor_.DecryptFile(file_path, file_decryption_properties);
}
TEST_F(TestEncryptionKeyManagementMultiThread, WrapLocally) {
#ifndef ARROW_ENABLE_THREADING
GTEST_SKIP() << "Test requires threading support";
#endif
this->SetupCryptoFactory(true);
this->WriteEncryptedParquetFiles();
this->ReadEncryptedParquetFiles();
}
TEST_F(TestEncryptionKeyManagementMultiThread, WrapOnServer) {
#ifndef ARROW_ENABLE_THREADING
GTEST_SKIP() << "Test requires threading support";
#endif
this->SetupCryptoFactory(false);
this->WriteEncryptedParquetFiles();
this->ReadEncryptedParquetFiles();
}
// Test reading a file written with parquet-mr using external key material
TEST_F(TestEncryptionKeyManagement, ReadParquetMRExternalKeyMaterialFile) {
// This test file was created using the same key identifiers used in the test KMS,
// so we don't need to modify the setup.
this->SetupCryptoFactory(false);
auto file_path = data_file("external_key_material_java.parquet.encrypted");
auto file_system = std::make_shared<::arrow::fs::LocalFileSystem>();
auto decryption_config = GetDecryptionConfiguration();
auto file_decryption_properties = crypto_factory_.GetFileDecryptionProperties(
kms_connection_config_, decryption_config, file_path, file_system);
parquet::ReaderProperties reader_properties = parquet::default_reader_properties();
reader_properties.file_decryption_properties(file_decryption_properties);
std::shared_ptr<::arrow::io::RandomAccessFile> source;
PARQUET_ASSIGN_OR_THROW(source, ::arrow::io::ReadableFile::Open(
file_path, reader_properties.memory_pool()));
auto file_reader = parquet::ParquetFileReader::Open(source, reader_properties);
auto file_metadata = file_reader->metadata();
ASSERT_EQ(file_metadata->num_row_groups(), 1);
ASSERT_EQ(file_metadata->num_columns(), 2);
auto row_group = file_reader->RowGroup(0);
auto num_rows = row_group->metadata()->num_rows();
ASSERT_EQ(num_rows, 100);
std::vector<int> int_values(num_rows);
std::vector<parquet::ByteArray> string_values(num_rows);
int64_t values_read;
auto int_reader = std::dynamic_pointer_cast<parquet::Int32Reader>(row_group->Column(0));
int_reader->ReadBatch(num_rows, nullptr, nullptr, int_values.data(), &values_read);
ASSERT_EQ(values_read, num_rows);
auto string_reader =
std::dynamic_pointer_cast<parquet::ByteArrayReader>(row_group->Column(1));
string_reader->ReadBatch(num_rows, nullptr, nullptr, string_values.data(),
&values_read);
ASSERT_EQ(values_read, num_rows);
std::vector<std::string> prefixes = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"};
for (int64_t row = 0; row < num_rows; ++row) {
ASSERT_EQ(int_values[row], row);
std::string expected_string = prefixes[row % prefixes.size()] + std::to_string(row);
std::string_view read_string(reinterpret_cast<const char*>(string_values[row].ptr),
string_values[row].len);
ASSERT_EQ(read_string, expected_string);
}
}
} // namespace parquet::encryption::test