Skip to content

feat(encryption): [11/N] Read encrypted Parquet data files - #4309

Merged
kevinjqliu merged 2 commits into
apache:mainfrom
xanderbailey:xb/encryption-read-parquet
Nov 9, 2026
Merged

kevinjqliu merged 2 commits into
apache:mainfrom
xanderbailey:xb/encryption-read-parquet

Conversation

@xanderbailey

Copy link
Copy Markdown
Contributor

Working towards #3222

Rationale for this change

We need to be able to read data files encrypted with Parquet Modular Encryption.

  • A data file's key_metadata in its manifest entry holds the plaintext key and AAD prefix for that file. The manifest holding it is itself encrypted, so this doesn't need the EncryptionManager or the KMS.
  • When key_metadata is set, scans build the Parquet file format with decryption properties from PyArrow's direct key API, pyarrow.parquet.encryption.create_decryption_properties (GH-47435: [Python][Parquet] Add direct key encryption/decryption API arrow#49667). This matches iceberg-rust, which builds arrow-rs FileDecryptionProperties from the key and AAD prefix ([9/N] feat(encryption) [9/N] Read encrypted parquet data-files iceberg-rust#2584).
  • This covers data files and Parquet position delete files. Encrypted ORC files raise, and deletion vectors come in a follow-up.
  • The AAD prefix always comes from key metadata, because Java doesn't store it in the file.

The direct key API was added in PyArrow 25, so reading an encrypted file on an older PyArrow raises an ImportError asking for 25.0.0 or later. The minimum PyArrow version for everything else is unchanged.

Are these changes tested?

Yes. A Parquet data file encrypted by iceberg-rust is checked in under tests/table/encryption, and reading it with its key metadata returns its rows. There's also a round trip of an encrypted data file with an encrypted position delete file. Reads fail with a wrong key, a wrong AAD prefix, missing key metadata, or key metadata on a plaintext file.

Are there any user-facing changes?

No. Nothing changes for unencrypted tables.

@xanderbailey
xanderbailey marked this pull request as ready for review November 8, 2026 18:24
Comment thread pyiceberg/io/pyarrow.py Outdated
from pyarrow.parquet import encryption

MIN_PYARROW_VERSION_SUPPORTING_PARQUET_DECRYPTION = "25.0.0"
if version.parse(pyarrow.__version__) < version.parse(MIN_PYARROW_VERSION_SUPPORTING_PARQUET_DECRYPTION):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not 100% sure what the blessed way to do this is...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yea i think we do this pattern in a few places already.

its not pretty but its what we got 🤷

@kevinjqliu kevinjqliu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I pushed up a small fix in 1ae3dd3

  • Combined _get_file_format and _get_data_file_format into a single _create_arrow_read_format(file_format, key_metadata, **kwargs), with key_metadata required so callers can't leave it out
  • Removed lru_cache so per-file keys aren't kept in memory (building a format costs ~1µs)
  • Detect PyArrow decryption support by trying the import instead of comparing version numbers (no packaging import, and dev builds work)
  • Reject encrypted non-Parquet files, including Puffin deletion vectors, with the file path in the error
  • Updated tests to match

LMK if you agree, and feel free to make changes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thanks for keeping this small 😄

as i mentioned in the other pr, would be good to add a script to be able to regenerate these test files

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Let me open an issue so I don’t forget

@xanderbailey

Copy link
Copy Markdown
Contributor Author

I’m happy with the changes, thanks @kevinjqliu

@xanderbailey

Copy link
Copy Markdown
Contributor Author

Will rebase on main

xanderbailey and others added 2 commits November 9, 2026 16:20
A data file's key_metadata holds its Parquet encryption key and AAD prefix,
so scans decrypt with pyarrow's direct key API when it is set, as iceberg-rust
does. That API needs pyarrow 25.
Merge format selection and Parquet decryption into _create_arrow_read_format,
dropping the lru_cache so per-file keys are not retained. Detect PyArrow
decryption support by import instead of parsing the version. Reject encrypted
non-Parquet files, including deletion vectors, with the file path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread pyiceberg/io/pyarrow.py
def _create_arrow_read_format(file_format: FileFormat, key_metadata: bytes | None, **kwargs: Any) -> ds.FileFormat:
if file_format == FileFormat.PARQUET:
if key_metadata is not None:
kwargs["decryption_properties"] = _parquet_decryption_properties(key_metadata)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread pyiceberg/io/pyarrow.py

@lru_cache
def _get_file_format(file_format: FileFormat, **kwargs: dict[str, Any]) -> ds.FileFormat:
def _create_arrow_read_format(file_format: FileFormat, key_metadata: bytes | None, **kwargs: Any) -> ds.FileFormat:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we could consider making key_metadata required so future callers wont forget to include it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is sorted now right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looks great, thanks!

@xanderbailey
xanderbailey force-pushed the xb/encryption-read-parquet branch from 1ae3dd3 to 0b2f6e5 Compare November 9, 2026 21:31
Comment thread pyiceberg/io/pyarrow.py

@lru_cache
def _get_file_format(file_format: FileFormat, **kwargs: dict[str, Any]) -> ds.FileFormat:
def _create_arrow_read_format(file_format: FileFormat, key_metadata: bytes | None, **kwargs: Any) -> ds.FileFormat:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looks great, thanks!

@kevinjqliu
kevinjqliu added this pull request to the merge queue Nov 9, 2026
@kevinjqliu

Copy link
Copy Markdown
Contributor

thanks again @xanderbailey 🚀

Merged via the queue into apache:main with commit 1310a19 Nov 9, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants