feat(encryption) [15/N] write encrypted parquet data files - #2701
feat(encryption) [15/N] write encrypted parquet data files#2701aarushigupta132 wants to merge 4 commits into
Conversation
xanderbailey
left a comment
There was a problem hiding this comment.
Thanks carrying the flame on the encryption work. Have made a couple of comments to get us started.
58df8f8 to
0783948
Compare
0783948 to
b80ebbf
Compare
cc5fca7 to
74c54db
Compare
| type R = ParquetWriter; | ||
|
|
||
| async fn build(&self, output_file: OutputFile) -> Result<Self::R> { | ||
| let key_metadata = self |
There was a problem hiding this comment.
Confirmed that in rolling_writer as well, we build this again with new dek for each new parquet file
| /// When writing into an existing Iceberg table, prefer | ||
| /// [`Self::from_table_properties`], which derives `WriterProperties` from | ||
| /// the table's `write.parquet.*` properties. |
There was a problem hiding this comment.
curious to know if we should deprecate new and new_with_match_mode in favor of from_table_properties. Or update the comment to prefer the latter in all cases?
There was a problem hiding this comment.
I would add a comment in the doc here that encrypted writing is not supported with this constructor. I would honestly be in favour of removing this constructor since it's not used in this crate and nor should it be. I don't think we should do that in this PR but it would be a good follow-up IMO.
xanderbailey
left a comment
There was a problem hiding this comment.
Few more comments and a test for the writer would be great. Maybe also something that shows datafusion writer works but that can be a follow up
| /// When writing into an existing Iceberg table, prefer | ||
| /// [`Self::from_table_properties`], which derives `WriterProperties` from | ||
| /// the table's `write.parquet.*` properties. |
There was a problem hiding this comment.
I would add a comment in the doc here that encrypted writing is not supported with this constructor. I would honestly be in favour of removing this constructor since it's not used in this crate and nor should it be. I don't think we should do that in this PR but it would be a good follow-up IMO.
5a0ea6f to
9832f50
Compare
9832f50 to
2ba0884
Compare
mbutrovich
left a comment
There was a problem hiding this comment.
First pass, thanks @aarushigupta132!
| let data_encryption_key_size = table_props | ||
| .encryption_key_id | ||
| .is_some() | ||
| .then(|| AesKeySize::from_key_length(table_props.encryption_data_key_length)) | ||
| .transpose()?; |
There was a problem hiding this comment.
This derives whether to encrypt purely from table_props.encryption_key_id.is_some(), independent of whether the Table this came from actually has an EncryptionManager.
Traced this through: today the two can't disagree at the point a Table is built. EncryptionManager::from_table_metadata (crates/iceberg/src/encryption/manager.rs:122-136) returns an error if encryption.key-id is set on the table's metadata but no KeyManagementClient was supplied to TableBuilder. So a freshly-built Table always has encryption_manager() in sync with table.metadata().table_properties().encryption_key_id.
But Table::with_metadata() (crates/iceberg/src/table.rs:212-215) swaps in new metadata without recomputing encryption_manager (that field is only set once, in TableBuilder::build()). If a commit turns on encryption.key-id for the first time and the caller keeps using the same in-memory Table handle afterward (via with_metadata) rather than reloading from the catalog, ParquetWriterBuilder::from_table_properties would see the new encryption_key_id and start minting DEKs and encrypting data files, while transaction/snapshot.rs's manifest writer (which branches on self.table.encryption_manager(), e.g. crates/iceberg/src/transaction/snapshot.rs:257) would still take the unencrypted path off the stale cached value. That would leave a freshly generated, unwrapped per-file DEK sitting in a plaintext manifest, the thing the two-layer envelope design is built to avoid.
This is in table.rs, outside this PR's diff, so it isn't this PR's bug to fix, and it's possible normal usage always reloads the Table from the catalog after a property-changing commit and never hits this. Worth either confirming that path is genuinely unreachable, or filing an issue to make with_metadata keep encryption_manager in sync (or recompute it lazily from current metadata + a stored kms_client, rather than caching once at construction).
There was a problem hiding this comment.
Yeah I thought about this earlier this week. Fortunately in the case of encryption.key-id the spec notes that it should not be changed or removed for the lifetime of the table. So it's existence is immutable per the spec. https://iceberg.apache.org/docs/nightly/encryption/#catalog-security-requirements .
I need to think a little more deeply about the case where you have encryption keys added in memory in the manager and then you refresh the table before the commit which risks loosing those keys but I think we're safe from that today.
There was a problem hiding this comment.
That being said I'll have a slightly deeper think about how we might improve our robustness here a little.
| /// parquet-rs defaults. | ||
| pub fn from_table_properties(table_props: &TableProperties, schema: SchemaRef) -> Self { | ||
| /// | ||
| /// When `encryption.key-id` is set, records the DEK length. |
There was a problem hiding this comment.
Doc comment above this says "When encryption.key-id is set, records the DEK length." Accurate, but might be worth a second line noting that the DEK itself is generated per-file in build() (line ~138), not here: from_table_properties only records the size to use later. Minor, only raising it because it took a moment to trace where the actual key material gets minted while checking comment above.
| table.metadata().current_schema(), | ||
| )?; | ||
|
|
||
| // The planner encrypted the file. |
There was a problem hiding this comment.
Not sure what this means
Which issue does this PR close?
Working towards #2034.
What changes are included in this PR?
Adds support for writing Iceberg data files with Parquet Modular Encryption.
When a table is configured for encryption,
ParquetWriterBuilder::from_table_propertiesderives the data-encryption key size from the table properties.ParquetWriterthen mints a fresh per-file DEK, writes the file encrypted, and records the encoded key metadata on the resultingDataFileso readers can decrypt.Are these changes tested?
Existing
ParquetWritertests pass; encryption round-trip coverage lands later in the series.