From 84f7d3fecbcc0a63d8eddb5a41e693586c28f67f Mon Sep 17 00:00:00 2001 From: YusefSyed <211442445+YusefSyed@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:45:03 -0400 Subject: [PATCH] GH-51669: [C++][Parquet] Preserve file algorithm in plaintext footers --- .../encryption/write_configurations_test.cc | 46 +++++++++++++++++++ cpp/src/parquet/metadata.cc | 4 +- .../pyarrow/tests/parquet/test_encryption.py | 10 ++-- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/cpp/src/parquet/encryption/write_configurations_test.cc b/cpp/src/parquet/encryption/write_configurations_test.cc index 0af0278b2604..0624b9996172 100644 --- a/cpp/src/parquet/encryption/write_configurations_test.cc +++ b/cpp/src/parquet/encryption/write_configurations_test.cc @@ -19,6 +19,8 @@ #include +#include + #include #include "parquet/column_reader.h" @@ -195,6 +197,50 @@ TEST_F(TestEncryptionConfiguration, EncryptTwoColumnsAndFooterUseAES_GCM_CTR) { "tmp_encrypt_columns_and_footer_ctr.parquet.encrypted")); } +// Cover the file algorithm independently of the footer signing cipher. +TEST_F(TestEncryptionConfiguration, PlaintextFooterAlgorithms) { + for (auto algorithm : {ParquetCipher::AES_GCM_V1, ParquetCipher::AES_GCM_CTR_V1}) { + SCOPED_TRACE(algorithm); + FileEncryptionProperties::Builder encryption_builder(kFooterEncryptionKey_); + auto encryption_properties = + encryption_builder.algorithm(algorithm)->set_plaintext_footer()->build(); + const std::string file = temp_dir->path().ToString() + "plaintext_footer.parquet"; + encryptor_.EncryptFile(file, encryption_properties); + + FileDecryptionProperties::Builder decryption_builder; + auto decryption_properties = + decryption_builder.footer_key(kFooterEncryptionKey_)->build(); + ReaderProperties reader_properties; + reader_properties.file_decryption_properties(decryption_properties); + auto reader = ParquetFileReader::OpenFile(file, false, reader_properties); + EXPECT_EQ(reader->metadata()->encryption_algorithm().algorithm, algorithm); + reader->Close(); + + FileDecryptor decryptor; + EXPECT_NO_THROW(decryptor.DecryptFile(file, decryption_properties)); + + FileDecryptionProperties::Builder wrong_key_builder; + ReaderProperties wrong_key_properties; + wrong_key_properties.file_decryption_properties( + wrong_key_builder.footer_key(kColumnEncryptionKey1_)->build()); + EXPECT_THROW(ParquetFileReader::OpenFile(file, false, wrong_key_properties), + ParquetException); + + // The signature precedes the footer length and trailing magic (eight bytes). + std::fstream stream(file, std::ios::in | std::ios::out | std::ios::binary); + stream.seekg(-9, std::ios::end); + char tag_byte; + stream.read(&tag_byte, 1); + ASSERT_TRUE(stream.good()); + tag_byte ^= 1; + stream.seekp(-9, std::ios::end); + stream.write(&tag_byte, 1); + stream.close(); + EXPECT_THROW(ParquetFileReader::OpenFile(file, false, reader_properties), + ParquetException); + } +} + // Set temp_dir before running the write/read tests. The encrypted files will // be written/read from this directory. void TestEncryptionConfiguration::SetUpTestCase() { diff --git a/cpp/src/parquet/metadata.cc b/cpp/src/parquet/metadata.cc index 61a111fc0c34..df0b093bf509 100644 --- a/cpp/src/parquet/metadata.cc +++ b/cpp/src/parquet/metadata.cc @@ -2133,7 +2133,7 @@ class FileMetaDataBuilder::FileMetaDataBuilderImpl { metadata_->column_orders.resize(schema_->num_columns(), column_order); metadata_->__isset.column_orders = true; - // if plaintext footer, set footer signing algorithm + // For a plaintext footer, record the file encryption algorithm. auto file_encryption_properties = properties_->file_encryption_properties(); if (file_encryption_properties && !file_encryption_properties->encrypted_footer()) { EncryptionAlgorithm signing_algorithm; @@ -2143,7 +2143,7 @@ class FileMetaDataBuilder::FileMetaDataBuilderImpl { if (!algo.aad.supply_aad_prefix) { signing_algorithm.aad.aad_prefix = algo.aad.aad_prefix; } - signing_algorithm.algorithm = ParquetCipher::AES_GCM_V1; + signing_algorithm.algorithm = algo.algorithm; metadata_->__set_encryption_algorithm(ToThrift(signing_algorithm)); const std::string& footer_signing_key_metadata = diff --git a/python/pyarrow/tests/parquet/test_encryption.py b/python/pyarrow/tests/parquet/test_encryption.py index 5185bf6a3013..fcb31718acc6 100644 --- a/python/pyarrow/tests/parquet/test_encryption.py +++ b/python/pyarrow/tests/parquet/test_encryption.py @@ -949,12 +949,16 @@ def test_encrypted_file_has_pare_magic(self, tempdir, data_table): magic = f.read(4) assert magic == b"PARE" - def test_plaintext_footer(self, tempdir, data_table): - path = tempdir / "direct_plaintext_footer.parquet" + @pytest.mark.parametrize("algorithm", ["AES_GCM_V1", "AES_GCM_CTR_V1"]) + @pytest.mark.parametrize("plaintext_footer", [False, True]) + def test_footer_algorithms(self, tempdir, data_table, algorithm, + plaintext_footer): + path = tempdir / "direct_footer_algorithms.parquet" enc_props = pe.create_encryption_properties( footer_key=DIRECT_KEY_128, - plaintext_footer=True, + plaintext_footer=plaintext_footer, + encryption_algorithm=algorithm, ) pq.write_table(data_table, path, encryption_properties=enc_props)