| // 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 <stdio.h> |
| |
| #include <fstream> |
| |
| #include "arrow/io/file.h" |
| #include "arrow/testing/gtest_compat.h" |
| #include "arrow/util/config.h" |
| |
| #include "parquet/column_reader.h" |
| #include "parquet/column_writer.h" |
| #include "parquet/encryption/test_encryption_util.h" |
| #include "parquet/file_reader.h" |
| #include "parquet/test_util.h" |
| |
| /* |
| * This file contains a unit-test for reading encrypted Parquet files with |
| * different decryption configurations. |
| * |
| * The unit-test is called multiple times, each time to decrypt parquet files using |
| * different decryption configuration as described below. |
| * In each call two encrypted files are read: one temporary file that was generated using |
| * write_configurations_test.cc test and will be deleted upon |
| * reading it, while the second resides in |
| * parquet-testing/data repository. Those two encrypted files were encrypted using the |
| * same encryption configuration. |
| * The encrypted parquet file names are passed as parameter to the unit-test. |
| * |
| * A detailed description of the Parquet Modular Encryption specification can be found |
| * here: |
| * https://github.com/apache/parquet-format/blob/encryption/Encryption.md |
| * |
| * The following decryption configurations are used to decrypt each parquet file: |
| * |
| * - Decryption configuration 1: Decrypt using key retriever that holds the keys of |
| * two encrypted columns and the footer key. |
| * - Decryption configuration 2: Decrypt using key retriever that holds the keys of |
| * two encrypted columns and the footer key. Supplies |
| * aad_prefix to verify file identity. |
| * - Decryption configuration 3: Decrypt using explicit column and footer keys |
| * (instead of key retrieval callback). |
| * - Decryption Configuration 4: PlainText Footer mode - test legacy reads, |
| * read the footer + all non-encrypted columns. |
| * (pairs with encryption configuration 3) |
| * |
| * The encrypted parquet files that are read were encrypted using one of the |
| * configurations below: |
| * |
| * - Encryption configuration 1: Encrypt all columns and the footer with the same key. |
| * (uniform encryption) |
| * - Encryption configuration 2: Encrypt two columns and the footer, with different |
| * keys. |
| * - Encryption configuration 3: Encrypt two columns, with different keys. |
| * Don’t encrypt footer (to enable legacy readers) |
| * - plaintext footer mode. |
| * - Encryption configuration 4: Encrypt two columns and the footer, with different |
| * keys. Supply aad_prefix for file identity |
| * verification. |
| * - Encryption configuration 5: Encrypt two columns and the footer, with different |
| * keys. Supply aad_prefix, and call |
| * disable_aad_prefix_storage to prevent file |
| * identity storage in file metadata. |
| * - Encryption configuration 6: Encrypt two columns and the footer, with different |
| * keys. Use the alternative (AES_GCM_CTR_V1) algorithm. |
| |
| */ |
| |
| namespace parquet::encryption::test { |
| |
| using parquet::test::ParquetTestException; |
| |
| class TestDecryptionConfiguration |
| : public testing::TestWithParam<std::tuple<int, const char*>> { |
| public: |
| void SetUp() { |
| #ifndef ARROW_WITH_SNAPPY |
| GTEST_SKIP() << "Test requires Snappy compression"; |
| #endif |
| CreateDecryptionConfigurations(); |
| } |
| |
| protected: |
| FileDecryptor decryptor_; |
| const std::string path_to_double_field_ = kDoubleFieldName; |
| const std::string path_to_float_field_ = kFloatFieldName; |
| // This vector will hold various decryption configurations. |
| std::vector<std::shared_ptr<parquet::FileDecryptionProperties>> |
| vector_of_decryption_configurations_; |
| const SecureString kFooterEncryptionKey_ = kFooterEncryptionKey; |
| const SecureString kColumnEncryptionKey1_ = kColumnEncryptionKey1; |
| const SecureString kColumnEncryptionKey2_ = kColumnEncryptionKey2; |
| const std::string kFileName_ = std::string(kFileName); |
| |
| void CreateDecryptionConfigurations() { |
| /********************************************************************************** |
| Creating a number of Decryption configurations |
| **********************************************************************************/ |
| |
| // Decryption configuration 1: Decrypt using key retriever callback that holds the |
| // keys of two encrypted columns and the footer key. |
| std::shared_ptr<parquet::StringKeyIdRetriever> string_kr1 = |
| std::make_shared<parquet::StringKeyIdRetriever>(); |
| string_kr1->PutKey("kf", kFooterEncryptionKey_); |
| string_kr1->PutKey("kc1", kColumnEncryptionKey1_); |
| string_kr1->PutKey("kc2", kColumnEncryptionKey2_); |
| std::shared_ptr<parquet::DecryptionKeyRetriever> kr1 = |
| std::static_pointer_cast<parquet::StringKeyIdRetriever>(string_kr1); |
| |
| parquet::FileDecryptionProperties::Builder file_decryption_builder_1; |
| vector_of_decryption_configurations_.push_back( |
| file_decryption_builder_1.key_retriever(std::move(kr1))->build()); |
| |
| // Decryption configuration 2: Decrypt using key retriever callback that holds the |
| // keys of two encrypted columns and the footer key. Supply aad_prefix. |
| std::shared_ptr<parquet::StringKeyIdRetriever> string_kr2 = |
| std::make_shared<parquet::StringKeyIdRetriever>(); |
| string_kr2->PutKey("kf", kFooterEncryptionKey_); |
| string_kr2->PutKey("kc1", kColumnEncryptionKey1_); |
| string_kr2->PutKey("kc2", kColumnEncryptionKey2_); |
| std::shared_ptr<parquet::DecryptionKeyRetriever> kr2 = |
| std::static_pointer_cast<parquet::StringKeyIdRetriever>(string_kr2); |
| |
| parquet::FileDecryptionProperties::Builder file_decryption_builder_2; |
| vector_of_decryption_configurations_.push_back( |
| file_decryption_builder_2.key_retriever(std::move(kr2)) |
| ->aad_prefix(kFileName_) |
| ->build()); |
| |
| // Decryption configuration 3: Decrypt using explicit column and footer keys. Supply |
| // aad_prefix. |
| std::string path_float_ptr = kFloatFieldName; |
| std::string path_double_ptr = kDoubleFieldName; |
| std::map<std::string, std::shared_ptr<parquet::ColumnDecryptionProperties>> |
| decryption_cols; |
| parquet::ColumnDecryptionProperties::Builder decryption_col_builder31( |
| path_double_ptr); |
| parquet::ColumnDecryptionProperties::Builder decryption_col_builder32(path_float_ptr); |
| |
| decryption_cols[path_double_ptr] = |
| decryption_col_builder31.key(kColumnEncryptionKey1_)->build(); |
| decryption_cols[path_float_ptr] = |
| decryption_col_builder32.key(kColumnEncryptionKey2_)->build(); |
| |
| parquet::FileDecryptionProperties::Builder file_decryption_builder_3; |
| vector_of_decryption_configurations_.push_back( |
| file_decryption_builder_3.footer_key(kFooterEncryptionKey_) |
| ->column_keys(std::move(decryption_cols)) |
| ->build()); |
| |
| // Decryption Configuration 4: use plaintext footer mode, read only footer + plaintext |
| // columns. |
| vector_of_decryption_configurations_.push_back(NULL); |
| } |
| |
| void DecryptFileInternal( |
| const std::string& file, int decryption_config_num, |
| std::function<void(const std::string& file, |
| const std::shared_ptr<FileDecryptionProperties>&)> |
| decrypt_func) { |
| std::shared_ptr<FileDecryptionProperties> file_decryption_properties; |
| if (vector_of_decryption_configurations_[decryption_config_num]) { |
| file_decryption_properties = |
| vector_of_decryption_configurations_[decryption_config_num]; |
| } |
| |
| decrypt_func(std::move(file), std::move(file_decryption_properties)); |
| } |
| |
| std::shared_ptr<FileDecryptionProperties> GetDecryptionProperties( |
| int decryption_config_num) { |
| const auto props = vector_of_decryption_configurations_[decryption_config_num]; |
| return props; |
| } |
| |
| void DecryptFile(const std::string& file, int decryption_config_num) { |
| decryptor_.DecryptFile(file, GetDecryptionProperties(decryption_config_num)); |
| } |
| |
| void DecryptPageIndex(const std::string& file, int decryption_config_num) { |
| decryptor_.DecryptPageIndex(file, GetDecryptionProperties(decryption_config_num)); |
| } |
| |
| // Check that the decryption result is as expected. |
| void CheckResults(const std::string& file_name, unsigned decryption_config_num, |
| unsigned encryption_config_num) { |
| // Encryption_configuration number five contains aad_prefix and |
| // disable_aad_prefix_storage. |
| // An exception is expected to be thrown if the file is not decrypted with aad_prefix. |
| if (encryption_config_num == 5) { |
| if (decryption_config_num == 1 || decryption_config_num == 3) { |
| EXPECT_THROW(DecryptFile(file_name, decryption_config_num - 1), ParquetException); |
| EXPECT_THROW(DecryptPageIndex(file_name, decryption_config_num - 1), |
| ParquetException); |
| return; |
| } |
| } |
| // Decryption configuration number two contains aad_prefix. An exception is expected |
| // to be thrown if the file was not encrypted with the same aad_prefix. |
| if (decryption_config_num == 2) { |
| if (encryption_config_num != 5 && encryption_config_num != 4) { |
| EXPECT_THROW(DecryptFile(file_name, decryption_config_num - 1), ParquetException); |
| EXPECT_THROW(DecryptPageIndex(file_name, decryption_config_num - 1), |
| ParquetException); |
| return; |
| } |
| } |
| |
| // decryption config 4 can only work when the encryption configuration is 3 |
| if (decryption_config_num == 4 && encryption_config_num != 3) { |
| return; |
| } |
| EXPECT_NO_THROW(DecryptFile(file_name, decryption_config_num - 1)); |
| EXPECT_NO_THROW(DecryptPageIndex(file_name, decryption_config_num - 1)); |
| } |
| |
| // Returns true if file exists. Otherwise returns false. |
| bool fexists(const std::string& filename) { |
| std::ifstream ifile(filename.c_str()); |
| return ifile.good(); |
| } |
| }; |
| |
| // Read encrypted parquet file. |
| // The test reads two parquet files that were encrypted using the same encryption |
| // configuration: |
| // one was generated in write_configurations_test.cc tests and is deleted |
| // once the file is read and the second exists in parquet-testing/data folder. |
| // The name of the files are passed as parameters to the unit-test. |
| TEST_P(TestDecryptionConfiguration, TestDecryption) { |
| int encryption_config_num = std::get<0>(GetParam()); |
| const char* param_file_name = std::get<1>(GetParam()); |
| // Decrypt parquet file that was generated in write_configurations_test.cc test. |
| std::string tmp_file_name = "tmp_" + std::string(param_file_name); |
| std::string file_name = temp_dir->path().ToString() + tmp_file_name; |
| if (!fexists(file_name)) { |
| std::stringstream ss; |
| ss << "File " << file_name << " is missing from temporary dir."; |
| throw ParquetTestException(ss.str()); |
| } |
| |
| // Iterate over the decryption configurations and use each one to read the encrypted |
| // parquet file. |
| for (unsigned index = 0; index < vector_of_decryption_configurations_.size(); ++index) { |
| unsigned decryption_config_num = index + 1; |
| CheckResults(file_name, decryption_config_num, encryption_config_num); |
| } |
| // Delete temporary test file. |
| ASSERT_EQ(std::remove(file_name.c_str()), 0); |
| |
| // Decrypt parquet file that resides in parquet-testing/data directory. |
| file_name = data_file(param_file_name); |
| |
| if (!fexists(file_name)) { |
| std::stringstream ss; |
| ss << "File " << file_name << " is missing from parquet-testing repo."; |
| throw ParquetTestException(ss.str()); |
| } |
| |
| // Iterate over the decryption configurations and use each one to read the encrypted |
| // parquet file. |
| for (unsigned index = 0; index < vector_of_decryption_configurations_.size(); ++index) { |
| unsigned decryption_config_num = index + 1; |
| CheckResults(file_name, decryption_config_num, encryption_config_num); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| DecryptionTests, TestDecryptionConfiguration, |
| ::testing::Values( |
| std::make_tuple(1, "uniform_encryption.parquet.encrypted"), |
| std::make_tuple(2, "encrypt_columns_and_footer.parquet.encrypted"), |
| std::make_tuple(3, "encrypt_columns_plaintext_footer.parquet.encrypted"), |
| std::make_tuple(4, "encrypt_columns_and_footer_aad.parquet.encrypted"), |
| std::make_tuple( |
| 5, "encrypt_columns_and_footer_disable_aad_storage.parquet.encrypted"), |
| std::make_tuple(6, "encrypt_columns_and_footer_ctr.parquet.encrypted"))); |
| |
| } // namespace parquet::encryption::test |