Skip to content

Commit 679eb2d

Browse files
committed
Add Table Properties for Encryption Configuration
This PR introduces table-level encryption properties to enable configuration of encryption settings for Iceberg tables. These properties lay the groundwork for future encryption implementation while maintaining compatibility with the Java implementation's property names and structure. Table-level encryption is a critical security feature in Apache Iceberg's Java implementation. To support encryption in iceberg-rust and ensure interoperability between Java and Rust implementations, we need to start by adding the configuration properties that control encryption behavior. This PR adds the property definitions and parsing logic without implementing the actual encryption, keeping the change focused and reviewable. **Modified:** `crates/iceberg/src/spec/table_properties.rs` Added encryption-related properties to the `TableProperties` struct: - `PROPERTY_ENCRYPTION_KEY_ID` (`"encryption.key-id"`) - Master key ID for encrypting data encryption keys - `PROPERTY_ENCRYPTION_DEK_LENGTH` (`"encryption.data-key-length"`) - Data encryption key length (default: 16 bytes) - `PROPERTY_ENCRYPTION_AAD_LENGTH` (`"encryption.aad-length"`) - AAD prefix length for GCM (default: 16 bytes) - `PROPERTY_ENCRYPTION_KMS_TYPE` (`"encryption.kms-type"`) - KMS type (e.g., "aws", "gcp", "azure") All `Option<T>` as encryption is optional: - `encryption_key_id: Option<String>` - `encryption_dek_length: Option<usize>` - `encryption_aad_length: Option<usize>` - `encryption_kms_type: Option<String>` Extended `TryFrom<&HashMap<String, String>>` implementation to parse encryption properties Property names match exactly with Java's implementation: - Java: `TableProperties.ENCRYPTION_TABLE_KEY` → Rust: `PROPERTY_ENCRYPTION_KEY_ID` - Java: `TableProperties.ENCRYPTION_DEK_LENGTH` → Rust: `PROPERTY_ENCRYPTION_DEK_LENGTH` - Java: `CatalogProperties.ENCRYPTION_KMS_TYPE` → Rust: `PROPERTY_ENCRYPTION_KMS_TYPE` **Note:** Java's `ENCRYPTION_KMS_IMPL` property (for custom KMS implementations via reflection) is intentionally not included since Rust doesn't support runtime reflection. KMS implementations will be selected based on the `encryption.kms-type` property with compiled-in implementations. Added comprehensive test coverage: 1. `test_table_properties_default`: Verifies encryption properties are None by default 2. `test_encryption_properties_valid`: Tests parsing all encryption properties with valid values 3. `test_encryption_properties_partial`: Tests partial encryption configuration 4. `test_encryption_properties_invalid_numeric`: Verifies invalid numeric values are handled gracefully (parsed as None) 5. `test_encryption_properties_with_other_properties`: Tests encryption properties alongside existing table properties All tests pass: ``` running 7 tests test spec::table_properties::tests::test_table_properties_default ... ok test spec::table_properties::tests::test_encryption_properties_partial ... ok test spec::table_properties::tests::test_encryption_properties_invalid_numeric ... ok test spec::table_properties::tests::test_encryption_properties_valid ... ok test spec::table_properties::tests::test_encryption_properties_with_other_properties ... ok test spec::table_properties::tests::test_table_properties_valid ... ok test spec::table_properties::tests::test_table_properties_invalid ... ok ``` 1. **Optional Fields**: All encryption properties are `Option<T>` since encryption is an optional feature 2. **Silent Failure for Invalid Numbers**: Invalid numeric values for `dek_length` and `aad_length` are parsed as None rather than failing, matching the pattern for optional properties 3. **No Validation**: This PR doesn't validate property values (e.g., valid key lengths), leaving that for the encryption implementation 4. **No Custom KMS**: Omitted `encryption.kms-impl` property since Rust lacks reflection - KMS type selection will use `encryption.kms-type` with a factory pattern 5. **Independent PR**: No dependencies on other encryption code, can be merged independently This PR is part of a series to implement encryption support: - ✅ PR 1: Core encryption primitives (AES-GCM operations) - ✅ PR 2: Table properties for encryption (this PR) - PR 3: Key management interfaces - PR 4: EncryptionManager implementation - PR 5: Native Parquet encryption support - PR 6: Integration with Table and FileIO
1 parent b05a675 commit 679eb2d

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

crates/iceberg/src/spec/table_properties.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ pub struct TableProperties {
5151
pub write_target_file_size_bytes: usize,
5252
/// Whether to use `FanoutWriter` for partitioned tables.
5353
pub write_datafusion_fanout_enabled: bool,
54+
/// Master key ID for encryption. When set, all data and manifest files will be encrypted.
55+
pub encryption_key_id: Option<String>,
56+
/// Length of data encryption keys in bytes.
57+
pub encryption_dek_length: Option<usize>,
58+
/// Length of AAD (Additional Authenticated Data) prefix for GCM encryption.
59+
pub encryption_aad_length: Option<usize>,
60+
/// KMS type for encryption (e.g., "aws", "gcp", "azure").
61+
pub encryption_kms_type: Option<String>,
5462
}
5563

5664
impl TableProperties {
@@ -144,6 +152,37 @@ impl TableProperties {
144152
pub const PROPERTY_DATAFUSION_WRITE_FANOUT_ENABLED: &str = "write.datafusion.fanout.enabled";
145153
/// Default value for fanout writer enabled
146154
pub const PROPERTY_DATAFUSION_WRITE_FANOUT_ENABLED_DEFAULT: bool = true;
155+
156+
// Encryption properties
157+
158+
/// Master key ID for encrypting data encryption keys.
159+
///
160+
/// When set, enables table-level encryption where all data and manifest
161+
/// files are encrypted using data encryption keys (DEKs) that are
162+
/// themselves encrypted with this master key.
163+
pub const PROPERTY_ENCRYPTION_KEY_ID: &str = "encryption.key-id";
164+
165+
/// Length of data encryption keys in bytes.
166+
///
167+
/// Controls the key size for AES encryption. Common values are 16 (AES-128)
168+
/// and 32 (AES-256).
169+
pub const PROPERTY_ENCRYPTION_DEK_LENGTH: &str = "encryption.data-key-length";
170+
/// Default length for data encryption keys (16 bytes = AES-128).
171+
pub const PROPERTY_ENCRYPTION_DEK_LENGTH_DEFAULT: usize = 16;
172+
173+
/// Length of AAD (Additional Authenticated Data) prefix for GCM encryption.
174+
///
175+
/// Used to provide additional context for authenticated encryption modes
176+
/// like AES-GCM.
177+
pub const PROPERTY_ENCRYPTION_AAD_LENGTH: &str = "encryption.aad-length";
178+
/// Default AAD length (16 bytes).
179+
pub const PROPERTY_ENCRYPTION_AAD_LENGTH_DEFAULT: usize = 16;
180+
181+
/// KMS type for encryption.
182+
///
183+
/// Specifies which Key Management System to use. Common values include
184+
/// "aws" for AWS KMS, "gcp" for Google Cloud KMS, "azure" for Azure Key Vault.
185+
pub const PROPERTY_ENCRYPTION_KMS_TYPE: &str = "encryption.kms-type";
147186
}
148187

149188
impl TryFrom<&HashMap<String, String>> for TableProperties {
@@ -187,6 +226,17 @@ impl TryFrom<&HashMap<String, String>> for TableProperties {
187226
TableProperties::PROPERTY_DATAFUSION_WRITE_FANOUT_ENABLED,
188227
TableProperties::PROPERTY_DATAFUSION_WRITE_FANOUT_ENABLED_DEFAULT,
189228
)?,
229+
// Encryption properties - all optional
230+
encryption_key_id: props.get(TableProperties::PROPERTY_ENCRYPTION_KEY_ID).cloned(),
231+
encryption_dek_length: props
232+
.get(TableProperties::PROPERTY_ENCRYPTION_DEK_LENGTH)
233+
.and_then(|v| v.parse().ok()),
234+
encryption_aad_length: props
235+
.get(TableProperties::PROPERTY_ENCRYPTION_AAD_LENGTH)
236+
.and_then(|v| v.parse().ok()),
237+
encryption_kms_type: props
238+
.get(TableProperties::PROPERTY_ENCRYPTION_KMS_TYPE)
239+
.cloned(),
190240
})
191241
}
192242
}
@@ -219,6 +269,11 @@ mod tests {
219269
table_properties.write_target_file_size_bytes,
220270
TableProperties::PROPERTY_WRITE_TARGET_FILE_SIZE_BYTES_DEFAULT
221271
);
272+
// Encryption properties should be None by default
273+
assert_eq!(table_properties.encryption_key_id, None);
274+
assert_eq!(table_properties.encryption_dek_length, None);
275+
assert_eq!(table_properties.encryption_aad_length, None);
276+
assert_eq!(table_properties.encryption_kms_type, None);
222277
}
223278

224279
#[test]
@@ -293,4 +348,131 @@ mod tests {
293348
"Invalid value for write.target-file-size-bytes: invalid digit found in string"
294349
));
295350
}
351+
352+
#[test]
353+
fn test_encryption_properties_valid() {
354+
let props = HashMap::from([
355+
(
356+
TableProperties::PROPERTY_ENCRYPTION_KEY_ID.to_string(),
357+
"test-key-123".to_string(),
358+
),
359+
(
360+
TableProperties::PROPERTY_ENCRYPTION_DEK_LENGTH.to_string(),
361+
"32".to_string(),
362+
),
363+
(
364+
TableProperties::PROPERTY_ENCRYPTION_AAD_LENGTH.to_string(),
365+
"24".to_string(),
366+
),
367+
(
368+
TableProperties::PROPERTY_ENCRYPTION_KMS_TYPE.to_string(),
369+
"aws".to_string(),
370+
),
371+
]);
372+
let table_properties = TableProperties::try_from(&props).unwrap();
373+
assert_eq!(
374+
table_properties.encryption_key_id,
375+
Some("test-key-123".to_string())
376+
);
377+
assert_eq!(table_properties.encryption_dek_length, Some(32));
378+
assert_eq!(table_properties.encryption_aad_length, Some(24));
379+
assert_eq!(
380+
table_properties.encryption_kms_type,
381+
Some("aws".to_string())
382+
);
383+
}
384+
385+
#[test]
386+
fn test_encryption_properties_partial() {
387+
// Test with only some encryption properties set
388+
let props = HashMap::from([
389+
(
390+
TableProperties::PROPERTY_ENCRYPTION_KEY_ID.to_string(),
391+
"my-master-key".to_string(),
392+
),
393+
(
394+
TableProperties::PROPERTY_ENCRYPTION_KMS_TYPE.to_string(),
395+
"gcp".to_string(),
396+
),
397+
]);
398+
let table_properties = TableProperties::try_from(&props).unwrap();
399+
assert_eq!(
400+
table_properties.encryption_key_id,
401+
Some("my-master-key".to_string())
402+
);
403+
assert_eq!(table_properties.encryption_dek_length, None);
404+
assert_eq!(table_properties.encryption_aad_length, None);
405+
assert_eq!(
406+
table_properties.encryption_kms_type,
407+
Some("gcp".to_string())
408+
);
409+
}
410+
411+
#[test]
412+
fn test_encryption_properties_invalid_numeric() {
413+
// Test that invalid numeric values are silently ignored (parsed as None)
414+
let props = HashMap::from([
415+
(
416+
TableProperties::PROPERTY_ENCRYPTION_KEY_ID.to_string(),
417+
"key-456".to_string(),
418+
),
419+
(
420+
TableProperties::PROPERTY_ENCRYPTION_DEK_LENGTH.to_string(),
421+
"not-a-number".to_string(),
422+
),
423+
(
424+
TableProperties::PROPERTY_ENCRYPTION_AAD_LENGTH.to_string(),
425+
"also-not-a-number".to_string(),
426+
),
427+
]);
428+
let table_properties = TableProperties::try_from(&props).unwrap();
429+
assert_eq!(
430+
table_properties.encryption_key_id,
431+
Some("key-456".to_string())
432+
);
433+
// Invalid numeric values should be parsed as None
434+
assert_eq!(table_properties.encryption_dek_length, None);
435+
assert_eq!(table_properties.encryption_aad_length, None);
436+
}
437+
438+
#[test]
439+
fn test_encryption_properties_with_other_properties() {
440+
// Test encryption properties alongside other table properties
441+
let props = HashMap::from([
442+
(
443+
TableProperties::PROPERTY_COMMIT_NUM_RETRIES.to_string(),
444+
"8".to_string(),
445+
),
446+
(
447+
TableProperties::PROPERTY_DEFAULT_FILE_FORMAT.to_string(),
448+
"orc".to_string(),
449+
),
450+
(
451+
TableProperties::PROPERTY_ENCRYPTION_KEY_ID.to_string(),
452+
"combined-test-key".to_string(),
453+
),
454+
(
455+
TableProperties::PROPERTY_ENCRYPTION_DEK_LENGTH.to_string(),
456+
"16".to_string(),
457+
),
458+
(
459+
TableProperties::PROPERTY_ENCRYPTION_KMS_TYPE.to_string(),
460+
"azure".to_string(),
461+
),
462+
]);
463+
let table_properties = TableProperties::try_from(&props).unwrap();
464+
// Check regular properties
465+
assert_eq!(table_properties.commit_num_retries, 8);
466+
assert_eq!(table_properties.write_format_default, "orc".to_string());
467+
// Check encryption properties
468+
assert_eq!(
469+
table_properties.encryption_key_id,
470+
Some("combined-test-key".to_string())
471+
);
472+
assert_eq!(table_properties.encryption_dek_length, Some(16));
473+
assert_eq!(
474+
table_properties.encryption_kms_type,
475+
Some("azure".to_string())
476+
);
477+
}
296478
}

0 commit comments

Comments
 (0)