Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
db8c46c
GH-3934: Encrypt DataPageV2 repetition and definition levels together…
RussellSpitzer Nov 5, 2026
425429f
review: address round 1 and 2 feedback on DataPageV2 encryption
RussellSpitzer Nov 5, 2026
5d8c446
Split DataPageV2 reading into plaintext-levels and encrypted-levels p…
RussellSpitzer Nov 5, 2026
e12a5cb
Remove the ParquetFileWriter overloads that wrote encrypted DataPageV…
RussellSpitzer Nov 5, 2026
8c94e13
GH-3934: Remove the Arrow fixture generator script
RussellSpitzer Nov 6, 2026
1df11ea
GH-3934: Deprecate the plaintext-levels writeDataPageV2 overloads ins…
RussellSpitzer Nov 6, 2026
04e54d3
GH-3934: Shorten the README note on DataPageV2 encryption
RussellSpitzer Nov 6, 2026
f1a75bc
GH-3934: Wrap the @deprecated links of the writeDataPageV2 overloads
RussellSpitzer Nov 6, 2026
335653f
GH-3934: Resolve ambiguous DataPageV2 layouts from created_by
RussellSpitzer Nov 6, 2026
fd6e421
GH-3934: Resolve every ambiguous DataPageV2 layout from created_by
RussellSpitzer Nov 12, 2026
1bb3fe5
GH-3934: Check the decrypted size of uncompressed DataPageV2 pages
RussellSpitzer Nov 12, 2026
4da0286
GH-3934: Rename FullBodyEncryptedDataPageV2 to SingleModuleDataPageV2
RussellSpitzer Nov 12, 2026
0ff47cf
GH-3934: Write the encrypted DataPageV2 module in place of the levels…
RussellSpitzer Nov 12, 2026
bb67788
GH-3934: Move the DataPageV2 encryption test data into its only test
RussellSpitzer Nov 12, 2026
8e714b2
GH-3934: Remove redundant DataPageV2 encryption tests
RussellSpitzer Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions parquet-hadoop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ Note: nested column names must be specified as full dot-separated paths for each

**Property:** `parquet.encryption.algorithm`
**Description:** Parquet encryption algorithm. Can be `AES_GCM_V1` or `AES_GCM_CTR_V1`.
Since 1.19.0, DataPageV2 pages of encrypted columns encrypt the repetition and definition levels with the values,
as the specification requires. Earlier versions wrote the levels in plaintext. Version 1.19.0 reads both layouts.
Versions 1.18 and earlier cannot read the new pages. With `AES_GCM_V1`, decryption fails. With `AES_GCM_CTR_V1`,
the levels are incorrect.
**Default value:** `AES_GCM_V1`

---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,85 +218,163 @@ public DataPage visit(DataPageV1 dataPageV1) {

@Override
public DataPage visit(DataPageV2 dataPageV2) {
if (!dataPageV2.isCompressed() && offsetIndex == null && null == blockDecryptor) {
return dataPageV2;
if (dataPageV2 instanceof SingleModuleDataPageV2) {
return readPageV2WithEncryptedLevels((SingleModuleDataPageV2) dataPageV2, currentPageIndex);
}
BytesInput pageBytes = dataPageV2.getData();
try {
if (options.getAllocator().isDirect() && options.useOffHeapDecryptBuffer()) {
ByteBuffer byteBuffer = pageBytes.toByteBuffer(releaser);
if (!byteBuffer.isDirect()) {
throw new ParquetDecodingException("Expected a direct buffer");
}
if (blockDecryptor != null) {
byteBuffer = blockDecryptor.decrypt(byteBuffer, dataPageAAD);
}
long compressedSize = byteBuffer.limit();
if (dataPageV2.isCompressed()) {
int uncompressedSize = Math.toIntExact(dataPageV2.getUncompressedSize()
- dataPageV2.getDefinitionLevels().size()
- dataPageV2.getRepetitionLevels().size());
ByteBuffer decompressedBuffer =
options.getAllocator().allocate(uncompressedSize);
releaser.releaseLater(decompressedBuffer);
long start = System.nanoTime();
decompressor.decompress(
byteBuffer, (int) compressedSize, decompressedBuffer, uncompressedSize);
setDecompressMetrics(pageBytes, start);
decompressedBuffer.flip();
pageBytes = BytesInput.from(decompressedBuffer);
} else {
pageBytes = BytesInput.from(byteBuffer);
}
} else {
if (null != blockDecryptor) {
pageBytes =
BytesInput.from(blockDecryptor.decrypt(pageBytes.toByteArray(), dataPageAAD));
}
if (dataPageV2.isCompressed()) {
int uncompressedSize = Math.toIntExact(dataPageV2.getUncompressedSize()
- dataPageV2.getDefinitionLevels().size()
- dataPageV2.getRepetitionLevels().size());
long start = System.nanoTime();
pageBytes = BytesInput.copy(decompressor.decompress(pageBytes, uncompressedSize));
setDecompressMetrics(pageBytes, start);
}
}
} catch (IOException e) {
throw new ParquetDecodingException("could not decompress page", e);
}

final DataPageV2 decompressedPage;
if (offsetIndex == null) {
decompressedPage = DataPageV2.uncompressed(
dataPageV2.getRowCount(),
dataPageV2.getNullCount(),
dataPageV2.getValueCount(),
dataPageV2.getRepetitionLevels(),
dataPageV2.getDefinitionLevels(),
dataPageV2.getDataEncoding(),
pageBytes,
dataPageV2.getStatistics());
} else {
decompressedPage = DataPageV2.uncompressed(
dataPageV2.getRowCount(),
dataPageV2.getNullCount(),
dataPageV2.getValueCount(),
offsetIndex.getFirstRowIndex(currentPageIndex),
dataPageV2.getRepetitionLevels(),
dataPageV2.getDefinitionLevels(),
dataPageV2.getDataEncoding(),
pageBytes,
dataPageV2.getStatistics());
}
if (dataPageV2.getCrc().isPresent()) {
decompressedPage.setCrc(dataPageV2.getCrc().getAsInt());
}
return decompressedPage;
return readPageV2WithPlaintextLevels(dataPageV2, currentPageIndex);
}
});
}

/**
* Reads a page whose levels are not encrypted. If the column is encrypted, only the values are an
* encrypted module.
Comment thread
RussellSpitzer marked this conversation as resolved.
Resolved
*
* <p>An encrypted page with plaintext levels does not follow the Parquet encryption spec, which
* encrypts the levels and the values as one module. Such pages are written only by parquet-java
* 1.18.0 and earlier, and are read here for backward compatibility only.
*/
private DataPageV2 readPageV2WithPlaintextLevels(DataPageV2 page, int currentPageIndex) {
if (!page.isCompressed() && offsetIndex == null && null == blockDecryptor) {
return page;
}
boolean offHeap = decryptsOffHeap();
BytesInput values = page.getData();
try {
if (null != blockDecryptor) {
values = BytesInput.from(decryptDataPageModule(values, offHeap));
} else if (offHeap) {
values = BytesInput.from(toDirectBuffer(values));
}
if (page.isCompressed()) {
int levelsByteLength =
Math.toIntExact(page.getRepetitionLevels().size()
+ page.getDefinitionLevels().size());
values = decompressValues(values, valuesUncompressedSize(page, levelsByteLength), offHeap);
}
} catch (IOException e) {
throw new ParquetDecodingException("could not decompress page", e);
}
return uncompressedPageV2(
page, page.getRepetitionLevels(), page.getDefinitionLevels(), values, currentPageIndex);
}

/**
* Reads a page whose levels and values are one encrypted module: decrypts the module, splits it
* into levels and values, and decompresses the values.
*/
private DataPageV2 readPageV2WithEncryptedLevels(SingleModuleDataPageV2 page, int currentPageIndex) {
boolean offHeap = decryptsOffHeap();
int repetitionLevelsByteLength = page.getRepetitionLevelsByteLength();
int definitionLevelsByteLength = page.getDefinitionLevelsByteLength();
int levelsByteLength = repetitionLevelsByteLength + definitionLevelsByteLength;
try {
ByteBuffer decryptedPage = decryptDataPageModule(page.getEncryptedModule(), offHeap);
checkDecryptedPageSize(page, decryptedPage.limit(), levelsByteLength);
BytesInput repetitionLevels = BytesInput.from(decryptedPage.slice(0, repetitionLevelsByteLength));
BytesInput definitionLevels =
BytesInput.from(decryptedPage.slice(repetitionLevelsByteLength, definitionLevelsByteLength));
BytesInput values = BytesInput.from(
decryptedPage.slice(levelsByteLength, decryptedPage.limit() - levelsByteLength));
if (page.isCompressed()) {
values = decompressValues(values, valuesUncompressedSize(page, levelsByteLength), offHeap);
}
return uncompressedPageV2(page, repetitionLevels, definitionLevels, values, currentPageIndex);
} catch (IOException e) {
throw new ParquetDecodingException("could not decompress page", e);
}
}

private void checkDecryptedPageSize(DataPageV2 page, int decryptedSize, int levelsByteLength) {
if (decryptedSize < levelsByteLength) {
throw new ParquetDecodingException("Decrypted DataPageV2 page has " + decryptedSize
+ " bytes, which is less than the " + levelsByteLength
+ " bytes of repetition and definition levels in the page header");
}
if (!page.isCompressed() && decryptedSize != page.getUncompressedSize()) {
throw new ParquetDecodingException("Decrypted DataPageV2 page has " + decryptedSize
+ " bytes, but the page header gives an uncompressed page size of "
+ page.getUncompressedSize() + " bytes");
}
}

private static int valuesUncompressedSize(DataPageV2 page, int levelsByteLength) {
return Math.toIntExact(page.getUncompressedSize() - levelsByteLength);
}

private boolean decryptsOffHeap() {
return options.getAllocator().isDirect() && options.useOffHeapDecryptBuffer();
}

/** Decrypts in place if off heap, otherwise into a new heap buffer. */
private ByteBuffer decryptDataPageModule(BytesInput module, boolean offHeap) throws IOException {
if (offHeap) {
return blockDecryptor.decrypt(toDirectBuffer(module), dataPageAAD);
}
return ByteBuffer.wrap(blockDecryptor.decrypt(module.toByteArray(), dataPageAAD));
}

private ByteBuffer toDirectBuffer(BytesInput bytes) {
ByteBuffer buffer = bytes.toByteBuffer(releaser);
if (!buffer.isDirect()) {
throw new ParquetDecodingException("Expected a direct buffer");
}
return buffer;
}

private BytesInput decompressValues(BytesInput compressed, int uncompressedSize, boolean offHeap)
throws IOException {
long start = System.nanoTime();
BytesInput decompressed;
if (offHeap) {
ByteBuffer input = toDirectBuffer(compressed);
ByteBuffer output = options.getAllocator().allocate(uncompressedSize);
releaser.releaseLater(output);
decompressor.decompress(input, input.limit(), output, uncompressedSize);
output.flip();
decompressed = BytesInput.from(output);
} else {
decompressed = BytesInput.copy(decompressor.decompress(compressed, uncompressedSize));
}
setDecompressMetrics(compressed, start);
return decompressed;
}

private DataPageV2 uncompressedPageV2(
DataPageV2 source,
BytesInput repetitionLevels,
BytesInput definitionLevels,
BytesInput values,
int currentPageIndex) {
final DataPageV2 decompressedPage;
if (offsetIndex == null) {
decompressedPage = DataPageV2.uncompressed(
source.getRowCount(),
source.getNullCount(),
source.getValueCount(),
repetitionLevels,
definitionLevels,
source.getDataEncoding(),
values,
source.getStatistics());
} else {
decompressedPage = DataPageV2.uncompressed(
source.getRowCount(),
source.getNullCount(),
source.getValueCount(),
offsetIndex.getFirstRowIndex(currentPageIndex),
repetitionLevels,
definitionLevels,
source.getDataEncoding(),
values,
source.getStatistics());
}
if (source.getCrc().isPresent()) {
decompressedPage.setCrc(source.getCrc().getAsInt());
}
return decompressedPage;
}

private void setDecompressMetrics(BytesInput bytes, long start) {
final ParquetMetricsCallback metricsCallback = options.getMetricsCallback();
if (metricsCallback != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,13 @@ public void writePageV2(
}
if (null != pageBlockEncryptor) {
AesCipher.quickUpdatePageAAD(dataPageAAD, pageOrdinal);
compressedData = BytesInput.from(pageBlockEncryptor.encrypt(compressedData.toByteArray(), dataPageAAD));
// The levels are encrypted with the values as one module, which is written in place of all three
compressedData = BytesInput.from(pageBlockEncryptor.encrypt(
BytesInput.concat(repetitionLevels, definitionLevels, compressedData)
.toByteArray(),
dataPageAAD));
repetitionLevels = BytesInput.empty();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably for a later refactor, but I couldn't find a better shape here. We can basically rewrite a lot of this code or we can just null out rep and def levels and only use compressedData.

definitionLevels = BytesInput.empty();
}
int compressedSize =
toIntWithCheck(compressedData.size() + repetitionLevels.size() + definitionLevels.size());
Expand Down
Loading
Loading