This is an automated email from the ASF dual-hosted git repository.

adamreeve pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 16ebf1a40e GH-51669: [C++][Parquet] Preserve cipher in plaintext 
footer metadata (#54884)
16ebf1a40e is described below

commit 16ebf1a40ecd9739988275049e126ab4d1f5f028
Author: autoCAT <[email protected]>
AuthorDate: Wed Oct 28 13:44:20 2026 -0700

    GH-51669: [C++][Parquet] Preserve cipher in plaintext footer metadata 
(#54884)
    
    ### Rationale for this change
    
    Closes #51669. CTR files with plaintext footers incorrectly declare GCM, 
causing encrypted-column reads to fail.
    
    ### What changes are included in this PR?
    
    * Serialize the configured file encryption algorithm directly.
    * Select AES-GCM explicitly for plaintext footer signing and verification.
    * Add generated GCM/CTR, footer and AAD cases, including an on-disk GCM tag 
check independent of reader selection.
    
    Original report by [dotnwat](https://github.com/dotnwat) in #51669. The 
follow-up incorporates [pitrou's 
review](https://github.com/apache/arrow/pull/54884#issuecomment-6342493385) and 
[adamreeve's 
clarification](https://github.com/apache/arrow/pull/54884#issuecomment-6352237621).
    
    ### Are these changes tested?
    
    * Encryption: 45 passed; metadata: 50 passed; writer: 296 passed, with 2 
existing disabled tests.
    * Released parquet-java 1.18.1 interoperability: 64 read outcomes and 8 raw 
metadata inspections matched their controls.
    * clang-format 18 and `git diff --check` pass.
    
    ### Are there any user-facing changes?
    
    New CTR/plaintext-footer files declare the correct algorithm. No public API 
changes.
    
    **This PR contains a "Critical Fix"**: it corrects invalid encryption 
algorithm metadata emitted by the writer.
    
    ### Was AI used for this PR?
    
    AI was used to generate the implementation, regression tests, and PR 
description.
    
    **PR code and description written by:**
    
    * [ ] Human
    * [x] AI
    
    **Reviewed before submission by:**
    
    * [x] Human
    * [x] AI
    * [ ] Not reviewed
    
    * GitHub Issue: #51669
    
    Authored-by: Yanzhi Xin <[email protected]>
    Signed-off-by: Adam Reeve <[email protected]>
---
 .../parquet/encryption/internal_file_encryptor.cc  |   4 +-
 .../encryption/write_configurations_test.cc        | 172 +++++++++++++++++++++
 cpp/src/parquet/metadata.cc                        |  26 ++--
 3 files changed, 186 insertions(+), 16 deletions(-)

diff --git a/cpp/src/parquet/encryption/internal_file_encryptor.cc 
b/cpp/src/parquet/encryption/internal_file_encryptor.cc
index 23dbfe34d3..4ab221fd55 100644
--- a/cpp/src/parquet/encryption/internal_file_encryptor.cc
+++ b/cpp/src/parquet/encryption/internal_file_encryptor.cc
@@ -68,7 +68,9 @@ std::shared_ptr<Encryptor> 
InternalFileEncryptor::GetFooterSigningEncryptor() {
     return footer_signing_encryptor_;
   }
 
-  ParquetCipher::type algorithm = properties_->algorithm().algorithm;
+  // A plaintext footer is always signed with AES-GCM, regardless of the 
algorithm
+  // configured for encryption.
+  const auto algorithm = ParquetCipher::AES_GCM_V1;
   std::string footer_aad = 
encryption::CreateFooterAad(properties_->file_aad());
   const SecureString& footer_signing_key = properties_->footer_key();
   auto aes_encryptor = GetMetaAesEncryptor(algorithm, 
footer_signing_key.size());
diff --git a/cpp/src/parquet/encryption/write_configurations_test.cc 
b/cpp/src/parquet/encryption/write_configurations_test.cc
index 0af0278b26..821130fa6c 100644
--- a/cpp/src/parquet/encryption/write_configurations_test.cc
+++ b/cpp/src/parquet/encryption/write_configurations_test.cc
@@ -19,10 +19,18 @@
 
 #include <stdio.h>
 
+#include <stdexcept>
+#include <tuple>
+#include <vector>
+
 #include <arrow/io/file.h>
 
+#include "arrow/util/endian.h"
+#include "arrow/util/ubsan.h"
+
 #include "parquet/column_reader.h"
 #include "parquet/column_writer.h"
+#include "parquet/encryption/encryption_internal.h"
 #include "parquet/encryption/test_encryption_util.h"
 #include "parquet/file_reader.h"
 #include "parquet/file_writer.h"
@@ -195,6 +203,170 @@ TEST_F(TestEncryptionConfiguration, 
EncryptTwoColumnsAndFooterUseAES_GCM_CTR) {
                         
"tmp_encrypt_columns_and_footer_ctr.parquet.encrypted"));
 }
 
+enum class AadPrefixMode { None, Stored, External };
+
+class TestGeneratedEncryptionConfiguration
+    : public ::testing::TestWithParam<
+          std::tuple<ParquetCipher::type, bool, AadPrefixMode>> {
+ public:
+  void SetUp() override {
+    PARQUET_ASSIGN_OR_THROW(temp_dir_, temp_data_dir());
+    file_path_ = temp_dir_->path().ToString() + 
"algorithm_metadata.parquet.encrypted";
+
+    std::map<std::string, std::shared_ptr<ColumnEncryptionProperties>> columns;
+    columns[kDoubleFieldName] =
+        ColumnEncryptionProperties::WithColumnKey(kColumnEncryptionKey1, 
"kc1");
+    columns[kFloatFieldName] =
+        ColumnEncryptionProperties::WithColumnKey(kColumnEncryptionKey2, 
"kc2");
+    columns[kBooleanFieldName] = ColumnEncryptionProperties::Unencrypted();
+
+    FileEncryptionProperties::Builder builder(kFooterEncryptionKey);
+    builder.footer_key_metadata("kf")
+        ->encrypted_columns(std::move(columns))
+        ->algorithm(std::get<0>(GetParam()));
+    if (std::get<1>(GetParam())) {
+      builder.set_plaintext_footer();
+    }
+    if (std::get<2>(GetParam()) != AadPrefixMode::None) {
+      builder.aad_prefix(kFileName);
+    }
+    if (std::get<2>(GetParam()) == AadPrefixMode::External) {
+      builder.disable_aad_prefix_storage();
+    }
+    auto properties = builder.build();
+    file_aad_ = properties->file_aad();
+    FileEncryptor().EncryptFile(file_path_, std::move(properties));
+  }
+
+ protected:
+  enum class Keys { All, MissingColumn, WrongFooter };
+  enum class AadPrefix { Match, Missing, Wrong };
+
+  std::shared_ptr<FileDecryptionProperties> MakeDecryptionProperties(
+      Keys keys = Keys::All, AadPrefix prefix = AadPrefix::Match) {
+    auto retriever = std::make_shared<StringKeyIdRetriever>();
+    retriever->PutKey("kf", keys == Keys::WrongFooter ? 
SecureString(kNewFooterMasterKey)
+                                                      : kFooterEncryptionKey);
+    if (keys != Keys::MissingColumn) {
+      retriever->PutKey("kc1", kColumnEncryptionKey1);
+    }
+    retriever->PutKey("kc2", kColumnEncryptionKey2);
+
+    FileDecryptionProperties::Builder builder;
+    builder.key_retriever(std::move(retriever));
+    if (std::get<2>(GetParam()) != AadPrefixMode::None && prefix != 
AadPrefix::Missing) {
+      builder.aad_prefix(prefix == AadPrefix::Wrong ? "incorrect-prefix" : 
kFileName);
+    }
+    return builder.build();
+  }
+
+  std::unique_ptr<ParquetFileReader> OpenReader(
+      std::shared_ptr<FileDecryptionProperties> decryption_properties) {
+    auto reader_properties = default_reader_properties();
+    
reader_properties.file_decryption_properties(std::move(decryption_properties));
+    return ParquetFileReader::OpenFile(file_path_, false, reader_properties);
+  }
+
+  void CheckPlaintextFooterSignature() {
+    PARQUET_ASSIGN_OR_THROW(auto file, 
::arrow::io::ReadableFile::Open(file_path_));
+    PARQUET_ASSIGN_OR_THROW(auto file_size, file->GetSize());
+    ASSERT_GT(file_size, 8);
+    PARQUET_ASSIGN_OR_THROW(auto trailer, file->ReadAt(file_size - 8, 8));
+    ASSERT_EQ(trailer->size(), 8);
+    const auto footer_size = ::arrow::bit_util::FromLittleEndian(
+        ::arrow::util::SafeLoadAs<uint32_t>(trailer->data()));
+    constexpr auto signature_size = kNonceLength + kGcmTagLength;
+    ASSERT_GT(footer_size, signature_size);
+    ASSERT_LE(footer_size, file_size - 8);
+    PARQUET_ASSIGN_OR_THROW(auto footer,
+                            file->ReadAt(file_size - 8 - footer_size, 
footer_size));
+    ASSERT_EQ(footer->size(), footer_size);
+    const auto footer_span = footer->span_as<uint8_t>();
+    const auto metadata = footer_span.first(footer_size - signature_size);
+    const auto nonce = footer_span.subspan(metadata.size(), kNonceLength);
+    const auto tag = footer_span.last(kGcmTagLength);
+    // Footer AAD is the file AAD followed by the footer module type byte.
+    const auto aad = file_aad_ + static_cast<char>(kFooter);
+
+    // Verify the on-disk tag with GCM explicitly, independently of reader 
selection.
+    auto encryptor = AesEncryptor::Make(ParquetCipher::AES_GCM_V1,
+                                        
static_cast<int32_t>(kFooterEncryptionKey.size()),
+                                        /*metadata=*/true,
+                                        /*write_length=*/false);
+    std::vector<uint8_t> 
encrypted(encryptor->CiphertextLength(metadata.size()));
+    encryptor->SignedFooterEncrypt(metadata, kFooterEncryptionKey.as_span(),
+                                   str2span(aad), nonce, encrypted);
+    EXPECT_EQ(std::vector<uint8_t>(tag.begin(), tag.end()),
+              std::vector<uint8_t>(encrypted.end() - kGcmTagLength, 
encrypted.end()));
+  }
+
+  void ReadFirstSecretValue(std::shared_ptr<FileDecryptionProperties> 
properties) {
+    auto reader = OpenReader(std::move(properties));
+    auto column = reader->RowGroup(0)->Column(5);
+    auto* double_reader = static_cast<DoubleReader*>(column.get());
+    double value;
+    int64_t values_read;
+    double_reader->ReadBatch(1, nullptr, nullptr, &value, &values_read);
+  }
+
+  std::unique_ptr<TemporaryDir> temp_dir_;
+  std::string file_path_;
+  std::string file_aad_;
+};
+
+TEST_P(TestGeneratedEncryptionConfiguration, AlgorithmMetadataAndRoundTrip) {
+  const auto cipher = std::get<0>(GetParam());
+  const bool plaintext_footer = std::get<1>(GetParam());
+  const auto prefix_mode = std::get<2>(GetParam());
+  if (plaintext_footer) {
+    CheckPlaintextFooterSignature();
+  }
+  auto reader = OpenReader(MakeDecryptionProperties());
+  const auto metadata = reader->metadata();
+  if (plaintext_footer) {
+    ASSERT_TRUE(metadata->is_encryption_algorithm_set());
+    const auto algorithm = metadata->encryption_algorithm();
+    ASSERT_EQ(cipher, algorithm.algorithm);
+    EXPECT_FALSE(algorithm.aad.aad_file_unique.empty());
+    EXPECT_EQ(prefix_mode == AadPrefixMode::External, 
algorithm.aad.supply_aad_prefix);
+    EXPECT_EQ(prefix_mode == AadPrefixMode::Stored ? kFileName : "",
+              algorithm.aad.aad_prefix);
+  } else {
+    // An encrypted footer declares the algorithm in FileCryptoMetaData 
instead.
+    EXPECT_FALSE(metadata->is_encryption_algorithm_set());
+  }
+  reader->Close();
+
+  FileDecryptor decryptor;
+  // Verify every physical column and all row groups through sync and async 
readers.
+  ASSERT_NO_THROW(decryptor.DecryptFile(file_path_, 
MakeDecryptionProperties()));
+  if (prefix_mode == AadPrefixMode::Stored) {
+    ASSERT_NO_THROW(decryptor.DecryptFile(
+        file_path_, MakeDecryptionProperties(Keys::All, AadPrefix::Missing)));
+  }
+  if (plaintext_footer) {
+    // This reads only public columns and does not authenticate the keyless 
footer.
+    ASSERT_NO_THROW(decryptor.DecryptFile(file_path_, nullptr));
+  }
+  
EXPECT_THROW(ReadFirstSecretValue(MakeDecryptionProperties(Keys::MissingColumn)),
+               std::out_of_range);
+  EXPECT_THROW(OpenReader(MakeDecryptionProperties(Keys::WrongFooter)), 
ParquetException);
+  if (prefix_mode == AadPrefixMode::External) {
+    EXPECT_THROW(OpenReader(MakeDecryptionProperties(Keys::All, 
AadPrefix::Missing)),
+                 ParquetException);
+    EXPECT_THROW(OpenReader(MakeDecryptionProperties(Keys::All, 
AadPrefix::Wrong)),
+                 ParquetException);
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    GeneratedFiles, TestGeneratedEncryptionConfiguration,
+    ::testing::Combine(::testing::Values(ParquetCipher::AES_GCM_V1,
+                                         ParquetCipher::AES_GCM_CTR_V1),
+                       ::testing::Bool(),
+                       ::testing::Values(AadPrefixMode::None, 
AadPrefixMode::Stored,
+                                         AadPrefixMode::External)));
+
 // 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 61a111fc0c..2bc871a8c8 100644
--- a/cpp/src/parquet/metadata.cc
+++ b/cpp/src/parquet/metadata.cc
@@ -823,9 +823,11 @@ class FileMetaData::FileMetaDataImpl {
     const SecureString& key = file_decryptor_->GetFooterKey();
     const std::string& aad = 
encryption::CreateFooterAad(file_decryptor_->file_aad());
 
-    auto aes_encryptor = 
encryption::AesEncryptor::Make(file_decryptor_->algorithm(),
-                                                        
static_cast<int>(key.size()),
-                                                        true, false 
/*write_length*/);
+    // A plaintext footer is always signed with AES-GCM, regardless of the 
algorithm
+    // configured for encryption.
+    auto aes_encryptor = encryption::AesEncryptor::Make(
+        ParquetCipher::AES_GCM_V1, static_cast<int>(key.size()), 
/*metadata=*/true,
+        /*write_length=*/false);
 
     std::shared_ptr<Buffer> encrypted_buffer = AllocateBuffer(
         file_decryptor_->pool(), aes_encryptor->CiphertextLength(
@@ -1202,8 +1204,10 @@ bool FileMetaData::VerifySignature(std::span<const 
uint8_t> serialized_metadata,
   const SecureString& key = file_decryptor->GetFooterKey();
   const std::string& aad = 
encryption::CreateFooterAad(file_decryptor->file_aad());
 
+  // A plaintext footer is always signed with AES-GCM, regardless of the 
algorithm
+  // configured for encryption.
   auto aes_encryptor = encryption::AesEncryptor::Make(
-      file_decryptor->algorithm(), static_cast<int>(key.size()), 
/*metadata=*/true,
+      ParquetCipher::AES_GCM_V1, static_cast<int>(key.size()), 
/*metadata=*/true,
       /*write_length=*/false);
 
   std::shared_ptr<Buffer> encrypted_buffer =
@@ -2133,19 +2137,11 @@ class FileMetaDataBuilder::FileMetaDataBuilderImpl {
     metadata_->column_orders.resize(schema_->num_columns(), column_order);
     metadata_->__isset.column_orders = true;
 
-    // if plaintext footer, set footer signing algorithm
+    // Plaintext footers declare the file encryption algorithm and signing key 
metadata.
     auto file_encryption_properties = 
properties_->file_encryption_properties();
     if (file_encryption_properties && 
!file_encryption_properties->encrypted_footer()) {
-      EncryptionAlgorithm signing_algorithm;
-      EncryptionAlgorithm algo = file_encryption_properties->algorithm();
-      signing_algorithm.aad.aad_file_unique = algo.aad.aad_file_unique;
-      signing_algorithm.aad.supply_aad_prefix = algo.aad.supply_aad_prefix;
-      if (!algo.aad.supply_aad_prefix) {
-        signing_algorithm.aad.aad_prefix = algo.aad.aad_prefix;
-      }
-      signing_algorithm.algorithm = ParquetCipher::AES_GCM_V1;
-
-      metadata_->__set_encryption_algorithm(ToThrift(signing_algorithm));
+      metadata_->__set_encryption_algorithm(
+          ToThrift(file_encryption_properties->algorithm()));
       const std::string& footer_signing_key_metadata =
           file_encryption_properties->footer_key_metadata();
       if (footer_signing_key_metadata.size() > 0) {

Reply via email to