Skip to content

Commit 721567f

Browse files
committed
GH-48334: Support reading encrypted bloom filters
1 parent aea1ad3 commit 721567f

6 files changed

Lines changed: 420 additions & 43 deletions

File tree

cpp/src/parquet/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ add_parquet_test(arrow-metadata-test SOURCES arrow/arrow_metadata_test.cc
415415
if(PARQUET_REQUIRE_ENCRYPTION)
416416
add_parquet_test(encryption-test
417417
SOURCES
418+
encryption/bloom_filter_encryption_test.cc
418419
encryption/encryption_internal_test.cc
419420
encryption/write_configurations_test.cc
420421
encryption/read_configurations_test.cc

cpp/src/parquet/arrow/fuzz_internal.cc

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,23 @@ Status FuzzReadPageIndex(RowGroupPageIndexReader* reader, const SchemaDescriptor
179179
return st;
180180
}
181181

182+
Status FuzzReadBloomFilter(RowGroupBloomFilterReader* reader, int column,
183+
std::uniform_int_distribution<uint64_t>& hash_dist,
184+
std::default_random_engine& rng) {
185+
Status st;
186+
BEGIN_PARQUET_CATCH_EXCEPTIONS
187+
std::unique_ptr<BloomFilter> bloom;
188+
bloom = reader->GetColumnBloomFilter(column);
189+
// If the column has a bloom filter, find a bunch of random hashes
190+
if (bloom != nullptr) {
191+
for (int k = 0; k < 100; ++k) {
192+
bloom->FindHash(hash_dist(rng));
193+
}
194+
}
195+
END_PARQUET_CATCH_EXCEPTIONS
196+
return st;
197+
}
198+
182199
ReaderProperties MakeFuzzReaderProperties(MemoryPool* pool) {
183200
FileDecryptionProperties::Builder builder;
184201
builder.key_retriever(MakeKeyRetriever());
@@ -231,31 +248,12 @@ Status FuzzReader(const uint8_t* data, int64_t size) {
231248
}
232249
{
233250
// Read and decode bloom filters
234-
try {
235-
auto& bloom_reader = pq_file_reader->GetBloomFilterReader();
236-
std::uniform_int_distribution<uint64_t> hash_dist;
237-
for (int i = 0; i < num_row_groups; ++i) {
238-
auto bloom_rg = bloom_reader.RowGroup(i);
239-
for (int j = 0; j < num_columns; ++j) {
240-
std::unique_ptr<BloomFilter> bloom;
241-
bloom = bloom_rg->GetColumnBloomFilter(j);
242-
// If the column has a bloom filter, find a bunch of random hashes
243-
if (bloom != nullptr) {
244-
for (int k = 0; k < 100; ++k) {
245-
bloom->FindHash(hash_dist(rng));
246-
}
247-
}
248-
}
249-
}
250-
} catch (const ParquetException& exc) {
251-
// XXX we just want to ignore encrypted bloom filters and validate the
252-
// rest of the file; there is no better way of doing this until GH-46597
253-
// is done.
254-
// (also see GH-48334 for reading encrypted bloom filters)
255-
if (std::string_view(exc.what())
256-
.find("BloomFilter decryption is not yet supported") ==
257-
std::string_view::npos) {
258-
throw;
251+
auto& bloom_reader = pq_file_reader->GetBloomFilterReader();
252+
std::uniform_int_distribution<uint64_t> hash_dist;
253+
for (int i = 0; i < num_row_groups; ++i) {
254+
auto bloom_rg = bloom_reader.RowGroup(i);
255+
for (int j = 0; j < num_columns; ++j) {
256+
st &= FuzzReadBloomFilter(bloom_rg.get(), j, hash_dist, rng);
259257
}
260258
}
261259
}

cpp/src/parquet/bloom_filter.cc

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include <cstdint>
1919
#include <cstring>
20+
#include <limits>
2021
#include <memory>
2122

2223
#include "arrow/result.h"
@@ -26,11 +27,28 @@
2627
#include "generated/parquet_types.h"
2728

2829
#include "parquet/bloom_filter.h"
30+
#include "parquet/encryption/internal_file_decryptor.h"
2931
#include "parquet/exception.h"
3032
#include "parquet/thrift_internal.h"
3133
#include "parquet/xxhasher.h"
3234

3335
namespace parquet {
36+
namespace {
37+
38+
constexpr int32_t kCiphertextLengthSize = 4;
39+
40+
int64_t ParseTotalCiphertextSize(const uint8_t* data, int64_t length) {
41+
if (length < kCiphertextLengthSize) {
42+
throw ParquetException("Ciphertext length buffer is too small");
43+
}
44+
uint32_t buffer_size =
45+
(static_cast<uint32_t>(data[3]) << 24) | (static_cast<uint32_t>(data[2]) << 16) |
46+
(static_cast<uint32_t>(data[1]) << 8) | (static_cast<uint32_t>(data[0]));
47+
return static_cast<int64_t>(buffer_size) + kCiphertextLengthSize;
48+
}
49+
50+
} // namespace
51+
3452
constexpr uint32_t BlockSplitBloomFilter::SALT[kBitsSetPerBlock];
3553

3654
BlockSplitBloomFilter::BlockSplitBloomFilter(::arrow::MemoryPool* pool)
@@ -106,7 +124,99 @@ static ::arrow::Status ValidateBloomFilterHeader(
106124

107125
BlockSplitBloomFilter BlockSplitBloomFilter::Deserialize(
108126
const ReaderProperties& properties, ArrowInputStream* input,
109-
std::optional<int64_t> bloom_filter_length) {
127+
std::optional<int64_t> bloom_filter_length, Decryptor* header_decryptor,
128+
Decryptor* bitset_decryptor) {
129+
if (header_decryptor != nullptr || bitset_decryptor != nullptr) {
130+
if (header_decryptor == nullptr || bitset_decryptor == nullptr) {
131+
throw ParquetException("Bloom filter decryptors must be both provided");
132+
}
133+
134+
// Encrypted path: header and bitset are encrypted separately.
135+
ThriftDeserializer deserializer(properties);
136+
format::BloomFilterHeader header;
137+
138+
// Read the length-prefixed ciphertext for the header.
139+
PARQUET_ASSIGN_OR_THROW(auto length_buf, input->Read(kCiphertextLengthSize));
140+
if (ARROW_PREDICT_FALSE(length_buf->size() < kCiphertextLengthSize)) {
141+
throw ParquetException("Bloom filter header read failed: not enough data");
142+
}
143+
144+
const int64_t header_cipher_total_len =
145+
ParseTotalCiphertextSize(length_buf->data(), length_buf->size());
146+
if (ARROW_PREDICT_FALSE(header_cipher_total_len >
147+
std::numeric_limits<int32_t>::max())) {
148+
throw ParquetException("Bloom filter header ciphertext length overflows int32");
149+
}
150+
if (bloom_filter_length && header_cipher_total_len > *bloom_filter_length) {
151+
throw ParquetException(
152+
"Bloom filter length less than encrypted bloom filter header length");
153+
}
154+
// Read the full header ciphertext and decrypt the Thrift header.
155+
auto header_cipher_buf =
156+
AllocateBuffer(properties.memory_pool(), header_cipher_total_len);
157+
std::memcpy(header_cipher_buf->mutable_data(), length_buf->data(),
158+
kCiphertextLengthSize);
159+
const int64_t header_cipher_remaining =
160+
header_cipher_total_len - kCiphertextLengthSize;
161+
PARQUET_ASSIGN_OR_THROW(
162+
auto read_size,
163+
input->Read(header_cipher_remaining,
164+
header_cipher_buf->mutable_data() + kCiphertextLengthSize));
165+
if (ARROW_PREDICT_FALSE(read_size < header_cipher_remaining)) {
166+
throw ParquetException("Bloom filter header read failed: not enough data");
167+
}
168+
169+
uint32_t header_cipher_len = static_cast<uint32_t>(header_cipher_total_len);
170+
try {
171+
deserializer.DeserializeMessage(
172+
reinterpret_cast<const uint8_t*>(header_cipher_buf->data()), &header_cipher_len,
173+
&header, header_decryptor);
174+
DCHECK_EQ(header_cipher_len, header_cipher_total_len);
175+
} catch (std::exception& e) {
176+
std::stringstream ss;
177+
ss << "Deserializing bloom filter header failed.\n" << e.what();
178+
throw ParquetException(ss.str());
179+
}
180+
PARQUET_THROW_NOT_OK(ValidateBloomFilterHeader(header));
181+
182+
const int32_t bloom_filter_size = header.numBytes;
183+
const int32_t bitset_cipher_len =
184+
bitset_decryptor->CiphertextLength(bloom_filter_size);
185+
const int64_t total_cipher_len =
186+
header_cipher_total_len + static_cast<int64_t>(bitset_cipher_len);
187+
if (bloom_filter_length && *bloom_filter_length != total_cipher_len) {
188+
std::stringstream ss;
189+
ss << "Bloom filter length (" << bloom_filter_length.value()
190+
<< ") does not match the actual bloom filter (size: " << total_cipher_len
191+
<< ").";
192+
throw ParquetException(ss.str());
193+
}
194+
195+
// Read and decrypt the bitset bytes.
196+
PARQUET_ASSIGN_OR_THROW(auto bitset_cipher_buf, input->Read(bitset_cipher_len));
197+
if (ARROW_PREDICT_FALSE(bitset_cipher_buf->size() < bitset_cipher_len)) {
198+
throw ParquetException("Bloom Filter read failed: not enough data");
199+
}
200+
201+
const int32_t bitset_plain_len =
202+
bitset_decryptor->PlaintextLength(static_cast<int32_t>(bitset_cipher_len));
203+
if (ARROW_PREDICT_FALSE(bitset_plain_len != bloom_filter_size)) {
204+
throw ParquetException("Bloom filter bitset size does not match header");
205+
}
206+
207+
auto bitset_plain_buf = AllocateBuffer(properties.memory_pool(), bitset_plain_len);
208+
int32_t decrypted_len =
209+
bitset_decryptor->Decrypt(bitset_cipher_buf->span_as<const uint8_t>(),
210+
bitset_plain_buf->mutable_span_as<uint8_t>());
211+
if (ARROW_PREDICT_FALSE(decrypted_len != bitset_plain_len)) {
212+
throw ParquetException("Bloom filter bitset decryption failed");
213+
}
214+
215+
// Initialize the bloom filter from the decrypted bitset.
216+
BlockSplitBloomFilter bloom_filter(properties.memory_pool());
217+
bloom_filter.Init(bitset_plain_buf->data(), bloom_filter_size);
218+
return bloom_filter;
219+
}
110220
ThriftDeserializer deserializer(properties);
111221
format::BloomFilterHeader header;
112222
int64_t bloom_filter_header_read_size = 0;

cpp/src/parquet/bloom_filter.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
namespace parquet {
3131

32+
class Decryptor;
33+
3234
// A Bloom filter is a compact structure to indicate whether an item is not in a set or
3335
// probably in a set. The Bloom filter usually consists of a bit set that represents a
3436
// set of elements, a hash strategy and a Bloom filter algorithm.
@@ -323,10 +325,15 @@ class PARQUET_EXPORT BlockSplitBloomFilter : public BloomFilter {
323325
/// @param input_stream The input stream from which to construct the bloom filter.
324326
/// @param bloom_filter_length The length of the serialized bloom filter including
325327
/// header.
328+
/// @param header_decryptor Optional decryptor for the serialized header.
329+
/// @param bitset_decryptor Optional decryptor for the bitset bytes.
330+
/// Both decryptors must be provided for encrypted bloom filters, or both be null
331+
/// for unencrypted. Providing only one will throw ParquetException.
326332
/// @return The BlockSplitBloomFilter.
327333
static BlockSplitBloomFilter Deserialize(
328334
const ReaderProperties& properties, ArrowInputStream* input_stream,
329-
std::optional<int64_t> bloom_filter_length = std::nullopt);
335+
std::optional<int64_t> bloom_filter_length = std::nullopt,
336+
Decryptor* header_decryptor = NULLPTR, Decryptor* bitset_decryptor = NULLPTR);
330337

331338
private:
332339
inline void InsertHashImpl(uint64_t hash);

cpp/src/parquet/bloom_filter_reader.cc

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
#include "parquet/bloom_filter_reader.h"
1919
#include "parquet/bloom_filter.h"
20+
#include "parquet/encryption/encryption_internal.h"
21+
#include "parquet/encryption/internal_file_decryptor.h"
2022
#include "parquet/exception.h"
2123
#include "parquet/metadata.h"
2224

@@ -26,10 +28,14 @@ class RowGroupBloomFilterReaderImpl final : public RowGroupBloomFilterReader {
2628
public:
2729
RowGroupBloomFilterReaderImpl(std::shared_ptr<::arrow::io::RandomAccessFile> input,
2830
std::shared_ptr<RowGroupMetaData> row_group_metadata,
29-
const ReaderProperties& properties)
31+
const ReaderProperties& properties,
32+
int32_t row_group_ordinal,
33+
std::shared_ptr<InternalFileDecryptor> file_decryptor)
3034
: input_(std::move(input)),
3135
row_group_metadata_(std::move(row_group_metadata)),
32-
properties_(properties) {}
36+
properties_(properties),
37+
row_group_ordinal_(row_group_ordinal),
38+
file_decryptor_(std::move(file_decryptor)) {}
3339

3440
std::unique_ptr<BloomFilter> GetColumnBloomFilter(int i) override;
3541

@@ -42,6 +48,12 @@ class RowGroupBloomFilterReaderImpl final : public RowGroupBloomFilterReader {
4248

4349
/// Reader properties used to deserialize thrift object.
4450
const ReaderProperties& properties_;
51+
52+
/// The ordinal of the row group in the file.
53+
int32_t row_group_ordinal_;
54+
55+
/// File-level decryptor.
56+
std::shared_ptr<InternalFileDecryptor> file_decryptor_;
4557
};
4658

4759
std::unique_ptr<BloomFilter> RowGroupBloomFilterReaderImpl::GetColumnBloomFilter(int i) {
@@ -50,11 +62,6 @@ std::unique_ptr<BloomFilter> RowGroupBloomFilterReaderImpl::GetColumnBloomFilter
5062
}
5163

5264
auto col_chunk = row_group_metadata_->ColumnChunk(i);
53-
std::unique_ptr<ColumnCryptoMetaData> crypto_metadata = col_chunk->crypto_metadata();
54-
if (crypto_metadata != nullptr) {
55-
ParquetException::NYI("BloomFilter decryption is not yet supported");
56-
}
57-
5865
auto bloom_filter_offset = col_chunk->bloom_filter_offset();
5966
if (!bloom_filter_offset.has_value()) {
6067
return nullptr;
@@ -76,10 +83,28 @@ std::unique_ptr<BloomFilter> RowGroupBloomFilterReaderImpl::GetColumnBloomFilter
7683
"bloom filter length + bloom filter offset greater than file size");
7784
}
7885
}
79-
auto stream = ::arrow::io::RandomAccessFile::GetStream(
80-
input_, *bloom_filter_offset, file_size - *bloom_filter_offset);
86+
std::unique_ptr<ColumnCryptoMetaData> crypto_metadata = col_chunk->crypto_metadata();
87+
std::unique_ptr<Decryptor> header_decryptor =
88+
InternalFileDecryptor::GetColumnMetaDecryptorFactory(file_decryptor_.get(),
89+
crypto_metadata.get())();
90+
std::unique_ptr<Decryptor> bitset_decryptor =
91+
InternalFileDecryptor::GetColumnDataDecryptorFactory(file_decryptor_.get(),
92+
crypto_metadata.get())();
93+
if (header_decryptor != nullptr) {
94+
UpdateDecryptor(header_decryptor.get(), row_group_ordinal_, static_cast<int16_t>(i),
95+
encryption::kBloomFilterHeader);
96+
}
97+
if (bitset_decryptor != nullptr) {
98+
UpdateDecryptor(bitset_decryptor.get(), row_group_ordinal_, static_cast<int16_t>(i),
99+
encryption::kBloomFilterBitset);
100+
}
101+
const int64_t stream_length =
102+
bloom_filter_length ? *bloom_filter_length : file_size - *bloom_filter_offset;
103+
auto stream = ::arrow::io::RandomAccessFile::GetStream(input_, *bloom_filter_offset,
104+
stream_length);
81105
auto bloom_filter =
82-
BlockSplitBloomFilter::Deserialize(properties_, stream->get(), bloom_filter_length);
106+
BlockSplitBloomFilter::Deserialize(properties_, stream->get(), bloom_filter_length,
107+
header_decryptor.get(), bitset_decryptor.get());
83108
return std::make_unique<BlockSplitBloomFilter>(std::move(bloom_filter));
84109
}
85110

@@ -91,11 +116,8 @@ class BloomFilterReaderImpl final : public BloomFilterReader {
91116
std::shared_ptr<InternalFileDecryptor> file_decryptor)
92117
: input_(std::move(input)),
93118
file_metadata_(std::move(file_metadata)),
94-
properties_(properties) {
95-
if (file_decryptor != nullptr) {
96-
ParquetException::NYI("BloomFilter decryption is not yet supported");
97-
}
98-
}
119+
properties_(properties),
120+
file_decryptor_(std::move(file_decryptor)) {}
99121

100122
std::shared_ptr<RowGroupBloomFilterReader> RowGroup(int i) {
101123
if (i < 0 || i >= file_metadata_->num_row_groups()) {
@@ -104,7 +126,7 @@ class BloomFilterReaderImpl final : public BloomFilterReader {
104126

105127
auto row_group_metadata = file_metadata_->RowGroup(i);
106128
return std::make_shared<RowGroupBloomFilterReaderImpl>(
107-
input_, std::move(row_group_metadata), properties_);
129+
input_, std::move(row_group_metadata), properties_, i, file_decryptor_);
108130
}
109131

110132
private:
@@ -116,6 +138,9 @@ class BloomFilterReaderImpl final : public BloomFilterReader {
116138

117139
/// Reader properties used to deserialize thrift object.
118140
const ReaderProperties& properties_;
141+
142+
/// File-level decryptor, if any.
143+
std::shared_ptr<InternalFileDecryptor> file_decryptor_;
119144
};
120145

121146
std::unique_ptr<BloomFilterReader> BloomFilterReader::Make(

0 commit comments

Comments
 (0)