Skip to content

feat(encryption) [5/N] Support encryption: Encryption Manager#2383

Open
xanderbailey wants to merge 3 commits intoapache:mainfrom
xanderbailey:xb/encryption_manager
Open

feat(encryption) [5/N] Support encryption: Encryption Manager#2383
xanderbailey wants to merge 3 commits intoapache:mainfrom
xanderbailey:xb/encryption_manager

Conversation

@xanderbailey
Copy link
Copy Markdown
Contributor

@xanderbailey xanderbailey commented Apr 28, 2026

Which issue does this PR close?

Part of #2034

What changes are included in this PR?

Stacked on #2340. Adds EncryptionManager — handles two-layer envelope encryption (master key → KEK → DEK) for Iceberg tables.

New files

  • encryption_manager.rsEncryptionManager with typed_builder construction:
    • encrypt() / decrypt() — AGS1 stream file encryption
    • generate_native_key_metadata() — generates StandardKeyMetadata (DEK + AAD prefix) for Parquet Modular Encryption (PME). Callers pass this directly to the Parquet writer's FileEncryptionProperties and serialize it for manifest storage.
    • wrap_key_metadata() / unwrap_key_metadata() — KEK envelope wrapping for manifest list key metadata
    • KEK lifecycle: creation, rotation (730-day NIST SP 800-57 lifespan), caching (1-hour TTL via moka)
  • encrypted_io.rsEncryptedInputFile, EncryptedOutputFile wrappers for transparent AGS1 stream encrypt/decrypt on read/write

Design decisions

  • No NativeEncryptedInputFile / NativeEncryptedOutputFile wrappers — For PME, StandardKeyMetadata already carries the DEK and AAD prefix. Wrapper types that just bundle an InputFile/OutputFile with the same data are unnecessary indirection. Readers deserialize StandardKeyMetadata directly from key_metadata bytes; writers receive (OutputFile, &StandardKeyMetadata) separately.
  • generate_native_key_metadata() is sync — unlike AGS1 encryption, PME key generation doesn't touch KMS. It just generates random bytes locally.

How this differs from Java's StandardEncryptionManager

  • KEK management is explicit: Java's addManifestListKeyMetadata() mutates an internal map and callers need to downcast to StandardEncryptionManager to access the keys. Our wrap_key_metadata() returns (wrapped_key, Option<new_kek>) directly. no hidden mutation, no downcasting. Java
  • No EncryptionUtil grab-bag: Java needs a static utility class (decryptManifestListKeyMetadata, encryptManifestListKeyMetadata, etc.) because the interface is too narrow. Here those are just methods on EncryptionManager.
  • table_key_id enforced at compile time: Required via typed_builder, can't forget it. Unencrypted tables use Option<EncryptionManager> instead of Java's PlaintextEncryptionManager.
  • Config values match Java exactly: KEK lifespan (730 days), cache TTL (1 hour), AAD prefix length (16 bytes) are all hardcoded.

Notes on usage for how this is used:
load_manifest_list does:

                let unwrapped_km = em
                    .unwrap_key_metadata(encrypted_key, table_metadata.encryption_keys_map())
                    .await?;
                let input = file_io.new_input(&self.manifest_list)?;
                em.decrypt(input, &unwrapped_km).await?.read().await?

load_manifest does:

        let avro = match &self.key_metadata {
            Some(km) if !km.is_empty() => {
                let em = encryption_manager.ok_or_else(|| {
                    Error::new(
                        ErrorKind::FeatureUnsupported,
                        "Encrypted manifest but no EncryptionManager configured on table",
                    )
                })?;
                let input = file_io.new_input(&self.manifest_path)?;
                em.decrypt(input, km).await?.read().await?
            }
            _ => file_io.new_input(&self.manifest_path)?.read().await?,
        };

        let (metadata, mut entries) = Manifest::try_from_avro_bytes(&avro)?;

Are these changes tested?

Yes

@xanderbailey xanderbailey changed the title feat(encryption) [4/N] Support encryption: Encryption Manager feat(encryption) [5/N] Support encryption: Encryption Manager Apr 28, 2026
@xanderbailey xanderbailey force-pushed the xb/encryption_manager branch from b7fe819 to 74f9e2f Compare April 29, 2026 08:44
default = moka::future::Cache::builder().time_to_live(DEFAULT_CACHE_TTL).build(),
setter(skip)
)]
kek_cache: moka::future::Cache<String, SensitiveBytes>,
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.

Put a cache here for the same reason Java does, KMS is not aware of the key_id and since we populate the cache when we create_kek it would be hard to do this purely within a CachingKmsService

@xanderbailey xanderbailey marked this pull request as draft April 29, 2026 20:16
@xanderbailey xanderbailey marked this pull request as ready for review April 29, 2026 21:15
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.

1 participant